first commit
This commit is contained in:
462
学生工作台测试.spec.ts
Normal file
462
学生工作台测试.spec.ts
Normal file
@ -0,0 +1,462 @@
|
||||
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<void>),
|
||||
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<void>) => {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user