first commit

This commit is contained in:
2025-11-04 16:29:07 +08:00
commit 8ee74625c7
19 changed files with 4562 additions and 0 deletions

361
ARCHITECTURE.md Normal file
View File

@ -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<void> {
// 实现页面操作
}
}
```
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<void>
- ✅ 使用描述性方法名
- ✅ 避免在 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. **健壮性**:完善的错误处理机制
通过这种架构,测试代码变得像文档一样易读,维护成本大大降低。

208
README.md Normal file
View File

@ -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<void> {
// 实现页面操作
}
}
```
### 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**: 运行环境

70
config/test.config.ts Normal file
View File

@ -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', // 报告文件前缀
},
};

58
pages/BasePage.ts Normal file
View File

@ -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<void> {
console.log(`等待 ${timeout / 1000} 秒让页面稳定...`);
await this.page.waitForTimeout(timeout);
console.log('页面已稳定,继续下一步');
}
/**
* 导航到指定URL
*/
async goto(url: string): Promise<void> {
await this.page.goto(url, { timeout: this.config.timeouts.navigation });
}
/**
* 点击元素
*/
async click(selector: string): Promise<void> {
await this.page.click(selector);
}
/**
* 填充输入框
*/
async fill(selector: string, text: string): Promise<void> {
await this.page.fill(selector, text);
}
/**
* 检查元素是否可见
*/
async isVisible(selector: string, timeout?: number): Promise<boolean> {
try {
await expect(this.page.locator(selector)).toBeVisible({ timeout });
return true;
} catch {
return false;
}
}
}

View File

@ -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<void> {
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<void> {
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 });
}
}

229
pages/EssayWritingPage.ts Normal file
View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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('加载消息已出现,文书正在生成中...');
}
}

50
pages/LoginPage.ts Normal file
View File

@ -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<void> {
await this.goto(this.config.env.baseUrl);
await expect(this.page.getByRole('button', { name: 'Log in' })).toBeVisible();
}
/**
* 点击登录按钮
*/
async clickLoginButton(): Promise<void> {
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<void> {
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<void> {
await this.visitHomePage();
await this.clickLoginButton();
await this.login(this.config.credentials.email, this.config.credentials.password);
}
}

64
pages/StudentPage.ts Normal file
View File

@ -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<void> {
await this.page.getByRole('link').filter({ hasText: '学生工作台' }).click();
await expect(this.page.getByText('Student Name')).toBeVisible();
}
/**
* 创建新学生
*/
async createNewStudent(studentData: StudentData): Promise<void> {
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();
}
}

11
test-1.spec.ts Normal file
View File

@ -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();
});

156
utils/error-handler.ts Normal file
View File

@ -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<void>,
page: Page
): Promise<void> {
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`);
}
}
}

View File

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

View File

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

View File

@ -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<void>) => {
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}`);
}
});

267
文书管理初版.spec.ts Normal file
View File

@ -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<void>) => {
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`);
// 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤
}
});

444
文书管理测试.spec.ts Normal file
View File

@ -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<void>) => {
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`);
// 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤
}
});

444
文书管理生产.spec.ts Normal file
View File

