477 lines
19 KiB
TypeScript
477 lines
19 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import Imap from 'imap';
|
||
import { simpleParser, ParsedMail } from 'mailparser';
|
||
|
||
test('counselor registration and login test', async ({ page }) => {
|
||
test.setTimeout(900000); // 设置超时时间为15分钟
|
||
|
||
// 设置更长的导航超时
|
||
page.setDefaultNavigationTimeout(90000); // 90秒导航超时
|
||
page.setDefaultTimeout(60000); // 60秒默认超时
|
||
|
||
// 错误收集数组
|
||
const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = [];
|
||
|
||
// 管理员登录信息
|
||
const adminInfo = {
|
||
email: 'prodream.admin@applify.ai',
|
||
password: 'b9#0!;+{Tx4649op'
|
||
};
|
||
|
||
// 新顾问信息
|
||
const counselorInfo = {
|
||
name: '黄子旭顾问测试',
|
||
email: 'dev.test@prodream.cn',
|
||
branch: '5257',
|
||
line: '11',
|
||
employeeId: '11',
|
||
password: '' // 将从邮件中提取
|
||
};
|
||
|
||
// 企业邮箱 IMAP 配置
|
||
const imapConfig = {
|
||
user: 'dev.test@prodream.cn',
|
||
password: 'phhuma3UKGHvZPwo', // 企业邮箱授权码(IMAP/SMTP服务授权码)
|
||
host: 'smtp.exmail.qq.com',
|
||
port: 993,
|
||
tls: true,
|
||
tlsOptions: { rejectUnauthorized: false }
|
||
};
|
||
|
||
// 辅助函数:执行步骤并捕获错误
|
||
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}`);
|
||
}
|
||
};
|
||
|
||
// 辅助函数:通过IMAP读取最新邮件密码
|
||
const getPasswordFromEmail = (): Promise<string> => {
|
||
return new Promise((resolve, reject) => {
|
||
console.log('正在连接邮箱服务器...');
|
||
const imap = new Imap(imapConfig);
|
||
|
||
imap.once('ready', () => {
|
||
console.log('邮箱连接成功');
|
||
imap.openBox('INBOX', false, (err, box) => {
|
||
if (err) {
|
||
imap.end();
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
console.log(`收件箱邮件数: ${box.messages.total}`);
|
||
|
||
// 搜索所有欢迎邮件
|
||
imap.search([['FROM', 'no-reply'], ['SUBJECT', 'Welcome to Prodream']], (err, results) => {
|
||
if (err) {
|
||
imap.end();
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
if (results.length === 0) {
|
||
imap.end();
|
||
reject(new Error('未找到欢迎邮件'));
|
||
return;
|
||
}
|
||
|
||
console.log(`找到 ${results.length} 封相关邮件`);
|
||
|
||
// 收集所有邮件的日期和ID
|
||
const emailDates: { id: number; date: Date }[] = [];
|
||
let processedCount = 0;
|
||
|
||
const fetchHeaders = imap.fetch(results, { bodies: 'HEADER.FIELDS (DATE)' });
|
||
|
||
fetchHeaders.on('message', (msg, seqno) => {
|
||
msg.on('body', (stream: NodeJS.ReadableStream) => {
|
||
let buffer = '';
|
||
stream.on('data', (chunk: Buffer) => {
|
||
buffer += chunk.toString('utf8');
|
||
});
|
||
stream.once('end', () => {
|
||
// 从头部提取日期
|
||
const dateMatch = buffer.match(/Date:\s*(.+?)(?:\r?\n|$)/i);
|
||
if (dateMatch) {
|
||
try {
|
||
const dateString = dateMatch[1].trim();
|
||
const emailDate = new Date(dateString);
|
||
|
||
// 验证日期是否有效
|
||
if (!isNaN(emailDate.getTime())) {
|
||
emailDates.push({ id: seqno, date: emailDate });
|
||
console.log(`邮件 ${seqno} 的日期: ${emailDate.toISOString()}`);
|
||
} else {
|
||
console.log(`⚠️ 邮件 ${seqno} 日期无效: ${dateString}`);
|
||
}
|
||
} catch (e) {
|
||
console.log(`⚠️ 邮件 ${seqno} 日期解析失败`);
|
||
}
|
||
} else {
|
||
console.log(`⚠️ 邮件 ${seqno} 未找到日期头部`);
|
||
}
|
||
});
|
||
});
|
||
|
||
msg.once('end', () => {
|
||
processedCount++;
|
||
|
||
// 所有邮件头部都处理完后,选择最新的
|
||
if (processedCount === results.length) {
|
||
console.log(`\n处理完成,共收集到 ${emailDates.length} 封邮件的日期信息`);
|
||
|
||
// 如果没有收集到任何日期信息,使用最大的邮件ID(通常是最新的)
|
||
let latestEmail: { id: number; date: Date };
|
||
|
||
if (emailDates.length === 0) {
|
||
console.log('⚠️ 未能从邮件头部提取日期,使用邮件ID排序');
|
||
const maxId = Math.max(...results);
|
||
latestEmail = { id: maxId, date: new Date() };
|
||
console.log(`选择邮件 ID: ${maxId}`);
|
||
} else {
|
||
// 按日期排序,最新的在最后
|
||
emailDates.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||
latestEmail = emailDates[emailDates.length - 1];
|
||
console.log(`选择最新邮件: ID=${latestEmail.id}, 日期=${latestEmail.date.toISOString()}`);
|
||
}
|
||
|
||
// 获取最新邮件的完整内容
|
||
const fetchBody = imap.fetch([latestEmail.id], { bodies: '' });
|
||
|
||
fetchBody.on('message', (msg2) => {
|
||
msg2.on('body', (stream: any) => {
|
||
simpleParser(stream, (err: any, parsed: ParsedMail) => {
|
||
if (err) {
|
||
imap.end();
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
console.log('邮件解析成功');
|
||
|
||
// 尝试从文本版本和 HTML 版本提取
|
||
const textBody = parsed.text || '';
|
||
const htmlBody = parsed.html || '';
|
||
|
||
console.log('=== 邮件文本内容 ===');
|
||
console.log(textBody);
|
||
console.log('=== 邮件HTML内容 ===');
|
||
console.log(htmlBody);
|
||
console.log('===================');
|
||
|
||
// 尝试多种密码提取模式
|
||
// 密码可以包含任何可打印字符,直到遇到空格、换行或特定的结束词
|
||
const patterns = [
|
||
/Password[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
|
||
/password[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
|
||
/密码[:\s:]+(\S+?)(?:\s+登录|\s+$|\s+\n|$)/,
|
||
/Your password is[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
|
||
/临时密码[:\s:]+(\S+?)(?:\s+登录|\s+$|\s+\n|$)/,
|
||
// 备用模式:匹配到下一个单词边界
|
||
/Password[:\s]+([^\s]+)/i,
|
||
/password[:\s]+([^\s]+)/i
|
||
];
|
||
|
||
let password = '';
|
||
|
||
// 首先尝试从文本内容提取
|
||
for (const pattern of patterns) {
|
||
const match = textBody.match(pattern);
|
||
if (match && match[1]) {
|
||
password = match[1].trim();
|
||
console.log(`✅ 从文本中提取到密码 (模式: ${pattern}): ${password}`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果文本提取失败,尝试从 HTML 提取
|
||
if (!password && htmlBody) {
|
||
// 移除 HTML 标签后再尝试
|
||
const cleanHtml = htmlBody.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ');
|
||
console.log('清理后的HTML内容:', cleanHtml.substring(0, 300));
|
||
|
||
for (const pattern of patterns) {
|
||
const match = cleanHtml.match(pattern);
|
||
if (match && match[1]) {
|
||
password = match[1].trim();
|
||
console.log(`✅ 从HTML中提取到密码 (模式: ${pattern}): ${password}`);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (password) {
|
||
console.log(`🎉 成功提取密码: ${password}`);
|
||
imap.end();
|
||
resolve(password);
|
||
} else {
|
||
console.error('❌ 所有模式都未能提取密码');
|
||
console.error('请检查上面的邮件内容,手动确认密码格式');
|
||
imap.end();
|
||
reject(new Error('无法从邮件中提取密码'));
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
fetchBody.once('error', (err: Error) => {
|
||
imap.end();
|
||
reject(err);
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
fetchHeaders.once('error', (err: Error) => {
|
||
imap.end();
|
||
reject(err);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
imap.once('error', (err: Error) => {
|
||
console.error('IMAP连接错误:', err);
|
||
reject(err);
|
||
});
|
||
|
||
imap.once('end', () => {
|
||
console.log('邮箱连接已关闭');
|
||
});
|
||
|
||
imap.connect();
|
||
});
|
||
};
|
||
|
||
// 步骤1: 管理员登录
|
||
await executeStep('访问首页', async () => {
|
||
await page.goto('https://prodream.cn/en', { timeout: 60000 });
|
||
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible({ timeout: 30000 });
|
||
});
|
||
|
||
await executeStep('点击登录按钮', async () => {
|
||
await page.getByRole('button', { name: 'Log in' }).click();
|
||
await expect(page.getByRole('textbox', { name: 'Email Address' })).toBeVisible();
|
||
});
|
||
|
||
await executeStep('输入管理员登录信息', async () => {
|
||
await page.getByRole('textbox', { name: 'Email Address' }).click();
|
||
await page.getByRole('textbox', { name: 'Email Address' }).fill(adminInfo.email);
|
||
await page.getByRole('textbox', { name: 'Psssword' }).click();
|
||
await page.getByRole('textbox', { name: 'Psssword' }).fill(adminInfo.password);
|
||
await page.getByRole('button', { name: 'Log in' }).click();
|
||
await expect(page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible();
|
||
});
|
||
|
||
// 步骤2: 创建新顾问
|
||
await executeStep('进入顾问管理页面', async () => {
|
||
await page.getByRole('link').filter({ hasText: '顾问管理' }).click();
|
||
await expect(page.getByRole('button', { name: 'New counselor' })).toBeVisible({ timeout: 10000 });
|
||
console.log('顾问管理页面已加载');
|
||
});
|
||
|
||
await executeStep('点击新建顾问', async () => {
|
||
await page.getByRole('button', { name: 'New counselor' }).click();
|
||
await expect(page.getByRole('textbox', { name: 'Name *' })).toBeVisible({ timeout: 5000 });
|
||
});
|
||
|
||
await executeStep('填写顾问信息', async () => {
|
||
await page.getByRole('textbox', { name: 'Name *' }).click();
|
||
await page.getByRole('textbox', { name: 'Name *' }).fill(counselorInfo.name);
|
||
|
||
await page.getByRole('textbox', { name: 'Email *' }).click();
|
||
await page.getByRole('textbox', { name: 'Email *' }).fill(counselorInfo.email);
|
||
|
||
await page.getByRole('textbox', { name: 'Branch *' }).click();
|
||
await page.getByRole('textbox', { name: 'Branch *' }).fill(counselorInfo.branch);
|
||
|
||
await page.getByRole('textbox', { name: 'Line *' }).click();
|
||
await page.getByRole('textbox', { name: 'Line *' }).fill(counselorInfo.line);
|
||
|
||
await page.getByRole('textbox', { name: 'Employee ID *' }).click();
|
||
await page.getByRole('textbox', { name: 'Employee ID *' }).fill(counselorInfo.employeeId);
|
||
|
||
console.log('顾问信息填写完成');
|
||
});
|
||
|
||
await executeStep('提交创建顾问', async () => {
|
||
await page.getByRole('button', { name: 'Add Counselor' }).click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 确认修改
|
||
const confirmButton = page.getByRole('button', { name: 'Confirm Modification' });
|
||
if (await confirmButton.isVisible()) {
|
||
await confirmButton.click();
|
||
}
|
||
|
||
await page.waitForTimeout(2000);
|
||
console.log('顾问创建请求已提交');
|
||
});
|
||
|
||
await executeStep('验证顾问创建成功', async () => {
|
||
await page.getByRole('textbox', { name: 'Search by counselor name,' }).click();
|
||
await page.getByRole('textbox', { name: 'Search by counselor name,' }).fill(counselorInfo.name);
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 验证顾问邮箱出现在列表中
|
||
await expect(page.locator('span').filter({ hasText: counselorInfo.email })).toBeVisible({ timeout: 10000 });
|
||
console.log('顾问创建成功并出现在列表中');
|
||
});
|
||
|
||
// 步骤3: 从邮箱获取密码
|
||
await executeStep('从QQ邮箱获取密码', async () => {
|
||
console.log('等待邮件发送(30秒)...');
|
||
await page.waitForTimeout(30000); // 等待邮件发送
|
||
|
||
try {
|
||
const password = await getPasswordFromEmail();
|
||
counselorInfo.password = password;
|
||
console.log(`成功获取密码: ${password}`);
|
||
} catch (error) {
|
||
console.error('获取邮件密码失败:', error);
|
||
throw new Error(`无法从邮箱获取密码: ${error instanceof Error ? error.message : String(error)}`);
|
||
}
|
||
});
|
||
|
||
// 步骤4: 管理员登出
|
||
await executeStep('管理员退出登录', async () => {
|
||
await page.locator('div').filter({ hasText: /^Admin$/ }).click();
|
||
await page.getByRole('menuitem', { name: '退出登录' }).click();
|
||
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible({ timeout: 10000 });
|
||
console.log('管理员已退出登录');
|
||
});
|
||
|
||
// 步骤5: 使用新顾问账号登录
|
||
await executeStep('使用新顾问账号登录', async () => {
|
||
await page.getByRole('textbox', { name: 'Email Address' }).click();
|
||
await page.getByRole('textbox', { name: 'Email Address' }).fill(counselorInfo.email);
|
||
|
||
await page.getByRole('textbox', { name: 'Psssword' }).click();
|
||
await page.getByRole('textbox', { name: 'Psssword' }).fill(counselorInfo.password);
|
||
|
||
await page.getByRole('button', { name: 'Log in' }).click();
|
||
|
||
// 验证登录成功
|
||
await expect(page.getByRole('img', { name: 'prodream' })).toBeVisible({ timeout: 15000 });
|
||
console.log('新顾问账号登录成功');
|
||
});
|
||
|
||
// 步骤6: 测试新顾问的各项功能
|
||
await executeStep('测试 DreamiExplore 功能', async () => {
|
||
await page.getByRole('link').filter({ hasText: 'DreamiExplore Everything' }).click();
|
||
await expect(page.getByText('我是Dreami,擅长升学领域各类问题,请随意提问咨询哦。')).toBeVisible({ timeout: 15000 });
|
||
await page.getByRole('textbox').click();
|
||
await page.getByRole('textbox').fill('hello');
|
||
await page.getByRole('button', { name: 'send' }).click();
|
||
await expect(page.getByRole('button', { name: '复制' })).toBeVisible({ timeout: 15000 });
|
||
console.log('DreamiExplore 功能正常');
|
||
});
|
||
|
||
await executeStep('测试学生工作台', async () => {
|
||
await page.getByRole('link').filter({ hasText: '学生工作台' }).click();
|
||
await expect(page.getByRole('button', { name: 'New Student' })).toBeVisible({ timeout: 10000 });
|
||
console.log('学生工作台可访问');
|
||
});
|
||
|
||
await executeStep('测试文书记录', async () => {
|
||
await page.getByRole('link').filter({ hasText: '文书记录' }).click();
|
||
await expect(page.getByRole('button', { name: 'New Essay' })).toBeVisible({ timeout: 10000 });
|
||
console.log('文书记录可访问');
|
||
});
|
||
|
||
await executeStep('测试规划记录', async () => {
|
||
await page.getByRole('link').filter({ hasText: '规划记录' }).click();
|
||
await expect(page.getByRole('button', { name: 'icon AI规划' })).toBeVisible({ timeout: 10000 });
|
||
console.log('规划记录可访问');
|
||
});
|
||
|
||
await executeStep('测试 Discover 页面', async () => {
|
||
await page.getByRole('link').filter({ hasText: 'Discover' }).click();
|
||
await page.waitForTimeout(2000);
|
||
console.log('Discover 页面可访问');
|
||
});
|
||
|
||
await executeStep('测试通知页面', async () => {
|
||
await page.getByRole('link').filter({ hasText: '通知' }).click();
|
||
await page.waitForTimeout(2000);
|
||
console.log('通知页面可访问');
|
||
});
|
||
|
||
// 最后输出错误汇总
|
||
console.log('\n\n========== 测试执行完成 ==========');
|
||
|
||
// 生成错误报告文件
|
||
const generateErrorReport = () => {
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const reportPath = path.join(process.cwd(), `test-2-counselor-report-${timestamp}.txt`);
|
||
|
||
let report = '========== 顾问注册与登录测试报告 ==========\n\n';
|
||
report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
|
||
|
||
report += '【测试账号信息】\n';
|
||
report += `管理员账号: ${adminInfo.email}\n`;
|
||
report += `管理员密码: ${adminInfo.password}\n`;
|
||
report += `新顾问姓名: ${counselorInfo.name}\n`;
|
||
report += `新顾问邮箱: ${counselorInfo.email}\n`;
|
||
report += `新顾问密码: ${counselorInfo.password || '(未获取到)'}\n`;
|
||
report += `测试环境: https://prodream.cn/en\n\n`;
|
||
report += `${'='.repeat(60)}\n\n`;
|
||
|
||
if (errors.length === 0) {
|
||
report += '✅ 所有功能测试通过,未发现问题!\n';
|
||
} else {
|
||
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) {
|
||
console.log('✅ 所有步骤执行成功!');
|
||
} else {
|
||
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}`);
|
||
console.log(`\n管理员账号: ${adminInfo.email}`);
|
||
console.log(`管理员密码: ${adminInfo.password}`);
|
||
console.log(`新顾问账号: ${counselorInfo.email}`);
|
||
console.log(`新顾问密码: ${counselorInfo.password}\n`);
|
||
}
|
||
});
|