import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; // 辅助函数:等待文件上传和AI识别完成 async function waitForFileRecognition(page: any, fileType: string) { console.log(`等待 ${fileType} 文件识别...`); // 第一步:等待文件上传完成(给更多时间) await page.waitForTimeout(5000); console.log(`${fileType} 文件已上传,等待AI识别...`); // 第二步:等待"正在智能文件识别"消失(增加超时时间到5分钟) await page.waitForFunction(() => { const bodyText = document.body.innerText; return !bodyText.includes('正在智能文件识别') && !bodyText.includes('识别中') && !bodyText.includes('Processing'); }, { timeout: 300000 }).catch(() => { console.log(`${fileType} - 未检测到"正在识别"状态,或已完成`); }); // 第三步:检查是否有文件损坏错误 const errorMessage = await page.locator('text=文件已损坏导致解析失败').isVisible({ timeout: 5000 }).catch(() => false); if (errorMessage) { console.log(`⚠️ ${fileType} - 检测到文件损坏错误`); throw new Error(`文件已损坏导致解析失败, 请使用新文件重试`); } // 第四步:等待识别完成的提示(增加超时时间到5分钟) console.log(`${fileType} - 等待识别完成提示...`); const successText = await page.getByText('文档信息已识别自动填充,可查看右侧学生信息对照走查,以免信息有误') .waitFor({ state: 'visible', timeout: 300000 }) .then(() => { console.log(`✅ ${fileType} 文件识别成功!`); return true; }) .catch(async () => { // 如果找不到完整文本,尝试查找部分文本(也增加超时时间) console.log(`${fileType} - 未找到完整提示文本,尝试部分匹配...`); const partialMatch = await page.locator('text=文档信息已识别').or(page.locator('text=已识别自动填充')) .first() .waitFor({ state: 'visible', timeout: 60000 }) .then(() => { console.log(`⚠️ ${fileType} 文件可能识别成功(部分匹配)`); return true; }) .catch(async () => { // 调试:截图并打印内容 console.error(`❌ ${fileType} 文件识别失败,正在截图...`); await page.screenshot({ path: `debug-${fileType}-failed.png`, fullPage: true }); const text = await page.locator('body').textContent(); console.log(`${fileType} 页面内容:`, text?.substring(0, 800)); return false; }); return partialMatch; }); if (!successText) { throw new Error(`${fileType} 文件识别失败,请查看截图 debug-${fileType}-failed.png`); } } // 辅助函数:点击智能文件识别按钮(带重试逻辑) async function clickUploadButton(page: any) { // 关闭可能的弹窗 await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(1000); // 尝试多种方式点击按钮 let clicked = false; // 方法1: footer中的按钮 try { const footerButton = page.locator('footer').getByRole('button', { name: 'star 智能文件识别' }); await footerButton.waitFor({ state: 'visible', timeout: 5000 }); await footerButton.click({ timeout: 5000 }); clicked = true; console.log('成功点击footer中的智能文件识别按钮'); } catch (e1) { console.log('footer按钮不可用,尝试其他方式...'); } // 方法2: 页面中任意位置的按钮 if (!clicked) { try { await page.getByRole('button', { name: 'star 智能文件识别' }).click({ timeout: 5000 }); clicked = true; console.log('成功点击页面中的智能文件识别按钮'); } catch (e2) { console.log('页面按钮也不可用,尝试文本点击...'); } } // 方法3: 通过文本点击 if (!clicked) { try { await page.locator('text=智能文件识别').click({ timeout: 5000 }); clicked = true; console.log('通过文本点击成功'); } catch (e3) { console.warn('所有点击方式都失败,可能不需要再次点击按钮'); } } await page.waitForTimeout(500); } // 辅助函数:上传文件并处理损坏文件重试逻辑 async function uploadFileWithRetry( page: any, fileType: string, filePath: string, clickSelector: string | (() => Promise), corruptedFiles: string[] ) { let retryCount = 0; const maxRetries = 2; // 最多尝试2次(首次 + 1次重试) while (retryCount < maxRetries) { try { console.log(`\n[尝试 ${retryCount + 1}/${maxRetries}] 上传 ${fileType} 文件`); // 触发文件选择器 const fileChooserPromise = page.waitForEvent('filechooser'); if (typeof clickSelector === 'string') { await page.locator(clickSelector).click(); } else { await clickSelector(); } const fileChooser = await fileChooserPromise; await fileChooser.setFiles(filePath); // 等待文件识别完成 await waitForFileRecognition(page, fileType); console.log(`✅ ${fileType} 文件上传成功(尝试 ${retryCount + 1} 次)`); return true; // 成功,退出函数 } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes('文件已损坏导致解析失败')) { retryCount++; if (retryCount < maxRetries) { console.log(`⚠️ ${fileType} 文件损坏,关闭错误提示并准备重试(第 ${retryCount} 次重试)...`); // 点击关闭按钮快速关闭错误提示 try { await page.getByRole('button', { name: 'close' }).click({ timeout: 3000 }); console.log('已关闭错误提示弹窗'); } catch (e) { console.log('未找到关闭按钮,继续重试'); } await page.waitForTimeout(1000); // 等待1秒后重试 } else { console.error(`❌ ${fileType} 文件重试 ${maxRetries} 次后仍然失败`); // 关闭最后一次的错误提示 try { await page.getByRole('button', { name: 'close' }).click({ timeout: 3000 }); } catch (e) { // 忽略错误 } corruptedFiles.push(fileType); return false; } } else { // 其他类型的错误,直接抛出 console.error(`❌ ${fileType} 文件上传失败: ${errorMsg}`); throw error; } } } return false; } test('comprehensive student workflow with AI file upload', async ({ page }) => { // 设置整个测试的超时时间为40分钟(6个文件 * 5分钟 + 其他操作时间) test.setTimeout(2400000); // 错误收集数组 const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = []; // 损坏文件收集数组 const corruptedFiles: string[] = []; // 辅助函数:执行步骤并捕获错误 const executeStep = async (stepName: string, stepFunction: () => Promise) => { try { console.log(`\n[执行] ${stepName}`); await stepFunction(); console.log(`[成功] ${stepName}`); } catch (error) { let errorMessage = error instanceof Error ? error.message : String(error); const firstLine = errorMessage.split('\n')[0]; const cleanError = firstLine.replace(/\s+\(.*?\)/, '').trim(); const timestamp = new Date().toISOString(); const pageUrl = page.url(); errors.push({ step: stepName, error: cleanError, timestamp, pageUrl }); console.error(`[失败] ${stepName}: ${cleanError}`); console.error(`[页面URL] ${pageUrl}`); } }; // 步骤1: 登录系统 await executeStep('访问首页并登录', async () => { await page.goto('https://pre.prodream.cn/en'); await page.getByRole('button', { name: 'Log in' }).click(); await page.getByRole('textbox', { name: 'Email Address' }).click(); await page.getByRole('textbox', { name: 'Email Address' }).fill('xdf.admin@applify.ai'); await page.getByRole('textbox', { name: 'Psssword' }).click(); await page.getByRole('textbox', { name: 'Psssword' }).fill('b9#0!;+{Tx4649op'); await page.getByRole('textbox', { name: 'Psssword' }).press('Enter'); await expect(page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible(); }); await executeStep('进入学生工作台', async () => { await page.getByRole('link').filter({ hasText: '学生工作台' }).click(); await expect(page.getByText('Student Name')).toBeVisible(); }); await executeStep('创建新学生', async () => { await page.getByRole('button', { name: 'New Student' }).click(); await page.getByRole('textbox', { name: 'Name *', exact: true }).click(); await page.getByRole('textbox', { name: 'Name *', exact: true }).fill('黄子旭测试'); await page.locator('div').filter({ hasText: /^Please select branch$/ }).nth(2).click(); await page.locator('div').filter({ hasText: /^1$/ }).nth(1).click(); await page.getByText('Select counselor').click(); await page.locator('div').filter({ hasText: /^2-2@2\.com$/ }).nth(1).click(); await page.locator('svg').nth(5).click(); await page.getByRole('textbox', { name: 'Contract Category *' }).click(); await page.getByRole('textbox', { name: 'Contract Category *' }).fill('11'); await page.getByRole('textbox', { name: 'Contract Name *' }).click(); await page.getByRole('textbox', { name: 'Contract Name *' }).fill('11'); await page.getByRole('button', { name: 'Confirm' }).click(); }); await executeStep('进入学生档案', async () => { await page.getByRole('button', { name: 'More' }).first().dblclick(); await page.getByText('学生档案').click(); }); // 步骤2: AI智能文件识别上传(6个文件) await executeStep('打开智能文件识别', async () => { await page.locator('footer').getByRole('button', { name: 'star 智能文件识别' }).click(); await page.waitForTimeout(2000); }); // 上传 DOCX 文件 await executeStep('上传DOCX文件', async () => { const success = await uploadFileWithRetry( page, 'DOCX', path.join(__dirname, '..', '建档测试文件', '测试建档DOCX.docx'), 'text=或点击选择文件 不超过10MB | 支持格式: DOCX、', corruptedFiles ); if (success) await clickUploadButton(page); }); // 上传 PDF 文件 await executeStep('上传PDF文件', async () => { const success = await uploadFileWithRetry( page, 'PDF', path.join(__dirname, '..', '建档测试文件', '测试建档PDF.pdf'), 'text=或点击选择文件 不超过10MB | 支持格式: DOCX、', corruptedFiles ); if (success) await clickUploadButton(page); }); // 上传 JPEG 文件 await executeStep('上传JPEG文件', async () => { const success = await uploadFileWithRetry( page, 'JPEG', path.join(__dirname, '..', '建档测试文件', '测试建档JPEG.jpg'), async () => { await page.locator('div').filter({ hasText: '拖拽文档到这里或点击选择文件 不超过10MB' }).nth(4).click(); }, corruptedFiles ); if (success) await clickUploadButton(page); }); // 上传 PNG 文件 await executeStep('上传PNG文件', async () => { const success = await uploadFileWithRetry( page, 'PNG', path.join(__dirname, '..', '建档测试文件', '测试建档PNG.png'), 'text=或点击选择文件 不超过10MB | 支持格式: DOCX、', corruptedFiles ); if (success) await clickUploadButton(page); }); // 上传 BMP 文件 await executeStep('上传BMP文件', async () => { const success = await uploadFileWithRetry( page, 'BMP', path.join(__dirname, '..', '建档测试文件', '测试建档 BMP.bmp'), 'text=或点击选择文件 不超过10MB | 支持格式: DOCX、', corruptedFiles ); if (success) await clickUploadButton(page); }); // 上传 XLSX 文件 await executeStep('上传XLSX文件', async () => { const success = await uploadFileWithRetry( page, 'XLSX', path.join(__dirname, '..', '建档测试文件', '测试建档XLSX.xlsx'), async () => { await page.getByRole('heading', { name: '拖拽文档到这里' }).click(); }, corruptedFiles ); // XLSX是最后一个文件,上传后不需要再点击智能识别按钮 }); // 步骤3: 验证学生档案信息已填充 await executeStep('验证学生基本信息已填充', async () => { // 等待页面稳定 await page.waitForTimeout(3000); // 验证姓名字段有值 const nameField = page.getByRole('textbox', { name: 'Name *', exact: true }); const nameValue = await nameField.inputValue(); if (!nameValue || nameValue.trim() === '') { throw new Error('学生姓名未填充'); } console.log(`✅ 学生姓名已填充: ${nameValue}`); }); await executeStep('验证学生详细信息已填充', async () => { // 检查多个关键字段是否有数据 const fieldsToCheck = [ { name: 'Email', selector: page.getByRole('textbox', { name: 'Email' }) }, { name: 'Phone', selector: page.getByRole('textbox', { name: 'Phone' }) } ]; let filledCount = 0; for (const field of fieldsToCheck) { try { const value = await field.selector.inputValue({ timeout: 5000 }); if (value && value.trim() !== '') { console.log(`✅ ${field.name} 已填充: ${value}`); filledCount++; } } catch (e) { console.log(`⚠️ ${field.name} 字段未找到或未填充`); } } if (filledCount === 0) { console.log('⚠️ 警告:部分学生信息字段未能自动填充'); } else { console.log(`✅ 成功填充 ${filledCount} 个学生信息字段`); } }); // 步骤4: 保存学生档案 await executeStep('保存学生档案', async () => { const saveButton = page.getByRole('button', { name: 'Save' }); await saveButton.click({ timeout: 5000 }); await page.waitForTimeout(2000); console.log('学生档案已保存'); }); // 最后输出错误汇总 console.log('\n\n========== 测试执行完成 =========='); // 显示损坏文件信息 if (corruptedFiles.length > 0) { console.log('\n⚠️ 文件损坏警告:以下文件重试2次后仍然失败\n'); corruptedFiles.forEach((fileType, index) => { console.log(` ${index + 1}. ${fileType} 文件 - 文件已损坏导致解析失败`); }); console.log(''); } // 生成错误报告文件 const generateErrorReport = () => { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const reportPath = path.join(process.cwd(), `test-2-student-workflow-report-${timestamp}.txt`); let report = '========== 学生工作流程与AI建档测试报告 ==========\n\n'; report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`; report += '【测试账号信息】\n'; report += `测试账号: xdf.admin@applify.ai\n`; report += `学生姓名: 黄子旭测试\n`; report += `测试环境: https://pre.prodream.cn/en\n\n`; report += `${'='.repeat(60)}\n\n`; // 添加损坏文件警告 if (corruptedFiles.length > 0) { report += '【AI建档 - 文件损坏警告】\n'; report += `发现 ${corruptedFiles.length} 个文件损坏(重试2次后仍失败):\n\n`; corruptedFiles.forEach((fileType, index) => { report += ` ${index + 1}. ${fileType} 文件 - 文件已损坏导致解析失败, 请使用新文件重试\n`; }); report += '\n'; report += `${'='.repeat(60)}\n\n`; } if (errors.length === 0 && corruptedFiles.length === 0) { report += '✅ 所有功能测试通过,未发现问题!\n'; } else { if (errors.length > 0) { report += `发现 ${errors.length} 个问题,详情如下:\n\n`; errors.forEach((err, index) => { report += `【问题 ${index + 1}】\n`; report += `问题功能: ${err.step}\n`; report += `页面链接: ${err.pageUrl}\n`; report += `错误详情: ${err.error}\n`; report += `发生时间: ${new Date(err.timestamp).toLocaleString('zh-CN')}\n`; report += '\n'; }); report += `${'='.repeat(60)}\n`; report += `\n说明: 测试过程中遇到错误会自动跳过并继续执行后续步骤。\n`; } } fs.writeFileSync(reportPath, report, 'utf-8'); return reportPath; }; if (errors.length === 0 && corruptedFiles.length === 0) { console.log('✅ 所有步骤执行成功!'); } else { if (errors.length > 0) { console.log(`\n⚠️ 发现 ${errors.length} 个问题:\n`); errors.forEach((err, index) => { console.log(`【问题 ${index + 1}】`); console.log(` 问题功能: ${err.step}`); console.log(` 页面链接: ${err.pageUrl}`); console.log(` 错误详情: ${err.error}`); console.log(''); }); } const reportPath = generateErrorReport(); console.log(`📄 详细错误报告已保存到: ${reportPath}`); } });