@ -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<void>) => {
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`);
// 不抛出错误,让测试标记为通过,但在日志中记录所有失败步骤
}
});

85
运行测试.bat Normal file
View File

@ -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

472
顾问管理测试.spec.ts Normal file
View File

@ -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<void>) => {
try {
console.log(`\n[执行] ${stepName}`);
await stepFunction();
console.log(`[成功] ${stepName}`);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : String(error);
const firstLine = errorMessage.split('\n')[0];
const cleanError = firstLine.replace(/\s+\(.*?\)/, '').trim();
const timestamp = new Date().toISOString();
const pageUrl = page.url();
errors.push({ step: stepName, error: cleanError, timestamp, pageUrl });
console.error(`[失败] ${stepName}: ${cleanError}`);
console.error(`[页面URL] ${pageUrl}`);
}
};
// 辅助函数通过IMAP读取最新邮件密码
const getPasswordFromEmail = (): Promise<string> => {
return new Promise((resolve, reject) => {
console.log('正在连接邮箱服务器...');
const imap = new Imap(imapConfig);
imap.once('ready', () => {
console.log('邮箱连接成功');
imap.openBox('INBOX', false, (err, box) => {
if (err) {
imap.end();
reject(err);
return;
}
console.log(`收件箱邮件数: ${box.messages.total}`);
// 搜索所有欢迎邮件
imap.search([['FROM', 'no-reply'], ['SUBJECT', 'Welcome to Prodream']], (err, results) => {
if (err) {
imap.end();
reject(err);
return;
}
if (results.length === 0) {
imap.end();
reject(new Error('未找到欢迎邮件'));
return;
}
console.log(`找到 ${results.length} 封相关邮件`);
// 收集所有邮件的日期和ID
const emailDates: { id: number; date: Date }[] = [];
let processedCount = 0;
const fetchHeaders = imap.fetch(results, { bodies: 'HEADER.FIELDS (DATE)' });
fetchHeaders.on('message', (msg, seqno) => {
msg.on('body', (stream: NodeJS.ReadableStream) => {
let buffer = '';
stream.on('data', (chunk: Buffer) => {
buffer += chunk.toString('utf8');
});
stream.once('end', () => {
// 从头部提取日期
const dateMatch = buffer.match(/Date:\s*(.+?)(?:\r?\n|$)/i);
if (dateMatch) {
try {
const dateString = dateMatch[1].trim();
const emailDate = new Date(dateString);
// 验证日期是否有效
if (!isNaN(emailDate.getTime())) {
emailDates.push({ id: seqno, date: emailDate });
console.log(`邮件 ${seqno} 的日期: ${emailDate.toISOString()}`);
} else {
console.log(`⚠️ 邮件 ${seqno} 日期无效: ${dateString}`);
}
} catch (e) {
console.log(`⚠️ 邮件 ${seqno} 日期解析失败`);
}
} else {
console.log(`⚠️ 邮件 ${seqno} 未找到日期头部`);
}
});
});
msg.once('end', () => {
processedCount++;
// 所有邮件头部都处理完后,选择最新的
if (processedCount === results.length) {
console.log(`\n处理完成共收集到 ${emailDates.length} 封邮件的日期信息`);
// 如果没有收集到任何日期信息使用最大的邮件ID通常是最新的
let latestEmail: { id: number; date: Date };
if (emailDates.length === 0) {
console.log('⚠️ 未能从邮件头部提取日期使用邮件ID排序');
const maxId = Math.max(...results);
latestEmail = { id: maxId, date: new Date() };
console.log(`选择邮件 ID: ${maxId}`);
} else {
// 按日期排序,最新的在最后
emailDates.sort((a, b) => a.date.getTime() - b.date.getTime());
latestEmail = emailDates[emailDates.length - 1];
console.log(`选择最新邮件: ID=${latestEmail.id}, 日期=${latestEmail.date.toISOString()}`);
}
// 获取最新邮件的完整内容
const fetchBody = imap.fetch([latestEmail.id], { bodies: '' });
fetchBody.on('message', (msg2) => {
msg2.on('body', (stream: any) => {
simpleParser(stream, (err: any, parsed: ParsedMail) => {
if (err) {
imap.end();
reject(err);
return;
}
console.log('邮件解析成功');
// 尝试从文本版本和 HTML 版本提取
const textBody = parsed.text || '';
const htmlBody = parsed.html || '';
console.log('=== 邮件文本内容 ===');
console.log(textBody);
console.log('=== 邮件HTML内容 ===');
console.log(htmlBody);
console.log('===================');
// 尝试多种密码提取模式
// 密码可以包含任何可打印字符,直到遇到空格、换行或特定的结束词
const patterns = [
/Password[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
/password[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
/密码[:\s]+(\S+?)(?:\s+登录|\s+$|\s+\n|$)/,
/Your password is[:\s]+(\S+?)(?:\s+Login|\s+$|\s+\n|$)/i,
/临时密码[:\s]+(\S+?)(?:\s+登录|\s+$|\s+\n|$)/,
// 备用模式:匹配到下一个单词边界
/Password[:\s]+([^\s]+)/i,
/password[:\s]+([^\s]+)/i
];
let password = '';
// 首先尝试从文本内容提取
for (const pattern of patterns) {
const match = textBody.match(pattern);
if (match && match[1]) {
password = match[1].trim();
console.log(`✅ 从文本中提取到密码 (模式: ${pattern}): ${password}`);
break;
}
}
// 如果文本提取失败,尝试从 HTML 提取
if (!password && htmlBody) {
// 移除 HTML 标签后再尝试
const cleanHtml = htmlBody.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ');
console.log('清理后的HTML内容:', cleanHtml.substring(0, 300));
for (const pattern of patterns) {
const match = cleanHtml.match(pattern);
if (match && match[1]) {
password = match[1].trim();
console.log(`✅ 从HTML中提取到密码 (模式: ${pattern}): ${password}`);
break;
}
}
}
if (password) {
console.log(`🎉 成功提取密码: ${password}`);
imap.end();
resolve(password);
} else {
console.error('❌ 所有模式都未能提取密码');
console.error('请检查上面的邮件内容,手动确认密码格式');
imap.end();
reject(new Error('无法从邮件中提取密码'));
}
});
});
});
fetchBody.once('error', (err: Error) => {
imap.end();
reject(err);
});
}
});
});
fetchHeaders.once('error', (err: Error) => {
imap.end();
reject(err);
});
});
});
});
imap.once('error', (err: Error) => {
console.error('IMAP连接错误:', err);
reject(err);
});
imap.once('end', () => {
console.log('邮箱连接已关闭');
});
imap.connect();
});
};
// 步骤1: 管理员登录
await executeStep('访问首页', async () => {
await page.goto('https://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`);
}
});

476
顾问管理生产.spec.ts Normal file
View File

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