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