AutoxJS避坑指南:从按钮定位到日志记录的8个实用技巧
AutoxJS实战进阶:从脚本稳定到性能优化的深度避坑手册
如果你已经用AutoxJS写过几个自动化脚本,尝到了解放双手的甜头,那么接下来大概率会遇到一些“成长的烦恼”。按钮死活点不到、脚本运行一半卡死、日志混乱难以排查……这些问题看似琐碎,却足以让一个精心设计的自动化流程功亏一篑。今天我们不谈基础操作,只聚焦于那些在真实项目打磨中才会暴露的深水区问题,分享一套经过实战检验的解决方案与高阶技巧。
1. 界面元素定位:超越id()和text()的稳定策略
几乎所有AutoxJS脚本的起点都是与屏幕上的元素交互。当id("btn_confirm").findOne()这种理想化的定位方式失效时,很多开发者会陷入反复调整选择器的困境。实际上,界面元素的稳定性受安卓版本、应用框架、动态加载机制等多重因素影响,需要一套更鲁棒的定位策略。
1.1 应对clickable为false的组件点击
在高版本安卓系统或某些特定UI框架(如Flutter、React Native)中,组件的clickable属性可能被设置为false,导致直接通过click()方法无效。此时,获取组件边界坐标进行模拟点击是更可靠的方法。
// 传统方式可能失效
let targetBtn = id("submit_button").findOne();
targetBtn.click(); // 可能无反应
// 改用坐标点击
let targetBtn = text("提交").findOne();
if (targetBtn) {
let bounds = targetBtn.bounds();
click(bounds.centerX(), bounds.centerY());
log("通过坐标点击成功");
}
但坐标点击也有其局限性:屏幕分辨率变化、UI缩放、动态布局都可能让坐标失效。一个更健壮的方案是结合多种定位属性,并添加容错机制。
function robustClick(selector, fallbackText = null) {
let element = selector.findOne(3000); // 等待3秒
if (!element && fallbackText) {
// 如果首选选择器失败,尝试备用文本
element = text(fallbackText).findOne(2000);
}
if (element) {
// 先尝试直接点击
if (element.click()) {
log("直接点击成功");
return true;
}
// 直接点击失败,尝试坐标点击
let bounds = element.bounds();
if (bounds) {
click(bounds.centerX(), bounds.centerY());
sleep(500); // 点击后等待
// 验证点击是否生效(例如通过界面变化)
return true;
}
}
log("元素定位或点击失败");
return false;
}
// 使用示例
robustClick(id("action_button"), "确认");
1.2 动态加载内容的等待策略
现代应用大量使用异步加载和懒加载技术,这意味着你需要的元素可能不会立即出现在屏幕上。waitFor()方法虽然常用,但在某些复杂场景下可能不够可靠。
注意:过度使用
sleep()进行固定时间等待会降低脚本效率,而轮询检查又会增加性能开销。需要根据具体场景平衡。
我推荐使用自适应等待策略,结合多种条件判断:
function waitForElement(selectors, timeout = 10000, checkInterval = 500) {
let startTime = Date.now();
let foundElement = null;
while (Date.now() - startTime < timeout) {
// 尝试多种选择器
for (let selector of selectors) {
let element = selector.findOne(100);
if (element) {
// 额外检查元素是否真正可用
if (isElementReady(element)) {
foundElement = element;
break;
}
}
}
if (foundElement) break;
sleep(checkInterval);
}
return foundElement;
}
function isElementReady(element) {
// 检查元素是否在屏幕可见区域
let bounds = element.bounds();
if (!bounds) return false;
// 检查元素是否被遮挡(简单版本)
let screenHeight = device.height;
let screenWidth = device.width;
return bounds.top >= 0 &&
bounds.left >= 0 &&
bounds.bottom <= screenHeight &&
bounds.right <= screenWidth;
}
// 使用示例:多种方式定位同一个元素
let submitButton = waitForElement([
id("btn_submit"),
textContains("提交"),
className("android.widget.Button").clickable(true)
], 15000);
if (submitButton) {
submitButton.click();
}
1.3 复杂列表和滚动界面的元素查找
在长列表或可滚动视图中定位特定项目时,简单的findOne()可能无法找到屏幕外的元素。这时需要结合滚动操作:
function findInScrollable(selector, scrollableSelector, maxScrolls = 10) {
let scrollable = scrollableSelector.findOne(3000);
if (!scrollable) {
log("未找到可滚动容器");
return null;
}
for (let i = 0; i < maxScrolls; i++) {
// 在当前屏幕查找
let target = selector.findOne(1000);
if (target) return target;
// 未找到,向下滚动
let scrollBounds = scrollable.bounds();
let startY = scrollBounds.bottom - 100;
let endY = scrollBounds.top + 100;
swipe(scrollBounds.centerX(), startY,
scrollBounds.centerX(), endY, 500);
sleep(800); // 等待滚动动画
// 防止无限滚动,检查是否已到底部
if (i > 2 && isScrollAtBottom(scrollable)) {
break;
}
}
return null;
}
function isScrollAtBottom(scrollable) {
// 简单实现:检查滚动位置(实际可能需要更复杂的判断)
let info = scrollable.scrollInfo();
return info && info.vertical &&
Math.abs(info.vertical.position - info.vertical.range) < 10;
}
2. 脚本稳定性:避免主线程阻塞与异常处理
AutoxJS脚本运行在JavaScript环境中,虽然不像传统编程那样有严格的多线程要求,但不当的代码结构仍然会导致脚本“假死”或响应缓慢。
2.1 识别和避免主线程阻塞
主线程阻塞通常由以下原因引起:
- 长时间的同步循环操作
- 未设置超时的网络请求
- 复杂的同步计算任务
- 不当的
sleep()使用
// 问题代码:同步处理大量数据
function processLargeData(dataArray) {
let results = [];
for (let i = 0; i < dataArray.length; i++) {
// 每个处理都很耗时
let result = complexCalculation(dataArray[i]);
results.push(result);
}
return results; // 在处理完成前,脚本无法响应其他事件
}
// 改进方案:使用分片处理
async function processLargeDataSafely(dataArray, chunkSize = 50) {
let results = [];
for (let i = 0; i < dataArray.length; i += chunkSize) {
let chunk = dataArray.slice(i, i + chunkSize);
let chunkResults = await processChunk(chunk);
results.push(...chunkResults);
// 每处理完一个分片,让出控制权
if (i + chunkSize < dataArray.length) {
await delay(100); // 短暂延迟
}
}
return results;
}
function processChunk(chunk) {
return new Promise(resolve => {
threads.start(function() {
let results = chunk.map(item => complexCalculation(item));
resolve(results);
});
});
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
2.2 实现可靠的超时控制机制
为关键操作添加超时控制是提升脚本稳定性的重要手段。下面是一个通用的带超时的操作包装器:
class TimeoutController {
constructor(timeout = 30000) {
this.timeout = timeout;
this.timer = null;
this.isTimeout = false;
}
async execute(operation) {
return new Promise((resolve, reject) => {
// 设置超时计时器
this.timer = setTimeout(() => {
this.isTimeout = true;
reject(new Error(`操作超时 (${this.timeout}ms)`));
}, this.timeout);
// 执行实际操作
try {
let result = operation();
if (!this.isTimeout) {
clearTimeout(this.timer);
resolve(result);
}
} catch (error) {
if (!this.isTimeout) {
clearTimeout(this.timer);
reject(error);
}
}
});
}
cancel() {
if (this.timer) {
clearTimeout(this.timer);
}
}
}
// 使用示例
async function safeNetworkRequest(url, options) {
let controller = new TimeoutController(10000);
try {
let response = await controller.execute(() => {
return http.get(url, options);
});
return response;
} catch (error) {
log(`请求失败: ${error.message}`);
// 根据错误类型决定重试或退出
if (error.message.includes('超时')) {
// 超时重试逻辑
return retryRequest(url, options);
}
throw error;
}
}
2.3 异常恢复与状态持久化
当脚本因异常中断时,能够从断点恢复是专业脚本的标志。实现这一功能需要状态管理:
class ScriptStateManager {
constructor(storageKey = 'script_state') {
this.storageKey = storageKey;
this.state = this.loadState() || {
currentStep: 0,
lastSuccessTime: null,
errorCount: 0,
customData: {}
};
}
loadState() {
try {
let saved = storages.create(this.storageKey);
let stateStr = saved.get('state', '{}');
return JSON.parse(stateStr);
} catch (e) {
log(`加载状态失败: ${e}`);
return null;
}
}
saveState() {
try {
let saved = storages.create(this.storageKey);
saved.put('state', JSON.stringify(this.state));
return true;
} catch (e) {
log(`保存状态失败: ${e}`);
return false;
}
}
updateStep(step, data = {}) {
this.state.currentStep = step;
Object.assign(this.state.customData, data);
this.state.lastSuccessTime = new Date().toISOString();
this.saveState();
}
recordError(error) {
this.state.errorCount = (this.state.errorCount || 0) + 1;
this.state.lastError = {
message: error.message,
time: new Date().toISOString(),
stack: error.stack
};
this.saveState();
}
shouldRetry(maxErrors = 3) {
return (this.state.errorCount || 0) < maxErrors;
}
reset() {
this.state = {
currentStep: 0,
lastSuccessTime: null,
errorCount: 0,
customData: {}
};
this.saveState();
}
}
// 在脚本中使用
let stateManager = new ScriptStateManager('my_script_v1');
try {
switch(stateManager.state.currentStep) {
case 0:
await step1();
stateManager.updateStep(1);
case 1:
await step2();
stateManager.updateStep(2);
case 2:
await step3();
stateManager.updateStep(0); // 完成循环
}
} catch (error) {
stateManager.recordError(error);
if (stateManager.shouldRetry()) {
log(`步骤${stateManager.state.currentStep}失败,准备重试`);
// 等待后重试当前步骤
sleep(5000);
continue;
} else {
log(`错误次数过多,停止脚本`);
exit();
}
}
3. 日志系统:从简单输出到结构化监控
在开发阶段,console.log()和toast()可能足够使用。但在生产环境中,你需要一个能够持久化、可查询、分等级的完整日志系统。
3.1 实现分级日志系统
const LogLevel = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
CRITICAL: 4
};
class Logger {
constructor(options = {}) {
this.level = options.level || LogLevel.INFO;
this.enableConsole = options.enableConsole !== false;
this.enableFile = options.enableFile || false;
this.logFile = options.logFile || '/sdcard/autoxjs_logs/';
this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB
this.context = options.context || 'main';
this.ensureLogDirectory();
}
ensureLogDirectory() {
if (this.enableFile) {
files.ensureDir(this.logFile);
}
}
log(level, message, data = null) {
if (level < this.level) return;
const levelNames = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'];
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: levelNames[level],
context: this.context,
message,
data,
device: device.brand + ' ' + device.model,
appVersion: app.autojs.versionName
};
const logString = JSON.stringify(logEntry);
// 控制台输出
if (this.enableConsole) {
let consoleMethod = level >= LogLevel.ERROR ? console.error :
level >= LogLevel.WARN ? console.warn : console.log;
consoleMethod(`[${levelNames[level]}] ${message}`, data || '');
}
// 文件输出
if (this.enableFile) {
this.writeToFile(logString);
}
// 重要日志显示Toast
if (level >= LogLevel.ERROR) {
toastLog(`${levelNames[level]}: ${message}`);
}
return logEntry;
}
writeToFile(logString) {
try {
let date = new Date();
let fileName = `log_${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}.json`;
let filePath = files.join(this.logFile, fileName);
// 检查文件大小
if (files.exists(filePath)) {
let size = files.getSize(filePath);
if (size > this.maxFileSize) {
// 文件过大,创建新的
let backupPath = filePath.replace('.json', `_${Date.now()}.json`);
files.copy(filePath, backupPath);
files.remove(filePath);
}
}
// 写入日志
files.append(filePath, logString + '\n');
} catch (e) {
console.error('写入日志文件失败:', e);
}
}
debug(message, data) {
return this.log(LogLevel.DEBUG, message, data);
}
info(message, data) {
return this.log(LogLevel.INFO, message, data);
}
warn(message, data) {
return this.log(LogLevel.WARN, message, data);
}
error(message, data) {
return this.log(LogLevel.ERROR, message, data);
}
critical(message, data) {
return this.log(LogLevel.CRITICAL, message, data);
}
}
// 使用示例
let logger = new Logger({
level: LogLevel.DEBUG,
enableFile: true,
context: 'checkin_script'
});
try {
logger.info('脚本启动', {time: Date.now()});
let result = await performCheckin();
logger.info('打卡成功', result);
} catch (error) {
logger.error('打卡失败', {
error: error.message,
stack: error.stack,
lastAction: '点击确认按钮'
});
}
3.2 日志分析与性能监控
有了结构化的日志,我们可以进一步实现性能监控和异常分析:
class PerformanceMonitor {
constructor(logger) {
this.logger = logger;
this.markers = new Map();
this.metrics = {
operations: [],
errors: [],
performance: []
};
}
startMark(name) {
this.markers.set(name, {
start: Date.now(),
memory: this.getMemoryUsage()
});
}
endMark(name, metadata = {}) {
if (!this.markers.has(name)) {
this.logger.warn(`未找到开始标记: ${name}`);
return;
}
let mark = this.markers.get(name);
let duration = Date.now() - mark.start;
let memoryDiff = this.getMemoryUsage() - mark.memory;
let metric = {
name,
duration,
memoryDiff,
timestamp: new Date().toISOString(),
...metadata
};
this.metrics.performance.push(metric);
this.markers.delete(name);
// 如果操作时间过长,记录警告
if (duration > 5000) { // 5秒阈值
this.logger.warn(`操作 ${name} 耗时过长`, metric);
}
return metric;
}
getMemoryUsage() {
// 简单的内存使用估算
if (typeof performance !== 'undefined' && performance.memory) {
return performance.memory.usedJSHeapSize;
}
return 0;
}
recordOperation(name, success, data = {}) {
let operation = {
name,
success,
timestamp: new Date().toISOString(),
...data
};
this.metrics.operations.push(operation);
if (!success) {
this.metrics.errors.push(operation);
this.logger.error(`操作失败: ${name}`, operation);
}
return operation;
}
generateReport() {
let totalOps = this.metrics.operations.length;
let successOps = this.metrics.operations.filter(op => op.success).length;
let successRate = totalOps > 0 ? (successOps / totalOps * 100).toFixed(2) : 0;
let avgDuration = 0;
if (this.metrics.performance.length > 0) {
let totalDuration = this.metrics.performance.reduce((sum, m) => sum + m.duration, 0);
avgDuration = totalDuration / this.metrics.performance.length;
}
return {
summary: {
totalOperations: totalOps,
successfulOperations: successOps,
successRate: successRate + '%',
averageDuration: avgDuration.toFixed(2) + 'ms',
errorCount: this.metrics.errors.length,
monitoringPeriod: this.getMonitoringPeriod()
},
recentErrors: this.metrics.errors.slice(-5),
slowOperations: this.metrics.performance
.filter(m => m.duration > 1000)
.sort((a, b) => b.duration - a.duration)
.slice(0, 5)
};
}
getMonitoringPeriod() {
if (this.metrics.operations.length === 0) return '无数据';
let first = this.metrics.operations[0].timestamp;
let last = this.metrics.operations[this.metrics.operations.length - 1].timestamp;
return `${first} 至 ${last}`;
}
}
// 集成使用
let logger = new Logger({enableFile: true});
let monitor = new PerformanceMonitor(logger);
async function monitoredOperation(name, operation) {
monitor.startMark(name);
try {
let result = await operation();
monitor.endMark(name, {success: true});
monitor.recordOperation(name, true, {result: 'success'});
return result;
} catch (error) {
monitor.endMark(name, {success: false, error: error.message});
monitor.recordOperation(name, false, {error: error.message});
throw error;
}
}
// 在脚本关键点使用
await monitoredOperation('应用启动', () => launchApp('目标应用'));
await monitoredOperation('登录流程', () => performLogin());
await monitoredOperation('数据提交', () => submitData());
// 生成报告
let report = monitor.generateReport();
logger.info('性能报告', report);
4. 高级技巧:提升脚本的适应性与可维护性
4.1 配置驱动与环境适配
硬编码的参数会让脚本难以维护和适应不同环境。通过配置系统,我们可以让脚本更加灵活:
class ConfigManager {
constructor(defaultConfig = {}) {
this.defaultConfig = {
// 超时设置
timeouts: {
elementWait: 10000,
networkRequest: 30000,
operation: 60000
},
// 重试策略
retry: {
maxAttempts: 3,
delay: 2000,
backoffFactor: 1.5
},
// 界面交互
ui: {
clickDelay: 300,
swipeDuration: 500,
screenshotOnError: true
},
// 功能开关
features: {
enableLogging: true,
enableScreenshots: false,
enableNotifications: true
},
// 应用特定配置
appSpecific: {}
};
// 合并默认配置和传入配置
Object.assign(this.defaultConfig, defaultConfig);
// 尝试加载用户配置
this.userConfig = this.loadUserConfig();
this.currentConfig = this.mergeConfigs();
}
loadUserConfig() {
try {
let configPath = '/sdcard/autoxjs_config/config.json';
if (files.exists(configPath)) {
let content = files.read(configPath);
return JSON.parse(content);
}
} catch (e) {
console.warn('加载用户配置失败,使用默认配置:', e);
}
return {};
}
mergeConfigs() {
// 深度合并配置
function deepMerge(target, source) {
for (let key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
target[key] = deepMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
return deepMerge(JSON.parse(JSON.stringify(this.defaultConfig)), this.userConfig);
}
get(keyPath, defaultValue = null) {
let keys = keyPath.split('.');
let value = this.currentConfig;
for (let key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return defaultValue;
}
}
return value !== undefined ? value : defaultValue;
}
set(keyPath, value) {
let keys = keyPath.split('.');
let config = this.currentConfig;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in config) || typeof config[keys[i]] !== 'object') {
config[keys[i]] = {};
}
config = config[keys[i]];
}
config[keys[keys.length - 1]] = value;
this.saveUserConfig();
}
saveUserConfig() {
try {
let configPath = '/sdcard/autoxjs_config/';
files.ensureDir(configPath);
files.write(
files.join(configPath, 'config.json'),
JSON.stringify(this.userConfig, null, 2)
);
return true;
} catch (e) {
console.error('保存用户配置失败:', e);
return false;
}
}
// 环境适配
adaptToEnvironment() {
let envConfig = {
deviceType: device.brand.toLowerCase().includes('xiaomi') ? 'xiaomi' : 'other',
androidVersion: parseInt(device.release.split('.')[0]),
screenDensity: Math.min(device.width, device.height) / 160
};
// 根据设备类型调整配置
if (envConfig.deviceType === 'xiaomi') {
// 小米设备可能需要不同的延迟
this.set('ui.clickDelay', 500);
}
if (envConfig.androidVersion >= 10) {
// Android 10+ 可能需要额外的权限处理
this.set('features.requireSpecialPermissions', true);
}
return envConfig;
}
}
// 在脚本中使用配置
let config = new ConfigManager();
let env = config.adaptToEnvironment();
// 使用配置值
let waitTimeout = config.get('timeouts.elementWait');
let maxRetries = config.get('retry.maxAttempts');
// 动态调整配置
if (env.androidVersion >= 11) {
config.set('ui.swipeDuration', 700); // Android 11需要更长的滑动时间
}
4.2 模块化与插件系统
随着脚本复杂度增加,将功能模块化可以大幅提升可维护性:
// 基础插件类
class ScriptPlugin {
constructor(name, config = {}) {
this.name = name;
this.config = config;
this.enabled = true;
this.logger = null;
}
setLogger(logger) {
this.logger = logger;
}
log(level, message, data) {
if (this.logger) {
this.logger.log(level, `[${this.name}] ${message}`, data);
}
}
async initialize() {
// 子类重写
}
async execute(context) {
// 子类重写
return context;
}
async cleanup() {
// 子类重写
}
}
// 具体插件实现
class ScreenshotPlugin extends ScriptPlugin {
constructor(config) {
super('screenshot', {
enabled: true,
savePath: '/sdcard/autoxjs_screenshots/',
onError: true,
onSuccess: false,
...config
});
}
async initialize() {
files.ensureDir(this.config.savePath);
this.log('info', '截图插件初始化完成');
}
async execute(context) {
if (!this.enabled || !this.config.onSuccess) return context;
try {
let timestamp = new Date().toISOString().replace(/[:.]/g, '-');
let filename = `success_${timestamp}.png`;
let filepath = files.join(this.config.savePath, filename);
captureScreen(filepath);
this.log('debug', `截图已保存: ${filepath}`);
context.lastScreenshot = filepath;
} catch (error) {
this.log('warn', `截图失败: ${error.message}`);
}
return context;
}
}
class ErrorHandlerPlugin extends ScriptPlugin {
constructor(config) {
super('error_handler', {
enabled: true,
maxRetries: 3,
retryDelay: 2000,
fallbackActions: [],
...config
});
this.retryCount = 0;
}
async execute(context) {
if (!context.error) return context;
this.log('error', `处理错误: ${context.error.message}`);
if (this.retryCount < this.config.maxRetries) {
this.retryCount++;
this.log('info', `准备第 ${this.retryCount} 次重试`);
await sleep(this.config.retryDelay);
context.shouldRetry = true;
} else {
this.log('critical', '重试次数用尽,执行备用方案');
for (let action of this.config.fallbackActions) {
try {
await action(context);
} catch (fallbackError) {
this.log('error', `备用方案失败: ${fallbackError.message}`);
}
}
context.shouldExit = true;
}
return context;
}
}
// 插件管理器
class PluginManager {
constructor() {
this.plugins = new Map();
this.executionOrder = [];
}
register(plugin) {
this.plugins.set(plugin.name, plugin);
this.executionOrder.push(plugin.name);
return this;
}
setLogger(logger) {
for (let plugin of this.plugins.values()) {
plugin.setLogger(logger);
}
return this;
}
async initializeAll() {
for (let name of this.executionOrder) {
let plugin = this.plugins.get(name);
if (plugin.enabled) {
await plugin.initialize();
}
}
}
async executeAll(context = {}) {
for (let name of this.executionOrder) {
let plugin = this.plugins.get(name);
if (plugin.enabled) {
context = await plugin.execute(context);
// 检查是否需要提前退出
if (context.shouldExit) {
break;
}
}
}
return context;
}
async cleanupAll() {
for (let name of this.executionOrder.reverse()) {
let plugin = this.plugins.get(name);
if (plugin.enabled) {
await plugin.cleanup();
}
}
}
}
// 使用插件系统构建脚本
async function runScriptWithPlugins() {
let logger = new Logger();
let pluginManager = new PluginManager();
// 注册插件
pluginManager
.register(new ScreenshotPlugin({
onSuccess: true,
onError: true
}))
.register(new ErrorHandlerPlugin({
maxRetries: 2,
fallbackActions: [
async (ctx) => {
toast('操作失败,已通知管理员');
// 发送通知的逻辑
}
]
}))
.setLogger(logger);
// 初始化插件
await pluginManager.initializeAll();
let context = {
step: 'start',
startTime: Date.now()
};
try {
// 执行主逻辑
context = await mainLogic(context);
// 执行插件(成功流程)
context = await pluginManager.executeAll(context);
} catch (error) {
// 执行插件(错误处理流程)
context.error = error;
context = await pluginManager.executeAll(context);
if (context.shouldRetry) {
logger.info('准备重试整个流程');
return runScriptWithPlugins(); // 递归重试
}
} finally {
// 清理插件
await pluginManager.cleanupAll();
}
return context;
}
4.3 自动化测试与模拟环境
在真实设备上调试脚本既耗时又有风险。建立模拟测试环境可以显著提高开发效率:
class MockEnvironment {
constructor() {
this.mockElements = new Map();
this.mockResponses = new Map();
this.interceptMode = false;
}
// 模拟UI元素
mockElement(selector, properties = {}) {
let mockId = JSON.stringify(selector);
this.mockElements.set(mockId, {
selector,
properties,
findCount: 0,
clickCount: 0
});
// 拦截原生的find方法
if (!this.interceptMode) {
this.interceptFindMethods();
this.interceptMode = true;
}
}
interceptFindMethods() {
let originalFind = selector => selector.findOne;
// 简化示例,实际需要更完整的拦截
selectorPrototype.findOne = function(timeout) {
let mockId = JSON.stringify(this);
if (mockEnvironment.mockElements.has(mockId)) {
let mock = mockEnvironment.mockElements.get(mockId);
mock.findCount++;
// 返回模拟元素
return {
click: function() {
mock.clickCount++;
console.log(`模拟点击: ${mockId}`);
return true;
},
bounds: function() {
return {
centerX: 540,
centerY: 960,
left: 500,
top: 940,
right: 580,
bottom: 980
};
},
// 其他属性和方法...
...mock.properties
};
}
// 没有模拟,调用原始方法
return originalFind.call(this, timeout);
};
}
// 模拟网络请求
mockHttpRequest(url, response, options = {}) {
let key = `${url}_${JSON.stringify(options)}`;
this.mockResponses.set(key, {
response,
delay: options.delay || 0,
callCount: 0
});
// 拦截http请求
if (typeof http !== 'undefined') {
let originalGet = http.get;
http.get = function(url, options, callback) {
let mockKey = `${url}_${JSON.stringify(options)}`;
if (mockEnvironment.mockResponses.has(mockKey)) {
let mock = mockEnvironment.mockResponses.get(mockKey);
mock.callCount++;
return new Promise(resolve => {
setTimeout(() => {
resolve(mock.response);
}, mock.delay);
});
}
return originalGet(url, options, callback);
};
}
}
// 验证模拟调用
verifyCalls() {
let report = {
elements: {},
requests: {}
};
for (let [id, mock] of this.mockElements) {
report.elements[id] = {
findCount: mock.findCount,
clickCount: mock.clickCount
};
}
for (let [key, mock] of this.mockResponses) {
report.requests[key] = {
callCount: mock.callCount
};
}
return report;
}
// 重置模拟环境
reset() {
this.mockElements.clear();
this.mockResponses.clear();
// 恢复原始方法(实际实现需要保存原始引用)
}
}
// 使用模拟环境进行测试
async function testScriptLogic() {
let mockEnv = new MockEnvironment();
// 设置模拟元素
mockEnv.mockElement(text("登录"), {
clickable: true,
text: "登录"
});
mockEnv.mockElement(id("username_input"), {
setText: function(text) {
console.log(`模拟输入用户名: ${text}`);
return true;
}
});
// 设置模拟网络响应
mockEnv.mockHttpRequest('https://api.example.com/login', {
statusCode: 200,
body: {success: true, token: 'mock_token'},
headers: {'Content-Type': 'application/json'}
}, {delay: 1000});
// 运行测试
try {
await runLoginScript();
let report = mockEnv.verifyCalls();
console.log('测试报告:', JSON.stringify(report, null, 2));
// 验证预期行为
if (report.elements[JSON.stringify(text("登录"))].clickCount === 1) {
console.log('✓ 登录按钮点击测试通过');
} else {
console.log('✗ 登录按钮点击测试失败');
}
} finally {
mockEnv.reset();
}
}
// 单元测试辅助函数
function testCase(description, testFunction) {
return async function() {
console.log(`\n测试: ${description}`);
try {
await testFunction();
console.log(`✓ ${description} 通过`);
return true;
} catch (error) {
console.error(`✗ ${description} 失败:`, error.message);
return false;
}
};
}
// 组织测试套件
async function runTestSuite() {
let tests = [
testCase('元素定位测试', testElementFinding),
testCase('网络请求测试', testNetworkRequests),
testCase('错误处理测试', testErrorHandling),
testCase('性能测试', testPerformance)
];
let passed = 0;
let failed = 0;
for (let test of tests) {
let result = await test();
if (result) passed++;
else failed++;
}
console.log(`\n测试完成: ${passed} 通过, ${failed} 失败`);
return failed === 0;
}
在实际项目中,我通常会将配置管理、插件系统和模拟测试结合使用。比如先通过配置系统调整不同设备的参数,然后用插件系统组织各个功能模块,最后通过模拟测试验证关键路径。这种架构让脚本不仅能在当前环境下工作,还能适应未来的变化和扩展。
调试复杂脚本时,最实用的技巧其实是保持耐心和系统性。每次遇到问题,不要急于寻找临时解决方案,而是思考:这个问题是特例还是普遍现象?能否通过架构设计避免?现有的日志和监控能否更快发现问题?把这些问题的答案沉淀到你的代码库中,慢慢就会形成自己的最佳实践。
更多推荐
所有评论(0)