108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import BaseAction from '../core/BaseAction';
|
||
import { ValidationError, ElementNotFoundError } from '../../../core/errors/CustomErrors';
|
||
|
||
/**
|
||
* 导航动作 - 打开页面
|
||
*/
|
||
class NavigateAction extends BaseAction {
|
||
async execute(): Promise<any> {
|
||
const url = this.replaceVariables(this.config.url);
|
||
const options = this.config.options || {
|
||
waitUntil: 'networkidle2',
|
||
timeout: 30000
|
||
};
|
||
|
||
// 重试配置
|
||
const maxRetries = this.config.maxRetries || 5;
|
||
const retryDelay = this.config.retryDelay || 3000;
|
||
const totalTimeout = this.config.totalTimeout || 180000; // 默认3分钟
|
||
|
||
const startTime = Date.now();
|
||
let lastError: any = null;
|
||
|
||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||
// 检查总超时
|
||
if (Date.now() - startTime > totalTimeout) {
|
||
this.log('error', `总超时 ${totalTimeout}ms,停止重试`);
|
||
break;
|
||
}
|
||
|
||
try {
|
||
if (attempt > 0) {
|
||
this.log('info', `第 ${attempt + 1} 次尝试导航...`);
|
||
} else {
|
||
this.log('info', `导航到: ${url}`);
|
||
}
|
||
|
||
// 尝试导航
|
||
await this.page.goto(url, options);
|
||
|
||
// 验证页面URL是否正确(避免重定向到会员中心等)
|
||
const currentUrl = this.page.url();
|
||
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
|
||
throw new ValidationError(
|
||
`页面跳转异常`,
|
||
`URL包含: ${this.config.verifyUrl}`,
|
||
`实际URL: ${currentUrl}`,
|
||
{ expectedUrl: this.config.verifyUrl, actualUrl: currentUrl }
|
||
);
|
||
}
|
||
|
||
// 验证关键元素存在(确保页面加载正确)
|
||
if (this.config.verifyElements) {
|
||
await this.verifyElements(this.config.verifyElements);
|
||
}
|
||
|
||
this.log('info', `✓ 页面加载完成${attempt > 0 ? ` (尝试 ${attempt + 1} 次)` : ''}`);
|
||
|
||
// 模拟人类阅读页面(1-3秒)
|
||
await this.readPageDelay();
|
||
|
||
// 可选的额外等待时间
|
||
if (this.config.waitAfter) {
|
||
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
|
||
}
|
||
|
||
return { success: true, url: currentUrl };
|
||
|
||
} catch (error: any) {
|
||
lastError = error;
|
||
this.log('warn', `导航失败 (尝试 ${attempt + 1}/${maxRetries}): ${error.message}`);
|
||
|
||
// 如果不是最后一次尝试,等待后重试
|
||
if (attempt < maxRetries - 1) {
|
||
this.log('debug', `等待 ${retryDelay}ms 后重试...`);
|
||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有重试都失败
|
||
this.log('error', `导航失败: ${lastError.message}`);
|
||
throw lastError;
|
||
}
|
||
|
||
/**
|
||
* 验证关键元素存在
|
||
*/
|
||
async verifyElements(selectors: string[]): Promise<void> {
|
||
this.log('debug', '验证页面元素...');
|
||
|
||
for (const selector of selectors) {
|
||
try {
|
||
await this.page.waitForSelector(selector, { timeout: 10000 });
|
||
} catch (error: any) {
|
||
throw new ElementNotFoundError(selector, {
|
||
action: 'navigate',
|
||
operation: 'verifyElements',
|
||
url: this.page.url()
|
||
});
|
||
}
|
||
}
|
||
|
||
this.log('debug', `✓ 已验证 ${selectors.length} 个关键元素`);
|
||
}
|
||
}
|
||
|
||
export default NavigateAction;
|