commit 8ee74625c71ad4f894a4d320d15ffdc4b74a543c Author: 黄子旭 <2188522595@qq.com> Date: Tue Nov 4 16:29:07 2025 +0800 first commit diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..784d213 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,361 @@ +# 项目架构说明 + +## 设计理念 + +本项目采用 **Page Object Model (POM)** 设计模式,遵循以下原则: + +1. **关注点分离**:页面逻辑与测试逻辑分离 +2. **DRY 原则**:避免重复代码 +3. **单一职责**:每个类只负责一个页面或功能 +4. **可测试性**:易于编写和维护测试 +5. **可扩展性**:便于添加新功能 + +## 架构图 + +``` +┌─────────────────────────────────────────────────┐ +│ 测试文件层 │ +│ (文书管理-refactored.spec.ts) │ +│ - 组织测试流程 │ +│ - 使用 Page Objects │ +│ - 错误处理 │ +└─────────────────┬───────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ Page Object 层 │ +│ - LoginPage (登录) │ +│ - StudentPage (学生管理) │ +│ - DreamiExplorePage (AI聊天) │ +│ - EssayWritingPage (文书写作) │ +│ - BasePage (基础页面类) │ +└─────────────────┬───────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ 工具层 │ +│ - ErrorHandler (错误处理) │ +│ - 其他工具类... │ +└─────────────────┬───────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────┐ +│ 配置层 │ +│ - TestConfig (测试配置) │ +│ - 环境变量、超时设置、测试数据 │ +└─────────────────────────────────────────────────┘ +``` + +## 核心组件 + +### 1. 配置层 (config/) + +**职责**:集中管理所有配置信息 + +#### test.config.ts +```typescript +export const TestConfig = { + env: { ... }, // 环境配置 + credentials: { ... }, // 登录凭证 + timeouts: { ... }, // 超时设置 + testData: { ... }, // 测试数据 + waits: { ... }, // 等待时间 + report: { ... }, // 报告配置 +} +``` + +**优势**: +- ✅ 环境切换方便 +- ✅ 配置集中管理 +- ✅ 避免硬编码 +- ✅ 易于维护 + +### 2. Page Object 层 (pages/) + +**职责**:封装页面操作,提供高级 API + +#### BasePage.ts +基础页面类,所有页面对象的父类 + +```typescript +export class BasePage { + protected page: Page; + protected config = TestConfig; + + // 通用方法 + async waitForPageStable() { } + async goto(url: string) { } + async click(selector: string) { } + // ... +} +``` + +#### LoginPage.ts +登录页面对象 + +```typescript +export class LoginPage extends BasePage { + async visitHomePage() { } + async clickLoginButton() { } + async login(email, password) { } + async performLogin() { } +} +``` + +**设计特点**: +- 继承 BasePage 获取通用功能 +- 方法名语义化,易于理解 +- 封装页面元素定位 +- 提供高级业务方法 + +#### StudentPage.ts +学生管理页面对象 + +```typescript +export class StudentPage extends BasePage { + async goToStudentWorkbench() { } + async createNewStudent(studentData) { } +} +``` + +#### EssayWritingPage.ts +文书写作页面对象(最复杂) + +```typescript +export class EssayWritingPage extends BasePage { + // 基础操作 + async enterEssayWriting() { } + async addMaterial(title, content, expectedText) { } + + // AI 生成功能 + async generateEssayIdea() { } + async generateEssay() { } + async generateOutline() { } + async generateDraft(level, length) { } + + // 检查功能 + async performGrammarCheck() { } + async performPlagiarismCheck() { } + async performAIDetection() { } + async performHumanize() { } + async performPolish() { } + async performGetRated() { } + async performImprovementCheck() { } +} +``` + +**优势**: +- ✅ 业务逻辑清晰 +- ✅ 方法可复用 +- ✅ UI 变化只需修改 Page Object +- ✅ 测试文件更简洁 + +### 3. 工具层 (utils/) + +**职责**:提供通用工具和辅助功能 + +#### ErrorHandler.ts +错误处理工具类 + +```typescript +export class ErrorHandler { + private errors: ErrorRecord[] = []; + + // 记录错误 + recordError(stepName, error, page) { } + + // 执行步骤并捕获错误 + async executeStep(stepName, stepFunction, page) { } + + // 生成错误报告 + generateErrorReport() { } + + // 打印摘要 + printSummary() { } +} +``` + +**功能**: +- ✅ 自动捕获并记录错误 +- ✅ 清理错误信息,去除技术细节 +- ✅ 生成友好的错误报告 +- ✅ 允许测试继续执行 + +### 4. 测试文件层 + +**职责**:组织测试流程,调用 Page Objects + +#### 文书管理-refactored.spec.ts +```typescript +test('文书管理完整流程测试', async ({ page }) => { + // 初始化 + const errorHandler = new ErrorHandler(TestConfig.credentials); + const loginPage = new LoginPage(page); + const essayWritingPage = new EssayWritingPage(page); + + // 测试流程 + await executeStep('访问首页', async () => { + await loginPage.visitHomePage(); + }); + + await executeStep('生成 Essay', async () => { + await essayWritingPage.generateEssay(); + }); + + // 生成报告 + errorHandler.printSummary(); +}); +``` + +**特点**: +- ✅ 代码清晰易读 +- ✅ 业务流程一目了然 +- ✅ 错误处理自动化 +- ✅ 配置与代码分离 + +## 数据流 + +``` +测试开始 + ↓ +读取 TestConfig + ↓ +初始化 Page Objects + ↓ +执行测试步骤 (executeStep) + ↓ +调用 Page Object 方法 + ↓ +Page Object 执行页面操作 + ↓ +发生错误? + ├─ 是 → ErrorHandler 记录错误 → 继续下一步 + └─ 否 → 继续下一步 + ↓ +所有步骤完成 + ↓ +生成错误报告 + ↓ +测试结束 +``` + +## 扩展指南 + +### 添加新页面 + +1. **创建 Page Object** +```typescript +// e2e/pages/NewPage.ts +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class NewPage extends BasePage { + constructor(page: Page) { + super(page); + } + + async yourMethod(): Promise { + // 实现页面操作 + } +} +``` + +2. **在测试中使用** +```typescript +import { NewPage } from './pages/NewPage'; + +test('your test', async ({ page }) => { + const newPage = new NewPage(page); + await newPage.yourMethod(); +}); +``` + +### 添加新配置 + +在 `test.config.ts` 中添加: +```typescript +export const TestConfig = { + // ... 现有配置 + + newConfig: { + // 新配置项 + } +} +``` + +### 添加新工具类 + +```typescript +// e2e/utils/YourUtil.ts +export class YourUtil { + // 实现工具方法 +} +``` + +## 最佳实践 + +### 1. Page Object 设计 +- ✅ 一个页面一个类 +- ✅ 方法返回 Promise +- ✅ 使用描述性方法名 +- ✅ 避免在 Page Object 中写断言(除非是验证页面状态) + +### 2. 测试文件编写 +- ✅ 使用 executeStep 包装每个步骤 +- ✅ 步骤名称清晰描述操作 +- ✅ 复杂流程拆分为多个步骤 +- ✅ 在测试末尾生成报告 + +### 3. 配置管理 +- ✅ 不要硬编码任何配置值 +- ✅ 环境相关的配置放在 env 中 +- ✅ 测试数据放在 testData 中 +- ✅ 超时设置放在 timeouts 中 + +### 4. 错误处理 +- ✅ 所有关键步骤都用 executeStep 包装 +- ✅ 不要在 Page Object 中处理错误 +- ✅ 让错误冒泡到测试层处理 +- ✅ 记录足够的错误上下文信息 + +## 代码复用 + +### 前后对比 + +**重构前**: +```typescript +// 代码重复,难以维护 +await page.getByRole('button', { name: 'Add Material' }).click(); +await page.getByRole('menuitem', { name: 'Manual Add' }).click(); +await page.getByRole('textbox', { name: 'Title' }).fill('title'); +// ... 10+ 行相似代码 +``` + +**重构后**: +```typescript +// 简洁清晰,易于维护 +await essayWritingPage.addMaterial( + TestConfig.testData.material.title, + TestConfig.testData.material.content, + TestConfig.testData.material.expectedButtonText +); +``` + +## 性能优化 + +- Page Object 方法中使用合理的超时 +- 配置中集中管理超时时间 +- 避免不必要的等待 +- 使用页面对象缓存(如需要) + +## 总结 + +这个架构具有以下优势: + +1. **可维护性**:模块化设计,职责清晰 +2. **可扩展性**:易于添加新功能 +3. **可读性**:代码清晰,易于理解 +4. **可测试性**:每个组件独立可测 +5. **健壮性**:完善的错误处理机制 + +通过这种架构,测试代码变得像文档一样易读,维护成本大大降低。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..865027a --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# E2E 测试项目 + +这是一个采用 Page Object Model (POM) 设计模式的 Playwright E2E 测试项目,具有良好的可扩展性、可维护性和可读性。 + +## 项目结构 + +``` +e2e/ +├── config/ +│ └── test.config.ts # 测试配置文件(环境、超时、测试数据等) +├── pages/ +│ ├── BasePage.ts # 页面基类 +│ ├── LoginPage.ts # 登录页面对象 +│ ├── StudentPage.ts # 学生管理页面对象 +│ ├── DreamiExplorePage.ts # DreamiExplore 页面对象 +│ └── EssayWritingPage.ts # Essay Writing 页面对象 +├── utils/ +│ └── error-handler.ts # 错误处理工具类 +├── test-1.spec.ts # 原始测试文件 +├── 文书管理.spec.ts # 原始文书管理测试 +├── 文书管理-refactored.spec.ts # 重构后的文书管理测试 +└── README.md # 本文档 + +根目录/ +├── run-tests.bat # Windows 一键运行脚本 +├── run-tests.sh # Linux/Mac 一键运行脚本 +└── playwright.config.ts # Playwright 配置文件 +``` + +## 设计特点 + +### 1. Page Object Model (POM) +- **职责分离**:每个页面有独立的 Page Object 类 +- **代码复用**:页面操作封装为可复用的方法 +- **易于维护**:UI 变化只需修改对应的 Page Object + +### 2. 配置集中管理 +- 所有配置信息集中在 `config/test.config.ts` +- 包括:环境 URL、登录凭证、超时设置、测试数据等 +- 方便切换不同环境和调整参数 + +### 3. 错误处理机制 +- 专门的 `ErrorHandler` 类管理错误 +- 自动捕获错误并继续执行 +- 生成详细的错误报告文件 +- 清理错误信息,去除技术细节 + +### 4. 可读性强 +- 测试步骤清晰明了 +- 采用描述性方法名 +- 良好的代码注释 + +## 快速开始 + +### 安装依赖 +```bash +npm install +``` + +### 运行测试 + +#### Windows 用户 +双击运行 `run-tests.bat`,或在命令行执行: +```cmd +run-tests.bat +``` + +#### Linux/Mac 用户 +首先给脚本添加执行权限: +```bash +chmod +x run-tests.sh +``` + +然后运行: +```bash +./run-tests.sh +``` + +### 手动运行测试 + +运行所有测试: +```bash +npx playwright test +``` + +运行特定测试文件: +```bash +# 运行重构版本 +npx playwright test e2e/文书管理-refactored.spec.ts + +# 运行原始版本 +npx playwright test e2e/文书管理.spec.ts +npx playwright test e2e/test-1.spec.ts +``` + +查看测试报告: +```bash +npx playwright show-report +``` + +## 配置说明 + +### 修改测试环境 +编辑 `e2e/config/test.config.ts`: + +```typescript +export const TestConfig = { + env: { + baseUrl: 'https://pre.prodream.cn/en', // 修改为你的测试环境 + loginUrl: 'https://pre.prodream.cn/en', + }, + credentials: { + email: 'your-email@example.com', // 修改登录账号 + password: 'your-password', + }, + // ... 其他配置 +}; +``` + +### 调整超时设置 +在 `test.config.ts` 中的 `timeouts` 部分调整各个操作的超时时间。 + +### 修改测试数据 +在 `test.config.ts` 中的 `testData` 部分修改测试使用的数据。 + +## 添加新的测试 + +### 1. 创建新的 Page Object +在 `e2e/pages/` 目录下创建新的页面类: + +```typescript +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class YourPage extends BasePage { + constructor(page: Page) { + super(page); + } + + async yourMethod(): Promise { + // 实现页面操作 + } +} +``` + +### 2. 在测试中使用 +在测试文件中导入并使用: + +```typescript +import { YourPage } from './pages/YourPage'; + +test('your test', async ({ page }) => { + const yourPage = new YourPage(page); + await yourPage.yourMethod(); +}); +``` + +## 错误报告 + +测试完成后会自动生成错误报告文件: +- 文件名格式:`test-error-report-{timestamp}.txt` +- 包含内容: + - 登录账号信息 + - 每个失败步骤的详细信息 + - 错误发生时的页面 URL + - 错误时间戳 + +## 最佳实践 + +1. **保持 Page Object 简洁**:每个方法只做一件事 +2. **使用有意义的方法名**:让测试代码像文档一样可读 +3. **集中管理配置**:避免硬编码 +4. **合理设置超时**:根据实际情况调整 +5. **错误处理**:利用 `executeStep` 包装测试步骤 +6. **定期维护**:随 UI 变化更新 Page Object + +## 常见问题 + +### Q: 测试超时怎么办? +A: 在 `test.config.ts` 中调整对应操作的超时时间。 + +### Q: 如何调试失败的测试? +A: +1. 查看生成的错误报告文件 +2. 使用 Playwright 的调试模式:`npx playwright test --debug` +3. 查看 HTML 报告:`npx playwright show-report` + +### Q: 如何只运行单个测试? +A: 使用 `.only`: +```typescript +test.only('your test', async ({ page }) => { + // ... +}); +``` + +## 贡献指南 + +1. 遵循现有的代码风格 +2. 为新功能添加适当的注释 +3. 更新相关文档 +4. 确保所有测试通过 + +## 技术栈 + +- **Playwright**: E2E 测试框架 +- **TypeScript**: 类型安全的 JavaScript +- **Page Object Model**: 设计模式 +- **Node.js**: 运行环境 diff --git a/config/test.config.ts b/config/test.config.ts new file mode 100644 index 0000000..38d0f53 --- /dev/null +++ b/config/test.config.ts @@ -0,0 +1,70 @@ +/** + * 测试配置文件 + * 集中管理所有测试相关的配置信息 + */ + +export const TestConfig = { + // 环境配置 + env: { + baseUrl: 'https://pre.prodream.cn/en', + loginUrl: 'https://pre.prodream.cn/en', + }, + + // 登录账号信息 + credentials: { + email: 'xdf.admin@applify.ai', + password: 'b9#0!;+{Tx4649op', + }, + + // 超时设置(毫秒) + timeouts: { + test: 600000, // 10分钟 + navigation: 30000, // 30秒 + action: 10000, // 10秒 + aiGeneration: 60000, // 1分钟 (AI生成) + longAiGeneration: 180000, // 3分钟 (长AI生成,如Essay) + veryLongAiGeneration: 240000, // 4分钟 (超长AI生成) + grammarCheck: 120000, // 2分钟 + plagiarismCheck: 100000, // 100秒 + aiDetection: 80000, // 80秒 + humanize: 110000, // 110秒 + polish: 110000, // 110秒 + rated: 120000, // 2分钟 + improvement: 200000, // 200秒 + }, + + // 测试数据 + testData: { + student: { + name: '黄子旭测试', + branchText: /^Please select branch$/, + branchValue: /^1$/, + counselorEmail: /^2-2@2\.com$/, + contractCategory: '11', + contractName: '11', + }, + material: { + title: 'Community Art & Cultural Education Project(社区艺术与文化教育项目)', + content: '在高二至高三期间,我每周投入约3小时参与社区艺术与文化教育项目,协助为当地儿童开设艺术工作坊。我主要负责示范水彩晕染、湿画法与层次叠加等技法,并教授楷书与行书的基础笔画练习,帮助孩子们理解中西方艺术表现的差异。', + expectedButtonText: 'Community Art', + }, + draft: { + level: 'Intermediate', + length: '500', + }, + }, + + // 等待时间配置 + waits: { + pageStable: 10000, // 页面稳定等待 + shortWait: 1000, // 短暂等待 + mediumWait: 3000, // 中等等待 + longWait: 10000, // 长等待 + }, + + // 报告配置 + report: { + directory: '.', // 报告保存目录 + filePrefix: 'test-error-report', // 报告文件前缀 + }, +}; diff --git a/pages/BasePage.ts b/pages/BasePage.ts new file mode 100644 index 0000000..51c4779 --- /dev/null +++ b/pages/BasePage.ts @@ -0,0 +1,58 @@ +/** + * 页面基类 + * 提供所有页面共享的方法和属性 + */ + +import { Page, expect } from '@playwright/test'; +import { TestConfig } from '../config/test.config'; + +export class BasePage { + protected page: Page; + protected config = TestConfig; + + constructor(page: Page) { + this.page = page; + } + + /** + * 等待页面稳定 + */ + async waitForPageStable(timeout: number = this.config.waits.pageStable): Promise { + console.log(`等待 ${timeout / 1000} 秒让页面稳定...`); + await this.page.waitForTimeout(timeout); + console.log('页面已稳定,继续下一步'); + } + + /** + * 导航到指定URL + */ + async goto(url: string): Promise { + await this.page.goto(url, { timeout: this.config.timeouts.navigation }); + } + + /** + * 点击元素 + */ + async click(selector: string): Promise { + await this.page.click(selector); + } + + /** + * 填充输入框 + */ + async fill(selector: string, text: string): Promise { + await this.page.fill(selector, text); + } + + /** + * 检查元素是否可见 + */ + async isVisible(selector: string, timeout?: number): Promise { + try { + await expect(this.page.locator(selector)).toBeVisible({ timeout }); + return true; + } catch { + return false; + } + } +} diff --git a/pages/DreamiExplorePage.ts b/pages/DreamiExplorePage.ts new file mode 100644 index 0000000..9a15b1c --- /dev/null +++ b/pages/DreamiExplorePage.ts @@ -0,0 +1,31 @@ +/** + * DreamiExplore 页面对象 + * 封装 DreamiExplore 相关的操作 + */ + +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class DreamiExplorePage extends BasePage { + constructor(page: Page) { + super(page); + } + + /** + * 进入 DreamiExplore + */ + async enterDreamiExplore(): Promise { + await this.page.getByRole('link').filter({ hasText: 'DreamiExplore Everything' }).click(); + await expect(this.page.getByRole('heading', { name: 'Hello, Admin' })).toBeVisible(); + } + + /** + * 测试聊天功能 + */ + async testChatFunction(message: string = 'hello'): Promise { + await this.page.getByRole('textbox').click(); + await this.page.getByRole('textbox').fill(message); + await this.page.getByRole('button', { name: 'send' }).click(); + await expect(this.page.getByRole('button', { name: '复制' })).toBeVisible({ timeout: 10000 }); + } +} diff --git a/pages/EssayWritingPage.ts b/pages/EssayWritingPage.ts new file mode 100644 index 0000000..e82b02f --- /dev/null +++ b/pages/EssayWritingPage.ts @@ -0,0 +1,229 @@ +/** + * Essay Writing 页面对象 + * 封装文书写作相关的所有操作 + */ + +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class EssayWritingPage extends BasePage { + constructor(page: Page) { + super(page); + } + + /** + * 进入 Essay Writing + */ + async enterEssayWriting(): Promise { + await this.page.getByRole('button', { name: 'documents Essay Writing' }).first().click(); + await expect(this.page.getByText('Notes')).toBeVisible(); + } + + /** + * 添加材料 + */ + async addMaterial(title: string, content: string, expectedButtonText: string): Promise { + await this.page.getByRole('button', { name: 'Add Material' }).click(); + await this.page.getByRole('menuitem', { name: 'Manual Add' }).click(); + await expect( + this.page.getByText('ClassificationGeneralGeneralAcademic Interests and AchievementsInternship and') + ).toBeVisible(); + + await this.page.getByRole('textbox', { name: 'Title' }).click(); + await this.page.getByRole('textbox', { name: 'Title' }).fill(title); + await this.page.getByRole('paragraph').filter({ hasText: /^$/ }).click(); + await this.page.locator('.tiptap').fill(content); + await this.page.getByRole('button', { name: 'icon Get Suggestions' }).click(); + await expect(this.page.getByRole('heading', { name: 'More Suggestions' })).toBeVisible(); + await this.page.getByRole('button', { name: 'Create', exact: true }).click(); + await expect(this.page.getByRole('button', { name: expectedButtonText })).toBeVisible({ + timeout: 15000 + }); + + await this.waitForPageStable(this.config.waits.shortWait); + } + + /** + * 探索 Essay Idea + */ + async exploreEssayIdea(): Promise { + await this.page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(this.page.getByRole('heading', { name: 'Select an essay prompt or' })).toBeVisible(); + } + + /** + * 加载 Recommendation + */ + async loadRecommendation(): Promise { + await this.page.getByRole('button', { name: 'icon Recommendation' }).click(); + await this.page.waitForTimeout(10000); + console.log('等待 Recommendation 加载完成...'); + await expect(this.page.getByRole('heading', { name: 'Recommendation Material' })).toBeVisible({ + timeout: this.config.timeouts.grammarCheck + }); + console.log('Recommendation Material 已出现'); + } + + /** + * 生成 Essay Idea + */ + async generateEssayIdea(): Promise { + await this.page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await this.page.getByRole('button', { name: 'Generate' }).click(); + await expect(this.page.getByRole('heading', { name: 'Essay Idea' })).toBeVisible({ + timeout: this.config.timeouts.aiGeneration + }); + } + + /** + * 生成 Essay + */ + async generateEssay(): Promise { + await this.page.getByRole('button', { name: 'icon Generate Essay' }).click(); + await this.page.getByRole('button', { name: 'Generate' }).click(); + + console.log('等待 Essay 生成完成...'); + const loadingMessage = this.page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + + await loadingMessage.waitFor({ state: 'hidden', timeout: this.config.timeouts.longAiGeneration }); + console.log('Essay 生成完成,加载消息已消失'); + + await this.waitForPageStable(); + } + + /** + * 语法检查 + */ + async performGrammarCheck(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).first().click(); + await this.page.getByRole('button', { name: 'Start Grammar Check' }).click(); + console.log('等待 Grammar Check 加载完成...'); + await expect(this.page.getByRole('heading', { name: 'suggestions' })).toBeVisible({ + timeout: this.config.timeouts.grammarCheck + }); + console.log('suggestions 已出现'); + await expect(this.page.getByLabel('suggestions')).toBeVisible({ timeout: 30000 }); + } + + /** + * 查重检查 + */ + async performPlagiarismCheck(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).nth(1).click(); + await this.page.getByRole('button', { name: 'Start Plagiarism Check' }).click(); + console.log('等待 Plagiarism Check 加载完成...'); + await expect(this.page.getByRole('button', { name: 'Re-check' })).toBeVisible({ + timeout: this.config.timeouts.plagiarismCheck + }); + console.log('Re-check 已出现'); + } + + /** + * AI检测 + */ + async performAIDetection(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).nth(2).click(); + await this.page.getByRole('button', { name: 'Start AI Detection' }).click(); + console.log('等待 AI Detection 加载完成...'); + await expect(this.page.getByRole('heading', { name: 'GPTZero Premium' })).toBeVisible({ + timeout: this.config.timeouts.aiDetection + }); + console.log('GPTZero Premium 已出现'); + } + + /** + * 人性化处理 + */ + async performHumanize(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).nth(3).click(); + await this.page.getByRole('button', { name: 'Start Humanize' }).click(); + console.log('等待 Humanize 加载完成...'); + await expect(this.page.getByText('Accept all')).toBeVisible({ + timeout: this.config.timeouts.humanize + }); + console.log('Accept all 已出现'); + } + + /** + * 润色 + */ + async performPolish(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).nth(4).click(); + await this.page.getByRole('button', { name: 'Start Polish' }).click(); + console.log('等待 Polish 加载完成...'); + await expect(this.page.getByText('All Suggestions')).toBeVisible({ + timeout: this.config.timeouts.polish + }); + console.log('All Suggestions 已出现'); + } + + /** + * 评分 + */ + async performGetRated(): Promise { + await this.page.getByRole('img', { name: 'trigger' }).nth(5).click(); + await this.page.getByRole('button', { name: 'Get Rated' }).click(); + console.log('等待 Get Rated 加载完成...'); + await expect(this.page.getByRole('heading', { name: 'Your essay rating is:' })).toBeVisible({ + timeout: this.config.timeouts.rated + }); + console.log('Your essay rating is: 已出现'); + } + + /** + * Improvement 检查 + */ + async performImprovementCheck(): Promise { + await this.page.getByRole('tab', { name: 'Improvement' }).click(); + console.log('等待 Improvement 加载完成...'); + await expect(this.page.getByText('Accept all')).toBeVisible({ + timeout: this.config.timeouts.improvement + }); + console.log('Accept all 已出现'); + } + + /** + * 返回 Essay Writing (从其他页面) + */ + async returnToEssayWriting(): Promise { + await this.page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(this.page.getByRole('button', { name: 'icon Explore Essay Idea' })).toBeVisible({ + timeout: 15000 + }); + } + + /** + * 生成 Outline + */ + async generateOutline(): Promise { + await this.page.getByRole('button', { name: 'icon Generate Outline First' }).click(); + await this.page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Outline 生成...'); + await expect(this.page.getByRole('heading', { name: 'Untitled Document' })).toBeVisible({ + timeout: this.config.timeouts.aiGeneration + }); + console.log('Outline 生成完成'); + } + + /** + * 生成 Draft + */ + async generateDraft(level: string, length: string): Promise { + await this.page.getByRole('button', { name: 'icon Generate Draft' }).click(); + await this.page.getByRole('button', { name: level }).click(); + await this.page.getByPlaceholder('Enter the expected length...').click(); + await this.page.getByPlaceholder('Enter the expected length...').fill(length); + await this.page.getByRole('button', { name: 'Generate' }).click(); + + console.log('等待 Draft 生成完成...'); + const loadingMessage = this.page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + } +} diff --git a/pages/LoginPage.ts b/pages/LoginPage.ts new file mode 100644 index 0000000..7d755c2 --- /dev/null +++ b/pages/LoginPage.ts @@ -0,0 +1,50 @@ +/** + * 登录页面对象 + * 封装登录相关的操作 + */ + +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class LoginPage extends BasePage { + constructor(page: Page) { + super(page); + } + + /** + * 访问首页 + */ + async visitHomePage(): Promise { + await this.goto(this.config.env.baseUrl); + await expect(this.page.getByRole('button', { name: 'Log in' })).toBeVisible(); + } + + /** + * 点击登录按钮 + */ + async clickLoginButton(): Promise { + await this.page.getByRole('button', { name: 'Log in' }).click(); + await expect(this.page.getByRole('textbox', { name: 'Email Address' })).toBeVisible(); + } + + /** + * 输入登录凭证并登录 + */ + async login(email: string, password: string): Promise { + await this.page.getByRole('textbox', { name: 'Email Address' }).click(); + await this.page.getByRole('textbox', { name: 'Email Address' }).fill(email); + await this.page.getByRole('textbox', { name: 'Psssword' }).click(); + await this.page.getByRole('textbox', { name: 'Psssword' }).fill(password); + await this.page.getByRole('button', { name: 'Log in' }).click(); + await expect(this.page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible(); + } + + /** + * 完整登录流程 + */ + async performLogin(): Promise { + await this.visitHomePage(); + await this.clickLoginButton(); + await this.login(this.config.credentials.email, this.config.credentials.password); + } +} diff --git a/pages/StudentPage.ts b/pages/StudentPage.ts new file mode 100644 index 0000000..f26265e --- /dev/null +++ b/pages/StudentPage.ts @@ -0,0 +1,64 @@ +/** + * 学生工作台页面对象 + * 封装学生管理相关的操作 + */ + +import { Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export interface StudentData { + name: string; + branchText: RegExp; + branchValue: RegExp; + counselorEmail: RegExp; + contractCategory: string; + contractName: string; +} + +export class StudentPage extends BasePage { + constructor(page: Page) { + super(page); + } + + /** + * 返回学生工作台 + */ + async goToStudentWorkbench(): Promise { + await this.page.getByRole('link').filter({ hasText: '学生工作台' }).click(); + await expect(this.page.getByText('Student Name')).toBeVisible(); + } + + /** + * 创建新学生 + */ + async createNewStudent(studentData: StudentData): Promise { + await this.page.getByRole('button', { name: 'New Student' }).click(); + + // 填写学生名称 + await this.page.getByRole('textbox', { name: 'Name *', exact: true }).click(); + await this.page.getByRole('textbox', { name: 'Name *', exact: true }).fill(studentData.name); + + // 选择分公司 + await this.page.locator('div').filter({ hasText: studentData.branchText }).nth(2).click(); + await this.page.locator('div').filter({ hasText: studentData.branchValue }).nth(1).click(); + + // 选择顾问 + await this.page.getByText('Select counselor').click(); + await this.page.locator('div').filter({ hasText: studentData.counselorEmail }).nth(1).click(); + + // 点击日期选择器关闭按钮 + await this.page.locator('svg').nth(5).click(); + + // 填写合同信息 + await this.page.getByRole('textbox', { name: 'Contract Category *' }).click(); + await this.page.getByRole('textbox', { name: 'Contract Category *' }).fill(studentData.contractCategory); + await this.page.getByRole('textbox', { name: 'Contract Name *' }).click(); + await this.page.getByRole('textbox', { name: 'Contract Name *' }).fill(studentData.contractName); + + // 确认创建 + await this.page.getByRole('button', { name: 'Confirm' }).click(); + + // 验证学生创建成功 + await expect(this.page.getByText(studentData.name).first()).toBeVisible(); + } +} diff --git a/test-1.spec.ts b/test-1.spec.ts new file mode 100644 index 0000000..6c2c9e6 --- /dev/null +++ b/test-1.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('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('prodream.admin@applify.ai'); + await page.getByRole('textbox', { name: 'Psssword' }).click(); + await page.getByRole('textbox', { name: 'Psssword' }).fill('b9#0!;+{Tx4649op'); + await page.getByRole('button', { name: 'Log in' }).click(); +}); \ No newline at end of file diff --git a/utils/error-handler.ts b/utils/error-handler.ts new file mode 100644 index 0000000..726bd9b --- /dev/null +++ b/utils/error-handler.ts @@ -0,0 +1,156 @@ +/** + * 错误处理工具类 + * 负责错误收集、清理和报告生成 + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { Page } from '@playwright/test'; + +export interface ErrorRecord { + step: string; + error: string; + timestamp: string; + pageUrl: string; +} + +export interface LoginInfo { + email: string; + password: string; +} + +export class ErrorHandler { + private errors: ErrorRecord[] = []; + private loginInfo: LoginInfo; + + constructor(loginInfo: LoginInfo) { + this.loginInfo = loginInfo; + } + + /** + * 清理错误信息,去除技术细节 + */ + private cleanErrorMessage(error: unknown): string { + let errorMessage = error instanceof Error ? error.message : String(error); + + // 只保留第一行主要错误,去掉 Call log 等技术细节 + const firstLine = errorMessage.split('\n')[0]; + const cleanError = firstLine.replace(/\s+\(.*?\)/, '').trim(); + + return cleanError; + } + + /** + * 记录错误 + */ + recordError(stepName: string, error: unknown, page: Page): void { + const cleanError = this.cleanErrorMessage(error); + const timestamp = new Date().toISOString(); + const pageUrl = page.url(); + + this.errors.push({ + step: stepName, + error: cleanError, + timestamp, + pageUrl, + }); + + console.error(`[失败] ${stepName}: ${cleanError}`); + console.error(`[页面URL] ${pageUrl}`); + } + + /** + * 执行步骤并捕获错误 + */ + async executeStep( + stepName: string, + stepFunction: () => Promise, + page: Page + ): Promise { + try { + console.log(`\n[执行] ${stepName}`); + await stepFunction(); + console.log(`[成功] ${stepName}`); + } catch (error) { + this.recordError(stepName, error, page); + // 继续执行下一步,不中断测试流程 + } + } + + /** + * 获取所有错误 + */ + getErrors(): ErrorRecord[] { + return this.errors; + } + + /** + * 检查是否有错误 + */ + hasErrors(): boolean { + return this.errors.length > 0; + } + + /** + * 生成错误报告文件 + */ + generateErrorReport(reportDir: string = '.', filePrefix: string = 'test-error-report'): string { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(reportDir, `${filePrefix}-${timestamp}.txt`); + + let report = '========== 测试错误报告 ==========\n\n'; + report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`; + + // 登录账号信息(只显示一次) + report += '【登录账号信息】\n'; + report += `账号: ${this.loginInfo.email}\n`; + report += `密码: ${this.loginInfo.password}\n`; + report += `测试环境: https://pre.prodream.cn/en\n\n`; + report += `${'='.repeat(60)}\n\n`; + + if (this.errors.length === 0) { + report += '✅ 所有功能测试通过,未发现问题!\n'; + } else { + report += `发现 ${this.errors.length} 个问题,详情如下:\n\n`; + + // 每个错误按照格式:问题功能 + 页面链接 + this.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; + } + + /** + * 打印错误摘要到控制台 + */ + printSummary(): void { + console.log('\n\n========== 测试执行完成 =========='); + + if (this.errors.length === 0) { + console.log('✅ 所有步骤执行成功!'); + } else { + console.log(`\n⚠️ 发现 ${this.errors.length} 个问题:\n`); + this.errors.forEach((err, index) => { + console.log(`【问题 ${index + 1}】`); + console.log(` 问题功能: ${err.step}`); + console.log(` 页面链接: ${err.pageUrl}`); + console.log(` 错误详情: ${err.error}`); + console.log(''); + }); + + console.log(`\n账号: ${this.loginInfo.email}`); + console.log(`密码: ${this.loginInfo.password}\n`); + } + } +} diff --git a/学生工作台测试.spec.ts b/学生工作台测试.spec.ts new file mode 100644 index 0000000..b69f7f3 --- /dev/null +++ b/学生工作台测试.spec.ts @@ -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), + 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}`); + } +}); \ No newline at end of file diff --git a/学生工作台生产.spec.ts b/学生工作台生产.spec.ts new file mode 100644 index 0000000..b69f7f3 --- /dev/null +++ b/学生工作台生产.spec.ts @@ -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), + 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}`); + } +}); \ No newline at end of file diff --git a/文书管理-refactored.spec.ts b/文书管理-refactored.spec.ts new file mode 100644 index 0000000..05f498e --- /dev/null +++ b/文书管理-refactored.spec.ts @@ -0,0 +1,212 @@ +/** + * 文书管理测试 - 重构版本 + * 采用 Page Object Model 设计模式,提高代码可维护性和可读性 + */ + +import { test } from '@playwright/test'; +import { TestConfig } from './config/test.config'; +import { ErrorHandler } from './utils/error-handler'; +import { LoginPage } from './pages/LoginPage'; +import { DreamiExplorePage } from './pages/DreamiExplorePage'; +import { StudentPage } from './pages/StudentPage'; +import { EssayWritingPage } from './pages/EssayWritingPage'; + +test('文书管理完整流程测试', async ({ page }) => { + // 设置测试超时时间 + test.setTimeout(TestConfig.timeouts.test); + + // 初始化错误处理器 + const errorHandler = new ErrorHandler(TestConfig.credentials); + + // 初始化页面对象 + const loginPage = new LoginPage(page); + const dreamiExplorePage = new DreamiExplorePage(page); + const studentPage = new StudentPage(page); + const essayWritingPage = new EssayWritingPage(page); + + // 辅助函数:执行步骤并捕获错误 + const executeStep = async (stepName: string, stepFunction: () => Promise) => { + await errorHandler.executeStep(stepName, stepFunction, page); + }; + + // ========== 登录流程 ========== + await executeStep('访问首页', async () => { + await loginPage.visitHomePage(); + }); + + await executeStep('点击登录按钮', async () => { + await loginPage.clickLoginButton(); + }); + + await executeStep('输入登录信息', async () => { + await loginPage.login(TestConfig.credentials.email, TestConfig.credentials.password); + }); + + // ========== DreamiExplore 测试 ========== + await executeStep('进入 DreamiExplore', async () => { + await dreamiExplorePage.enterDreamiExplore(); + }); + + await executeStep('测试聊天功能', async () => { + await dreamiExplorePage.testChatFunction(); + }); + + // ========== 学生管理流程 ========== + await executeStep('返回学生工作台', async () => { + await studentPage.goToStudentWorkbench(); + }); + + await executeStep('创建新学生', async () => { + await studentPage.createNewStudent(TestConfig.testData.student); + }); + + // ========== Essay Writing 第一轮流程 ========== + await executeStep('进入 Essay Writing', async () => { + await essayWritingPage.enterEssayWriting(); + }); + + await executeStep('添加材料', async () => { + await essayWritingPage.addMaterial( + TestConfig.testData.material.title, + TestConfig.testData.material.content, + TestConfig.testData.material.expectedButtonText + ); + }); + + await executeStep('探索 Essay Idea', async () => { + await essayWritingPage.exploreEssayIdea(); + }); + + await executeStep('加载 Recommendation', async () => { + await essayWritingPage.loadRecommendation(); + }); + + await executeStep('生成 Essay Idea', async () => { + await essayWritingPage.generateEssayIdea(); + }); + + await executeStep('生成 Essay', async () => { + await essayWritingPage.generateEssay(); + }); + + // ========== 各种检查功能 ========== + await executeStep('语法检查 (Grammar Check)', async () => { + await essayWritingPage.performGrammarCheck(); + }); + + await executeStep('查重检查 (Plagiarism Check)', async () => { + await essayWritingPage.performPlagiarismCheck(); + }); + + await executeStep('AI检测 (AI Detection)', async () => { + await essayWritingPage.performAIDetection(); + }); + + await executeStep('人性化处理 (Humanize)', async () => { + await essayWritingPage.performHumanize(); + }); + + await executeStep('润色 (Polish)', async () => { + await essayWritingPage.performPolish(); + }); + + await executeStep('评分 (Get Rated)', async () => { + await essayWritingPage.performGetRated(); + }); + + await executeStep('Improvement 检查', async () => { + await essayWritingPage.performImprovementCheck(); + }); + + // ========== Essay Writing 第二轮流程 ========== + await executeStep('返回 Essay Writing', async () => { + await essayWritingPage.returnToEssayWriting(); + }); + + await executeStep('再次探索 Essay Idea', async () => { + await essayWritingPage.exploreEssayIdea(); + }); + + await executeStep('再次加载 Recommendation', async () => { + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + console.log('等待 Recommendation 加载完成...'); + await page.waitForTimeout(10000); + console.log('Recommendation 加载完成'); + }); + + await executeStep('再次生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Essay Idea 生成...'); + await page.waitForTimeout(60000); + console.log('Essay Idea 生成完成'); + }); + + await page.waitForTimeout(10000); + + // ========== Student Guide 测试 ========== + await executeStep('测试 Student Guide 展开/收起', async () => { + await page.getByRole('button', { name: 'Student Guide' }).click(); + console.log('等待 Student Guide 加载完成...'); + await page.waitForTimeout(110000); + + await page.getByRole('button', { name: 'Copy' }).click(); + await page.waitForTimeout(5000); + + await page.getByLabel('Student Guide').getByRole('button').filter({ hasText: /^$/ }).click(); + await page.waitForTimeout(10000); + }); + + // ========== 翻译和复制测试 ========== + await executeStep('测试翻译功能', async () => { + await page.getByRole('button', { name: 'Translate Translate' }).click(); + await page.waitForTimeout(3000); + }); + + await executeStep('测试复制到剪贴板', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await page.waitForTimeout(5000); + await page.waitForTimeout(10000); + }); + + await executeStep('测试翻译返回英文', async () => { + await page.getByRole('button', { name: 'Translate Translate' }).click(); + await page.waitForTimeout(2000); + }); + + await executeStep('再次测试复制功能', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await page.waitForTimeout(5000); + }); + + // ========== 重新生成测试 ========== + await executeStep('测试重新生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'Regenerate' }).click(); + console.log('等待重新生成 Essay Idea...'); + await page.waitForTimeout(60000); + console.log('重新生成完成'); + }); + + // ========== Outline 和 Draft 生成 ========== + await executeStep('生成 Outline', async () => { + await essayWritingPage.generateOutline(); + }); + + await executeStep('生成 Draft', async () => { + await essayWritingPage.generateDraft( + TestConfig.testData.draft.level, + TestConfig.testData.draft.length + ); + }); + + // ========== 生成测试报告 ========== + errorHandler.printSummary(); + + if (errorHandler.hasErrors()) { + const reportPath = errorHandler.generateErrorReport( + TestConfig.report.directory, + TestConfig.report.filePrefix + ); + console.log(`📄 详细错误报告已保存到: ${reportPath}`); + } +}); diff --git a/文书管理初版.spec.ts b/文书管理初版.spec.ts new file mode 100644 index 0000000..60f43d5 --- /dev/null +++ b/文书管理初版.spec.ts @@ -0,0 +1,267 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +test('test', async ({ page }) => { + test.setTimeout(600000); // 设置超时时间为10分钟,因为有多个 AI 生成步骤和长时间等待 + + // 错误收集数组 + const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = []; + + // 登录信息(用于报告) + const loginInfo = { + email: 'xdf.admin@applify.ai', + password: 'b9#0!;+{Tx4649op' + }; + + // 辅助函数:执行步骤并捕获错误 + const executeStep = async (stepName: string, stepFunction: () => Promise) => { + try { + console.log(`\n[执行] ${stepName}`); + await stepFunction(); + console.log(`[成功] ${stepName}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const timestamp = new Date().toISOString(); + const pageUrl = page.url(); // 获取出错时的页面URL + errors.push({ step: stepName, error: errorMessage, timestamp, pageUrl }); + console.error(`[失败] ${stepName}: ${errorMessage}`); + console.error(`[页面URL] ${pageUrl}`); + // 继续执行下一步,不中断测试流程 + } + }; + + // 步骤1: 访问首页并登录 + await executeStep('访问首页', async () => { + await page.goto('https://pre.prodream.cn/en'); + await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible(); + }); + + 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('xdf.admin@applify.ai'); + await page.getByRole('textbox', { name: 'Psssword' }).click(); + await page.getByRole('textbox', { name: 'Psssword' }).fill('b9#0!;+{Tx4649op'); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible(); + }); + + // 步骤2: 测试 DreamiExplore + await executeStep('进入 DreamiExplore', async () => { + await page.getByRole('link').filter({ hasText: 'DreamiExplore Everything' }).click(); + await expect(page.getByRole('heading', { name: 'Hello, Admin' })).toBeVisible(); + }); + + await executeStep('测试聊天功能', async () => { + 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: 10000 }); + }); + + // 步骤3: 创建新学生 + 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: /^请选择分公司$/ }).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(); + // 等待学生创建成功,使用 first() 处理多个匹配 + await expect(page.getByText('黄子旭测试').first()).toBeVisible(); + }); + + // 步骤4: Essay Writing 流程 + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'documents Essay Writing' }).first().click(); + await expect(page.getByText('Notes')).toBeVisible(); + }); + + await executeStep('添加材料', async () => { + await page.getByRole('button', { name: 'Add Material' }).click(); + await page.getByRole('menuitem', { name: 'Manual Add' }).click(); + await expect(page.getByText('ClassificationGeneralGeneralAcademic Interests and AchievementsInternship and')).toBeVisible(); + await page.getByRole('textbox', { name: 'Title' }).click(); + await page.getByRole('textbox', { name: 'Title' }).fill('i like Math and English'); + await page.getByRole('paragraph').filter({ hasText: /^$/ }).click(); + await page.locator('.tiptap').fill('i like Math and English'); + await page.getByRole('button', { name: 'icon Get Suggestions' }).click(); + await expect(page.getByRole('heading', { name: 'More Suggestions' })).toBeVisible(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await expect(page.getByRole('button', { name: 'General i like Math and' })).toBeVisible({ timeout: 15000 }); + // 等待页面稳定后再点击 + await page.waitForTimeout(1000); + }); + + await executeStep('探索 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(page.getByRole('heading', { name: 'Select an essay prompt or' })).toBeVisible(); + }); + + await executeStep('加载 Recommendation', async () => { + // 点击 Recommendation 按钮 + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + await page.waitForTimeout(10000); + console.log('等待 Recommendation 加载完成...'); + await expect(page.getByRole('heading', { name: 'Recommendation Material' })).toBeVisible({ timeout: 120000 }); + console.log('Recommendation Material 已出现'); + }); + + await executeStep('生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + // AI 生成需要时间,增加超时 + await expect(page.getByRole('heading', { name: 'Essay Idea' })).toBeVisible({ timeout: 60000 }); + }); + + await executeStep('生成 Essay', async () => { + await page.getByRole('button', { name: 'icon Generate Essay' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + // 等待 Essay 生成完成 - 通过检测占位符文本消失来判断 + console.log('等待 Essay 生成完成...'); + const placeholderText = page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + // 等待占位符文本消失,最多等待 4 分钟(240秒) + await placeholderText.waitFor({ state: 'hidden', timeout: 240000 }); + console.log('Essay 生成完成,占位符已消失'); + // 等待 10 秒钟让页面稳定 + await page.waitForTimeout(10000); + }); + + // 步骤5: 各种检查功能 + await executeStep('语法检查 (Grammar Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).first().click(); + await page.getByRole('button', { name: 'Start Grammar Check' }).click(); + // 语法检查需要时间 + console.log('等待 Start Grammar Check 加载完成...'); + await expect(page.getByRole('heading', { name: 'suggestions' })).toBeVisible({ timeout: 120000 }); + console.log('suggestions 已出现'); + await expect(page.getByLabel('suggestions')).toBeVisible({ timeout: 30000 }); + }); + + await executeStep('查重检查 (Plagiarism Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(1).click(); + await page.getByRole('button', { name: 'Start Plagiarism Check' }).click(); + console.log('等待 Start Plagiarism Check 加载完成...'); + // 查重检查需要较长时间 + await expect(page.getByRole('button', { name: 'Re-check' })).toBeVisible({ timeout: 100000 }); + console.log('Re-check 已出现'); + }); + + await executeStep('AI检测 (AI Detection)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(2).click(); + await page.getByRole('button', { name: 'Start AI Detection' }).click(); + console.log('等待 Start AI Detection 加载完成...'); + await expect(page.getByRole('heading', { name: 'GPTZero Premium' })).toBeVisible({ timeout: 80000 }); + console.log('GPTZero Premium 已出现'); + }); + + await executeStep('人性化处理 (Humanize)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(3).click(); + await page.getByRole('button', { name: 'Start Humanize' }).click(); + console.log('等待 Start Humanize 加载完成...'); + await expect(page.getByText('Accept all')).toBeVisible({ timeout: 110000 }); + console.log('Accept all 已出现'); + }); + + await executeStep('润色 (Polish)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(4).click(); + await page.getByRole('button', { name: 'Start Polish' }).click(); + console.log('等待 Start Polish 加载完成...'); + await expect(page.getByText('All Suggestions')).toBeVisible({ timeout: 110000 }); + console.log('All Suggestions 已出现'); + }); + + await executeStep('评分 (Get Rated)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(5).click(); + await page.getByRole('button', { name: 'Get Rated' }).click(); + console.log('等待 Get Rated 加载完成...'); + await expect(page.getByRole('heading', { name: 'Your essay rating is:' })).toBeVisible({ timeout: 120000 }); + console.log('Your essay rating is: 已出现'); + }); + + await executeStep('Improvement 检查', async () => { + await page.getByRole('tab', { name: 'Improvement' }).click(); + console.log('等待 Improvement 加载完成...'); + await expect(page.getByRole('heading', { name: 'Suggestions' })).toBeVisible({ timeout: 120000 }); + console.log('Suggestions 已出现)'); + }); + + // 最后输出错误汇总 + console.log('\n\n========== 测试执行完成 =========='); + + // 生成错误报告文件 + const generateErrorReport = () => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(process.cwd(), `test-error-report-${timestamp}.txt`); + + let report = '========== 测试错误报告 ==========\n\n'; + report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`; + + // 第一部分:登录账号信息(只显示一次) + report += '【登录账号信息】\n'; + report += `账号: ${loginInfo.email}\n`; + report += `密码: ${loginInfo.password}\n`; + report += `测试环境: https://pre.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账号: ${loginInfo.email}`); + console.log(`密码: ${loginInfo.password}\n`); + + // 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤 + } +}); diff --git a/文书管理测试.spec.ts b/文书管理测试.spec.ts new file mode 100644 index 0000000..67a1d81 --- /dev/null +++ b/文书管理测试.spec.ts @@ -0,0 +1,444 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +test('test', async ({ page }) => { + test.setTimeout(600000); // 设置超时时间为10分钟,因为有多个 AI 生成步骤和长时间等待 + + // 错误收集数组 + const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = []; + + // 登录信息(用于报告) + const loginInfo = { + email: 'xdf.admin@applify.ai', + password: 'b9#0!;+{Tx4649op' + }; + + // 辅助函数:执行步骤并捕获错误 + 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); + + // 清理错误信息:只保留第一行主要错误,去掉 Call log 等技术细节 + const firstLine = errorMessage.split('\n')[0]; + const cleanError = firstLine.replace(/\s+\(.*?\)/, '').trim(); + + const timestamp = new Date().toISOString(); + const pageUrl = page.url(); // 获取出错时的页面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 expect(page.getByRole('button', { name: 'Log in' })).toBeVisible(); + }); + + 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('xdf.admin@applify.ai'); + await page.getByRole('textbox', { name: 'Psssword' }).click(); + await page.getByRole('textbox', { name: 'Psssword' }).fill('b9#0!;+{Tx4649op'); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible(); + }); + + // 步骤2: 测试 DreamiExplore + await executeStep('进入 DreamiExplore', async () => { + await page.getByRole('link').filter({ hasText: 'DreamiExplore Everything' }).click(); + await expect(page.getByRole('heading', { name: 'Hello, Admin' })).toBeVisible(); + }); + + await executeStep('测试聊天功能', async () => { + 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: 10000 }); + }); + + // 步骤3: 创建新学生 + 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(); + // 等待学生创建成功,使用 first() 处理多个匹配 + await expect(page.getByText('黄子旭测试').first()).toBeVisible(); + }); + + // 步骤4: Essay Writing 流程 + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'documents Essay Writing' }).first().click(); + await expect(page.getByText('Notes')).toBeVisible(); + }); + + await executeStep('添加材料', async () => { + await page.getByRole('button', { name: 'Add Material' }).click(); + await page.getByRole('menuitem', { name: 'Manual Add' }).click(); + await expect(page.getByText('ClassificationGeneralGeneralAcademic Interests and AchievementsInternship and')).toBeVisible(); + await page.getByRole('textbox', { name: 'Title' }).click(); + await page.getByRole('textbox', { name: 'Title' }).fill('Community Art & Cultural Education Project(社区艺术与文化教育项目)'); + await page.getByRole('paragraph').filter({ hasText: /^$/ }).click(); + await page.locator('.tiptap').fill('在高二至高三期间,我每周投入约3小时参与社区艺术与文化教育项目,协助为当地儿童开设艺术工作坊。我主要负责示范水彩晕染、湿画法与层次叠加等技法,并教授楷书与行书的基础笔画练习,帮助孩子们理解中西方艺术表现的差异。'); + await page.getByRole('button', { name: 'icon Get Suggestions' }).click(); + await expect(page.getByRole('heading', { name: 'More Suggestions' })).toBeVisible(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await expect(page.getByRole('button', { name: 'Community Art' })).toBeVisible({ timeout: 15000 }); + // 等待页面稳定后再点击 + await page.waitForTimeout(1000); + }); + + await executeStep('探索 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(page.getByRole('heading', { name: 'Select an essay prompt or' })).toBeVisible(); + }); + + await executeStep('加载 Recommendation', async () => { + // 点击 Recommendation 按钮 + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + await page.waitForTimeout(10000); + console.log('等待 Recommendation 加载完成...'); + await expect(page.getByRole('heading', { name: 'Recommendation Material' })).toBeVisible({ timeout: 120000 }); + console.log('Recommendation Material 已出现'); + }); + + await executeStep('生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + // AI 生成需要时间,增加超时 + await expect(page.getByRole('heading', { name: 'Essay Idea' })).toBeVisible({ timeout: 60000 }); + }); + + await executeStep('生成 Essay', async () => { + await page.getByRole('button', { name: 'icon Generate Essay' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + + console.log('等待 Essay 生成完成...'); + const loadingMessage = page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + + // 第一步:先等待加载消息出现(确保生成已开始) + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + + // 第二步:等待加载消息消失(最多等待 3 分钟) + await loadingMessage.waitFor({ state: 'hidden', timeout: 180000 }); + console.log('Essay 生成完成,加载消息已消失'); + + // 第三步:等待 10 秒钟让页面稳定 + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤5: 各种检查功能 + await executeStep('语法检查 (Grammar Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).first().click(); + await page.getByRole('button', { name: 'Start Grammar Check' }).click(); + // 语法检查需要时间 + console.log('等待 Start Grammar Check 加载完成...'); + await expect(page.getByRole('heading', { name: 'suggestions' })).toBeVisible({ timeout: 120000 }); + console.log('suggestions 已出现'); + await expect(page.getByLabel('suggestions')).toBeVisible({ timeout: 30000 }); + }); + + await executeStep('查重检查 (Plagiarism Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(1).click(); + await page.getByRole('button', { name: 'Start Plagiarism Check' }).click(); + console.log('等待 Start Plagiarism Check 加载完成...'); + // 查重检查需要较长时间 + await expect(page.getByRole('button', { name: 'Re-check' })).toBeVisible({ timeout: 200000 }); + console.log('Re-check 已出现'); + }); + + await executeStep('AI检测 (AI Detection)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(2).click(); + await page.getByRole('button', { name: 'Start AI Detection' }).click(); + console.log('等待 Start AI Detection 加载完成...'); + await expect(page.getByRole('heading', { name: 'GPTZero Premium' })).toBeVisible({ timeout: 80000 }); + console.log('GPTZero Premium 已出现'); + }); + + await executeStep('人性化处理 (Humanize)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(3).click(); + await page.getByRole('button', { name: 'Start Humanize' }).click(); + console.log('等待 Start Humanize 加载完成...'); + await expect(page.getByText('Accept all')).toBeVisible({ timeout: 110000 }); + console.log('Accept all 已出现'); + }); + + await executeStep('润色 (Polish)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(4).click(); + await page.getByRole('button', { name: 'Start Polish' }).click(); + console.log('等待 Start Polish 加载完成...'); + await expect(page.getByText('All Suggestions')).toBeVisible({ timeout: 110000 }); + console.log('All Suggestions 已出现'); + }); + + await executeStep('评分 (Get Rated)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(5).click(); + await page.getByRole('button', { name: 'Get Rated' }).click(); + console.log('等待 Get Rated 加载完成...'); + await expect(page.getByRole('heading', { name: 'Your essay rating is:' })).toBeVisible({ timeout: 120000 }); + console.log('Your essay rating is: 已出现'); + }); + + await executeStep('Improvement 检查', async () => { + await page.getByRole('tab', { name: 'Improvement' }).click(); + console.log('等待 Improvement 加载完成...'); + await expect(page.getByText('Accept all')).toBeVisible({ timeout: 110000 }); + console.log('Accept all 已出现)'); + }); + + // 步骤6: 进入 Essay Writing + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByRole('button', { name: 'icon Explore Essay Idea' })).toBeVisible({ timeout: 15000 }); + }); + + // 步骤7: 探索 Essay Idea + await executeStep('探索 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(page.getByRole('heading', { name: 'Prompt Answer Guide' })).toBeVisible({ timeout: 15000 }); + }); + + // 步骤8: 加载 Recommendation + await executeStep('加载 Recommendation', async () => { + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + console.log('等待 Recommendation 加载完成...'); + await page.waitForTimeout(10000); + console.log('Recommendation 加载完成'); + }); + + // 步骤9: 生成 Essay Idea + await executeStep('生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Essay Idea 生成...'); + await expect(page.getByRole('button', { name: 'Student Guide' })).toBeVisible({ timeout: 60000 }); + console.log('Essay Idea 生成完成'); + }); + + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + + // 步骤10: 测试 Student Guide 功能 + await executeStep('测试 Student Guide 展开/收起', async () => { + // 展开 Student Guide + await page.getByRole('button', { name: 'Student Guide' }).click(); + console.log('等待 Student Guide 加载完成...'); + await expect(page.getByText('**')).toBeVisible({ timeout: 110000 }); + // 测试复制功能 + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 110000 }); + await page.getByLabel('Student Guide').getByRole('button').filter({ hasText: /^$/ }).click(); + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤11: 测试翻译功能 + await executeStep('测试翻译功能', async () => { + // 点击翻译按钮,英文 → 中文 + await page.getByRole('button', { name: 'Translate Translate' }).click(); + console.log('点击翻译按钮,等待翻译完成...'); + + // 等待翻译完成(等待按钮重新变为可用状态或等待固定时间) + await page.waitForTimeout(3000); + + // 验证翻译按钮仍然存在(表示功能可用) + await expect(page.getByRole('button', { name: 'Translate Translate' })).toBeVisible({ timeout: 5000 }); + console.log('翻译功能正常(已切换为中文)'); + }); + + // 步骤12: 测试复制功能 + await executeStep('测试复制到剪贴板', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 5000 }); + console.log('复制功能正常'); + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤13: 测试再次翻译 + await executeStep('测试翻译返回英文', async () => { + await page.getByRole('button', { name: 'Translate Translate' }).click(); + await page.waitForTimeout(2000); + console.log('翻译返回英文完成'); + }); + + // 步骤14: 再次测试复制 + await executeStep('再次测试复制功能', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 5000 }); + }); + + // 步骤15: 测试重新生成 Essay Idea + await executeStep('测试重新生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'Regenerate' }).click(); + console.log('等待重新生成 Essay Idea...'); + await expect(page.getByRole('button', { name: 'Student Guide' })).toBeVisible({ timeout: 60000 }); + console.log('重新生成完成'); + }); + + // 步骤16: 生成 Outline + await executeStep('生成 Outline', async () => { + await page.getByRole('button', { name: 'icon Generate Outline First' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Outline 生成...'); + await expect(page.getByRole('heading', { name: 'Untitled Document' })).toBeVisible({ timeout: 60000 }); + console.log('Outline 生成完成'); + }); + + // 步骤17: 生成 Draft + await executeStep('生成 Draft', async () => { + await page.getByRole('button', { name: 'icon Generate Draft' }).click(); + await page.getByRole('button', { name: 'Intermediate' }).click(); + await page.getByPlaceholder('Enter the expected length...').click(); + await page.getByPlaceholder('Enter the expected length...').fill('500'); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Draft 生成完成...'); + const loadingMessage = page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + // 第一步:先等待加载消息出现(确保生成已开始) + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + }); + // 步骤18: 进入 Essay Writing + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByText('Notes')).toBeVisible({ timeout: 15000 }); + }); + + // 步骤19: 点击 Essay Writing 确认界面 + await executeStep('确认 Essay Writing 界面', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByRole('button', { name: 'Add Material' })).toBeVisible({ timeout: 10000 }); + console.log('Essay Writing 界面已加载'); + }); + + // 步骤20: 测试文件上传功能 + await executeStep('打开文件上传对话框', async () => { + await page.getByRole('button', { name: 'Add Material' }).click(); + await page.getByRole('menuitem', { name: 'Upload File' }).click(); + await expect(page.getByText('Click to upload or drag files')).toBeVisible({ timeout: 10000 }); + console.log('文件上传对话框已打开'); + }); + + await executeStep('上传文件', async () => { + // 检查文件是否存在 + const filePath = path.join(process.cwd(), 'math.docx'); + if (!fs.existsSync(filePath)) { + console.warn(`⚠️ 警告: 文件 math.docx 不存在,跳过上传测试`); + throw new Error(`文件不存在: ${filePath}`); + } + + console.log(`准备上传文件: ${filePath}`); + await page.locator('input[type="file"]').setInputFiles(filePath); + + // 等待上传状态显示 + console.log('等待文件上传...'); + await page.waitForTimeout(2000); + + // 点击上传按钮 + await page.getByRole('button', { name: 'Upload' }).click(); + console.log('已点击上传按钮'); + + // 等待上传完成 - 验证文件出现在材料列表中 + await expect(page.getByRole('button', { name: 'docx' }).first()).toBeVisible({ timeout: 30000 }); + console.log('文件上传成功'); + }); + + // 最后输出错误汇总 + console.log('\n\n========== 测试执行完成 =========='); + + // 生成错误报告文件 + const generateErrorReport = () => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(process.cwd(), `test-error-report-${timestamp}.txt`); + + let report = '========== 测试错误报告 ==========\n\n'; + report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`; + + // 第一部分:登录账号信息(只显示一次) + report += '【登录账号信息】\n'; + report += `账号: ${loginInfo.email}\n`; + report += `密码: ${loginInfo.password}\n`; + report += `测试环境: https://pre.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账号: ${loginInfo.email}`); + console.log(`密码: ${loginInfo.password}\n`); + + // 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤 + } +}); diff --git a/文书管理生产.spec.ts b/文书管理生产.spec.ts new file mode 100644 index 0000000..87b6395 --- /dev/null +++ b/文书管理生产.spec.ts @@ -0,0 +1,444 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +test('test', async ({ page }) => { + test.setTimeout(600000); // 设置超时时间为10分钟,因为有多个 AI 生成步骤和长时间等待 + + // 错误收集数组 + const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = []; + + // 登录信息(用于报告) + const loginInfo = { + email: 'xdf.admin@applify.ai', + password: 'b9#0!;+{Tx4649op' + }; + + // 辅助函数:执行步骤并捕获错误 + 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); + + // 清理错误信息:只保留第一行主要错误,去掉 Call log 等技术细节 + const firstLine = errorMessage.split('\n')[0]; + const cleanError = firstLine.replace(/\s+\(.*?\)/, '').trim(); + + const timestamp = new Date().toISOString(); + const pageUrl = page.url(); // 获取出错时的页面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://prodream.cn/en'); + await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible(); + }); + + 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('prodream.admin@applify.ai'); + await page.getByRole('textbox', { name: 'Psssword' }).click(); + await page.getByRole('textbox', { name: 'Psssword' }).fill('b9#0!;+{Tx4649op'); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.getByRole('link').filter({ hasText: '学生工作台' })).toBeVisible(); + }); + + // 步骤2: 测试 DreamiExplore + await executeStep('进入 DreamiExplore', async () => { + await page.getByRole('link').filter({ hasText: 'DreamiExplore Everything' }).click(); + await expect(page.getByRole('heading', { name: 'Hello, Admin' })).toBeVisible(); + }); + + await executeStep('测试聊天功能', async () => { + 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: 10000 }); + }); + + // 步骤3: 创建新学生 + 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(); + // 等待学生创建成功,使用 first() 处理多个匹配 + await expect(page.getByText('黄子旭测试').first()).toBeVisible(); + }); + + // 步骤4: Essay Writing 流程 + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'documents Essay Writing' }).first().click(); + await expect(page.getByText('Notes')).toBeVisible(); + }); + + await executeStep('添加材料', async () => { + await page.getByRole('button', { name: 'Add Material' }).click(); + await page.getByRole('menuitem', { name: 'Manual Add' }).click(); + await expect(page.getByText('ClassificationGeneralGeneralAcademic Interests and AchievementsInternship and')).toBeVisible(); + await page.getByRole('textbox', { name: 'Title' }).click(); + await page.getByRole('textbox', { name: 'Title' }).fill('Community Art & Cultural Education Project(社区艺术与文化教育项目)'); + await page.getByRole('paragraph').filter({ hasText: /^$/ }).click(); + await page.locator('.tiptap').fill('在高二至高三期间,我每周投入约3小时参与社区艺术与文化教育项目,协助为当地儿童开设艺术工作坊。我主要负责示范水彩晕染、湿画法与层次叠加等技法,并教授楷书与行书的基础笔画练习,帮助孩子们理解中西方艺术表现的差异。'); + await page.getByRole('button', { name: 'icon Get Suggestions' }).click(); + await expect(page.getByRole('heading', { name: 'More Suggestions' })).toBeVisible(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await expect(page.getByRole('button', { name: 'Community Art' })).toBeVisible({ timeout: 15000 }); + // 等待页面稳定后再点击 + await page.waitForTimeout(1000); + }); + + await executeStep('探索 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(page.getByRole('heading', { name: 'Select an essay prompt or' })).toBeVisible(); + }); + + await executeStep('加载 Recommendation', async () => { + // 点击 Recommendation 按钮 + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + await page.waitForTimeout(10000); + console.log('等待 Recommendation 加载完成...'); + await expect(page.getByRole('heading', { name: 'Recommendation Material' })).toBeVisible({ timeout: 120000 }); + console.log('Recommendation Material 已出现'); + }); + + await executeStep('生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + // AI 生成需要时间,增加超时 + await expect(page.getByRole('heading', { name: 'Essay Idea' })).toBeVisible({ timeout: 60000 }); + }); + + await executeStep('生成 Essay', async () => { + await page.getByRole('button', { name: 'icon Generate Essay' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + + console.log('等待 Essay 生成完成...'); + const loadingMessage = page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + + // 第一步:先等待加载消息出现(确保生成已开始) + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + + // 第二步:等待加载消息消失(最多等待 3 分钟) + await loadingMessage.waitFor({ state: 'hidden', timeout: 180000 }); + console.log('Essay 生成完成,加载消息已消失'); + + // 第三步:等待 10 秒钟让页面稳定 + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤5: 各种检查功能 + await executeStep('语法检查 (Grammar Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).first().click(); + await page.getByRole('button', { name: 'Start Grammar Check' }).click(); + // 语法检查需要时间 + console.log('等待 Start Grammar Check 加载完成...'); + await expect(page.getByRole('heading', { name: 'suggestions' })).toBeVisible({ timeout: 120000 }); + console.log('suggestions 已出现'); + await expect(page.getByLabel('suggestions')).toBeVisible({ timeout: 30000 }); + }); + + await executeStep('查重检查 (Plagiarism Check)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(1).click(); + await page.getByRole('button', { name: 'Start Plagiarism Check' }).click(); + console.log('等待 Start Plagiarism Check 加载完成...'); + // 查重检查需要较长时间 + await expect(page.getByRole('button', { name: 'Re-check' })).toBeVisible({ timeout: 200000 }); + console.log('Re-check 已出现'); + }); + + await executeStep('AI检测 (AI Detection)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(2).click(); + await page.getByRole('button', { name: 'Start AI Detection' }).click(); + console.log('等待 Start AI Detection 加载完成...'); + await expect(page.getByRole('heading', { name: 'GPTZero Premium' })).toBeVisible({ timeout: 80000 }); + console.log('GPTZero Premium 已出现'); + }); + + await executeStep('人性化处理 (Humanize)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(3).click(); + await page.getByRole('button', { name: 'Start Humanize' }).click(); + console.log('等待 Start Humanize 加载完成...'); + await expect(page.getByText('Accept all')).toBeVisible({ timeout: 110000 }); + console.log('Accept all 已出现'); + }); + + await executeStep('润色 (Polish)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(4).click(); + await page.getByRole('button', { name: 'Start Polish' }).click(); + console.log('等待 Start Polish 加载完成...'); + await expect(page.getByText('All Suggestions')).toBeVisible({ timeout: 110000 }); + console.log('All Suggestions 已出现'); + }); + + await executeStep('评分 (Get Rated)', async () => { + await page.getByRole('img', { name: 'trigger' }).nth(5).click(); + await page.getByRole('button', { name: 'Get Rated' }).click(); + console.log('等待 Get Rated 加载完成...'); + await expect(page.getByRole('heading', { name: 'Your essay rating is:' })).toBeVisible({ timeout: 120000 }); + console.log('Your essay rating is: 已出现'); + }); + + await executeStep('Improvement 检查', async () => { + await page.getByRole('tab', { name: 'Improvement' }).click(); + console.log('等待 Improvement 加载完成...'); + await expect(page.getByText('Accept all')).toBeVisible({ timeout: 110000 }); + console.log('Accept all 已出现)'); + }); + + // 步骤6: 进入 Essay Writing + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByRole('button', { name: 'icon Explore Essay Idea' })).toBeVisible({ timeout: 15000 }); + }); + + // 步骤7: 探索 Essay Idea + await executeStep('探索 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Explore Essay Idea' }).click(); + await expect(page.getByRole('heading', { name: 'Prompt Answer Guide' })).toBeVisible({ timeout: 15000 }); + }); + + // 步骤8: 加载 Recommendation + await executeStep('加载 Recommendation', async () => { + await page.getByRole('button', { name: 'icon Recommendation' }).click(); + console.log('等待 Recommendation 加载完成...'); + await page.waitForTimeout(10000); + console.log('Recommendation 加载完成'); + }); + + // 步骤9: 生成 Essay Idea + await executeStep('生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'icon Generate Essay Idea' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Essay Idea 生成...'); + await expect(page.getByRole('button', { name: 'Student Guide' })).toBeVisible({ timeout: 60000 }); + console.log('Essay Idea 生成完成'); + }); + + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + + // 步骤10: 测试 Student Guide 功能 + await executeStep('测试 Student Guide 展开/收起', async () => { + // 展开 Student Guide + await page.getByRole('button', { name: 'Student Guide' }).click(); + console.log('等待 Student Guide 加载完成...'); + await expect(page.getByText('**')).toBeVisible({ timeout: 110000 }); + // 测试复制功能 + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 110000 }); + await page.getByLabel('Student Guide').getByRole('button').filter({ hasText: /^$/ }).click(); + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤11: 测试翻译功能 + await executeStep('测试翻译功能', async () => { + // 点击翻译按钮,英文 → 中文 + await page.getByRole('button', { name: 'Translate Translate' }).click(); + console.log('点击翻译按钮,等待翻译完成...'); + + // 等待翻译完成(等待按钮重新变为可用状态或等待固定时间) + await page.waitForTimeout(3000); + + // 验证翻译按钮仍然存在(表示功能可用) + await expect(page.getByRole('button', { name: 'Translate Translate' })).toBeVisible({ timeout: 5000 }); + console.log('翻译功能正常(已切换为中文)'); + }); + + // 步骤12: 测试复制功能 + await executeStep('测试复制到剪贴板', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 5000 }); + console.log('复制功能正常'); + console.log('等待 10 秒让页面稳定...'); + await page.waitForTimeout(10000); + console.log('页面已稳定,继续下一步'); + }); + + // 步骤13: 测试再次翻译 + await executeStep('测试翻译返回英文', async () => { + await page.getByRole('button', { name: 'Translate Translate' }).click(); + await page.waitForTimeout(2000); + console.log('翻译返回英文完成'); + }); + + // 步骤14: 再次测试复制 + await executeStep('再次测试复制功能', async () => { + await page.getByRole('button', { name: 'Copy' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 5000 }); + }); + + // 步骤15: 测试重新生成 Essay Idea + await executeStep('测试重新生成 Essay Idea', async () => { + await page.getByRole('button', { name: 'Regenerate' }).click(); + console.log('等待重新生成 Essay Idea...'); + await expect(page.getByRole('button', { name: 'Student Guide' })).toBeVisible({ timeout: 60000 }); + console.log('重新生成完成'); + }); + + // 步骤16: 生成 Outline + await executeStep('生成 Outline', async () => { + await page.getByRole('button', { name: 'icon Generate Outline First' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Outline 生成...'); + await expect(page.getByRole('heading', { name: 'Untitled Document' })).toBeVisible({ timeout: 60000 }); + console.log('Outline 生成完成'); + }); + + // 步骤17: 生成 Draft + await executeStep('生成 Draft', async () => { + await page.getByRole('button', { name: 'icon Generate Draft' }).click(); + await page.getByRole('button', { name: 'Intermediate' }).click(); + await page.getByPlaceholder('Enter the expected length...').click(); + await page.getByPlaceholder('Enter the expected length...').fill('500'); + await page.getByRole('button', { name: 'Generate' }).click(); + console.log('等待 Draft 生成完成...'); + const loadingMessage = page.getByText(/正在生成文书,预计需要30秒,请耐心等待/i); + // 第一步:先等待加载消息出现(确保生成已开始) + console.log('等待加载消息出现...'); + await loadingMessage.waitFor({ state: 'visible', timeout: 10000 }); + console.log('加载消息已出现,文书正在生成中...'); + }); + // 步骤18: 进入 Essay Writing + await executeStep('进入 Essay Writing', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByText('Notes')).toBeVisible({ timeout: 15000 }); + }); + + // 步骤19: 点击 Essay Writing 确认界面 + await executeStep('确认 Essay Writing 界面', async () => { + await page.getByRole('button', { name: 'Essay Writing' }).click(); + await expect(page.getByRole('button', { name: 'Add Material' })).toBeVisible({ timeout: 10000 }); + console.log('Essay Writing 界面已加载'); + }); + + // 步骤20: 测试文件上传功能 + await executeStep('打开文件上传对话框', async () => { + await page.getByRole('button', { name: 'Add Material' }).click(); + await page.getByRole('menuitem', { name: 'Upload File' }).click(); + await expect(page.getByText('Click to upload or drag files')).toBeVisible({ timeout: 10000 }); + console.log('文件上传对话框已打开'); + }); + + await executeStep('上传文件', async () => { + // 检查文件是否存在 + const filePath = path.join(process.cwd(), 'math.docx'); + if (!fs.existsSync(filePath)) { + console.warn(`⚠️ 警告: 文件 math.docx 不存在,跳过上传测试`); + throw new Error(`文件不存在: ${filePath}`); + } + + console.log(`准备上传文件: ${filePath}`); + await page.locator('input[type="file"]').setInputFiles(filePath); + + // 等待上传状态显示 + console.log('等待文件上传...'); + await page.waitForTimeout(2000); + + // 点击上传按钮 + await page.getByRole('button', { name: 'Upload' }).click(); + console.log('已点击上传按钮'); + + // 等待上传完成 - 验证文件出现在材料列表中 + await expect(page.getByRole('button', { name: 'docx' }).first()).toBeVisible({ timeout: 30000 }); + console.log('文件上传成功'); + }); + + // 最后输出错误汇总 + console.log('\n\n========== 测试执行完成 =========='); + + // 生成错误报告文件 + const generateErrorReport = () => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(process.cwd(), `test-error-report-${timestamp}.txt`); + + let report = '========== 测试错误报告 ==========\n\n'; + report += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`; + + // 第一部分:登录账号信息(只显示一次) + report += '【登录账号信息】\n'; + report += `账号: ${loginInfo.email}\n`; + report += `密码: ${loginInfo.password}\n`; + report += `测试环境: https://pre.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账号: ${loginInfo.email}`); + console.log(`密码: ${loginInfo.password}\n`); + + // 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤 + } +}); diff --git a/运行测试.bat b/运行测试.bat new file mode 100644 index 0000000..d66ba3e --- /dev/null +++ b/运行测试.bat @@ -0,0 +1,85 @@ +@echo off +echo. +echo ================================ +echo E2E 测试运行工具 +echo ================================ +echo. + +:menu +echo. +echo 请选择测试: +echo. +echo [1] test-1.spec.ts +echo [2] 文书管理.spec.ts +echo [3] 文书管理-refactored.spec.ts (推荐) +echo [4] 运行所有测试 +echo [0] 退出 +echo. + +set /p choice=请输入选项: + +if "%choice%"=="0" goto end +if "%choice%"=="1" goto test1 +if "%choice%"=="2" goto test2 +if "%choice%"=="3" goto test3 +if "%choice%"=="4" goto testall + +echo 无效选项,请重试 +goto menu + +:test1 +echo. +echo 运行 test-1.spec.ts... +echo. +npx playwright test e2e/test-1.spec.ts +goto result + +:test2 +echo. +echo 运行 文书管理.spec.ts... +echo. +npx playwright test "e2e/文书管理.spec.ts" +goto result + +:test3 +echo. +echo 运行 文书管理-refactored.spec.ts... +echo. +npx playwright test "e2e/文书管理-refactored.spec.ts" +goto result + +:testall +echo. +echo 运行所有测试... +echo. +npx playwright test +goto result + +:result +echo. +echo ================================ +echo 测试完成 +echo ================================ +echo. + +REM 查找并显示错误报告 +for /f "delims=" %%f in ('dir /b /o-d test-error-report-*.txt 2^>nul') do ( + echo 找到错误报告: %%f + echo. + type "%%f" + goto :done_report +) +:done_report + +echo. +set /p view=是否查看HTML报告? (y/n): +if /i "%view%"=="y" npx playwright show-report + +echo. +set /p again=是否继续测试? (y/n): +if /i "%again%"=="y" goto menu + +:end +echo. +echo 感谢使用! +pause diff --git a/顾问管理测试.spec.ts b/顾问管理测试.spec.ts new file mode 100644 index 0000000..c673e5f --- /dev/null +++ b/顾问管理测试.spec.ts @@ -0,0 +1,472 @@ +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(600000); // 设置超时时间为10分钟 + + // 错误收集数组 + const errors: { step: string; error: string; timestamp: string; pageUrl: string }[] = []; + + // 管理员登录信息 + const adminInfo = { + email: 'xdf.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) => { + 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 => { + 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://pre.prodream.cn/en'); + await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible(); + }); + + 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://pre.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`); + } +}); diff --git a/顾问管理生产.spec.ts b/顾问管理生产.spec.ts new file mode 100644 index 0000000..fcd0598 --- /dev/null +++ b/顾问管理生产.spec.ts @@ -0,0 +1,476 @@ +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) => { + 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 => { + 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`); + } +});