Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
428
frontend-vben/apps/web-antd/tests/README.md
Normal file
428
frontend-vben/apps/web-antd/tests/README.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# 端到端测试指南
|
||||
|
||||
这是 Telegram 管理系统的完整端到端测试套件,使用 Playwright 进行自动化测试。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 Playwright 浏览器
|
||||
pnpm test:install
|
||||
|
||||
# 或手动安装
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pnpm test
|
||||
|
||||
# 运行特定测试文件
|
||||
pnpm test:auth # 登录认证测试
|
||||
pnpm test:account # 账号管理测试
|
||||
pnpm test:message # 私信群发测试
|
||||
pnpm test:marketing # 营销中心测试
|
||||
pnpm test:permission # 权限控制测试
|
||||
pnpm test:websocket # WebSocket实时通信测试
|
||||
pnpm test:responsive # 响应式布局测试
|
||||
|
||||
# 运行特定浏览器测试
|
||||
pnpm test:chrome # Chrome浏览器
|
||||
pnpm test:firefox # Firefox浏览器
|
||||
pnpm test:safari # Safari浏览器
|
||||
pnpm test:mobile # 移动端测试
|
||||
|
||||
# 有头模式运行(显示浏览器界面)
|
||||
pnpm test:headed
|
||||
|
||||
# 使用测试UI界面
|
||||
pnpm test:ui
|
||||
|
||||
# 查看测试报告
|
||||
pnpm test:report
|
||||
```
|
||||
|
||||
## 📁 测试文件结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/ # 端到端测试
|
||||
│ ├── auth.test.ts # 登录认证流程测试
|
||||
│ ├── account-management.test.ts # 账号管理功能测试
|
||||
│ ├── private-message.test.ts # 私信群发功能测试
|
||||
│ ├── marketing-center.test.ts # 营销中心功能测试
|
||||
│ ├── permission-control.test.ts # 权限控制功能测试
|
||||
│ ├── websocket-realtime.test.ts # WebSocket实时通信测试
|
||||
│ └── responsive-layout.test.ts # 响应式布局测试
|
||||
├── global-setup.ts # 全局测试设置
|
||||
├── global-teardown.ts # 全局测试清理
|
||||
└── README.md # 测试指南
|
||||
```
|
||||
|
||||
## 🧪 测试覆盖范围
|
||||
|
||||
### 1. 登录认证测试 (`auth.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 登录页面显示
|
||||
- ✅ 表单验证
|
||||
- ✅ 登录流程
|
||||
- ✅ 记住密码功能
|
||||
- ✅ 语言切换
|
||||
- ✅ Token过期处理
|
||||
- ✅ 退出登录
|
||||
- ✅ 页面刷新状态保持
|
||||
- ✅ 多标签页同步
|
||||
- ✅ 权限角色测试
|
||||
- ✅ 国际化功能
|
||||
|
||||
**测试账号**:
|
||||
|
||||
```javascript
|
||||
// 管理员账号
|
||||
admin / 111111;
|
||||
|
||||
// 超级管理员
|
||||
super_admin / super123456;
|
||||
|
||||
// 普通用户
|
||||
test_user / test123456;
|
||||
```
|
||||
|
||||
### 2. 账号管理测试 (`account-management.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 账号列表显示
|
||||
- ✅ 搜索筛选功能
|
||||
- ✅ 创建新账号
|
||||
- ✅ 表单验证
|
||||
- ✅ 编辑账号信息
|
||||
- ✅ 查看账号详情
|
||||
- ✅ 账号状态切换
|
||||
- ✅ 批量操作
|
||||
- ✅ 导出功能
|
||||
- ✅ 分页功能
|
||||
- ✅ 表格排序
|
||||
- ✅ 删除账号
|
||||
- ✅ 账号导入功能
|
||||
|
||||
### 3. 私信群发测试 (`private-message.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 消息模板管理
|
||||
- ✅ 创建消息模板
|
||||
- ✅ 模板预览功能
|
||||
- ✅ 编辑模板
|
||||
- ✅ 变量替换功能
|
||||
- ✅ 群发任务管理
|
||||
- ✅ 创建群发任务
|
||||
- ✅ 任务状态管理
|
||||
- ✅ 任务详情页面
|
||||
- ✅ 任务暂停恢复
|
||||
- ✅ 发送记录查看
|
||||
- ✅ 记录搜索筛选
|
||||
- ✅ 记录详情查看
|
||||
- ✅ 记录导出
|
||||
- ✅ 统计图表
|
||||
- ✅ 实时监控
|
||||
|
||||
### 4. 营销中心测试 (`marketing-center.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 智能活动管理
|
||||
- ✅ 创建营销活动
|
||||
- ✅ 活动状态管理
|
||||
- ✅ 活动详情页面
|
||||
- ✅ 智能推荐功能
|
||||
- ✅ 活动复制功能
|
||||
- ✅ 客户分析
|
||||
- ✅ 客户分群功能
|
||||
- ✅ 客户行为分析
|
||||
- ✅ 客户价值分析
|
||||
- ✅ 客户流失分析
|
||||
- ✅ 效果统计
|
||||
- ✅ 时间范围筛选
|
||||
- ✅ 渠道对比分析
|
||||
- ✅ 活动效果对比
|
||||
- ✅ 转化漏斗分析
|
||||
- ✅ ROI分析
|
||||
- ✅ 报告导出
|
||||
|
||||
### 5. 权限控制测试 (`permission-control.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 权限管理页面
|
||||
- ✅ 创建新权限
|
||||
- ✅ 权限树展开折叠
|
||||
- ✅ 权限详情查看
|
||||
- ✅ 编辑权限信息
|
||||
- ✅ 权限搜索功能
|
||||
- ✅ 删除权限
|
||||
- ✅ 角色管理页面
|
||||
- ✅ 创建新角色
|
||||
- ✅ 角色权限分配
|
||||
- ✅ 角色状态切换
|
||||
- ✅ 角色复制功能
|
||||
- ✅ 删除角色
|
||||
- ✅ 路由权限验证
|
||||
- ✅ 菜单权限过滤
|
||||
- ✅ 按钮权限控制
|
||||
- ✅ API权限验证
|
||||
- ✅ 权限缓存更新
|
||||
- ✅ v-permission指令
|
||||
|
||||
### 6. WebSocket实时通信测试 (`websocket-realtime.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ WebSocket连接建立
|
||||
- ✅ 断线重连机制
|
||||
- ✅ 手动重连功能
|
||||
- ✅ 连接心跳机制
|
||||
- ✅ 系统通知接收
|
||||
- ✅ 消息状态更新
|
||||
- ✅ 用户在线状态
|
||||
- ✅ 任务进度更新
|
||||
- ✅ 实时性能指标
|
||||
- ✅ 实时日志显示
|
||||
- ✅ 告警信息推送
|
||||
- ✅ 实时图表更新
|
||||
- ✅ 多标签页同步
|
||||
|
||||
### 7. 响应式布局测试 (`responsive-layout.test.ts`)
|
||||
|
||||
**测试功能**:
|
||||
|
||||
- ✅ 桌面端布局
|
||||
- ✅ 侧边栏折叠展开
|
||||
- ✅ 桌面端表格显示
|
||||
- ✅ 平板端布局适配
|
||||
- ✅ 平板端表格优化
|
||||
- ✅ 平板端搜索表单
|
||||
- ✅ 手机端移动导航
|
||||
- ✅ 手机端侧边栏抽屉
|
||||
- ✅ 手机端卡片布局
|
||||
- ✅ 手机端搜索折叠
|
||||
- ✅ 手机端表单适配
|
||||
- ✅ 大屏幕内容显示
|
||||
- ✅ 大屏幕表格列数
|
||||
- ✅ 大屏幕仪表盘
|
||||
- ✅ 响应式断点测试
|
||||
- ✅ 触摸设备交互
|
||||
- ✅ 触摸滑动功能
|
||||
- ✅ 触摸点击区域
|
||||
- ✅ 长按菜单功能
|
||||
|
||||
## 🔧 测试配置
|
||||
|
||||
### Playwright 配置 (`playwright.config.ts`)
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
// 测试报告
|
||||
reporter: [
|
||||
['html', { outputFolder: 'test-results/html-report' }],
|
||||
['json', { outputFile: 'test-results/results.json' }],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['line'],
|
||||
],
|
||||
|
||||
// 支持的浏览器
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
|
||||
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
|
||||
{ name: 'Microsoft Edge', use: { ...devices['Desktop Edge'] } },
|
||||
],
|
||||
|
||||
// 本地开发服务器
|
||||
webServer: {
|
||||
command: 'pnpm dev',
|
||||
port: 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 测试环境变量
|
||||
|
||||
```bash
|
||||
# .env.test
|
||||
NODE_ENV=test
|
||||
VITE_API_URL=http://localhost:3001/api
|
||||
VITE_WS_URL=ws://localhost:3001/ws
|
||||
```
|
||||
|
||||
## 📊 测试报告
|
||||
|
||||
测试完成后,报告文件位于:
|
||||
|
||||
```
|
||||
test-results/
|
||||
├── html-report/ # HTML格式测试报告
|
||||
├── results.json # JSON格式测试结果
|
||||
├── results.xml # JUnit格式测试结果
|
||||
├── test-summary.json # 测试摘要
|
||||
└── artifacts/ # 测试截图和视频
|
||||
```
|
||||
|
||||
### 查看测试报告
|
||||
|
||||
```bash
|
||||
# 查看HTML报告
|
||||
pnpm test:report
|
||||
|
||||
# 或直接打开
|
||||
open test-results/html-report/index.html
|
||||
```
|
||||
|
||||
## 🐛 调试测试
|
||||
|
||||
### 调试模式
|
||||
|
||||
```bash
|
||||
# 有头模式运行(显示浏览器)
|
||||
pnpm test:headed
|
||||
|
||||
# 使用调试器
|
||||
pnpm test --debug
|
||||
|
||||
# 运行特定测试并暂停
|
||||
npx playwright test --headed --debug tests/e2e/auth.test.ts
|
||||
```
|
||||
|
||||
### 生成测试代码
|
||||
|
||||
```bash
|
||||
# 使用录制器生成测试代码
|
||||
npx playwright codegen http://localhost:5173
|
||||
```
|
||||
|
||||
### 查看测试追踪
|
||||
|
||||
```bash
|
||||
# 生成追踪文件
|
||||
pnpm test --trace on
|
||||
|
||||
# 查看追踪
|
||||
npx playwright show-trace test-results/trace.zip
|
||||
```
|
||||
|
||||
## 📝 编写测试
|
||||
|
||||
### 基本测试结构
|
||||
|
||||
```typescript
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
test.describe('功能模块测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 测试前置条件
|
||||
await page.goto('http://localhost:5173');
|
||||
await login(page);
|
||||
});
|
||||
|
||||
test('应该正确显示页面', async ({ page }) => {
|
||||
// 测试步骤
|
||||
await page.click('[data-testid="button"]');
|
||||
|
||||
// 断言
|
||||
await expect(page.locator('.result')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用 data-testid 属性**
|
||||
|
||||
```html
|
||||
<button data-testid="submit-button">提交</button>
|
||||
```
|
||||
|
||||
2. **等待异步操作完成**
|
||||
|
||||
```typescript
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('[data-testid="content"]');
|
||||
```
|
||||
|
||||
3. **使用页面对象模式**
|
||||
```typescript
|
||||
class LoginPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
await this.page.fill('[data-testid="username"]', username);
|
||||
await this.page.fill('[data-testid="password"]', password);
|
||||
await this.page.click('[data-testid="login-button"]');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 CI/CD 集成
|
||||
|
||||
### GitHub Actions 示例
|
||||
|
||||
```yaml
|
||||
name: E2E Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright
|
||||
run: pnpm test:install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
```
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题,请:
|
||||
|
||||
1. 查看 [Playwright 官方文档](https://playwright.dev/)
|
||||
2. 检查测试日志和错误信息
|
||||
3. 联系开发团队
|
||||
|
||||
## 📄 更新日志
|
||||
|
||||
### v1.0.0
|
||||
|
||||
- 初始版本发布
|
||||
- 支持所有主要功能模块测试
|
||||
- 多浏览器和移动端支持
|
||||
- 完整的CI/CD集成
|
||||
431
frontend-vben/apps/web-antd/tests/e2e/account-management.test.ts
Normal file
431
frontend-vben/apps/web-antd/tests/e2e/account-management.test.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 账号管理功能端到端测试
|
||||
* 测试账号列表、创建、编辑、删除等功能
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
adminUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
};
|
||||
|
||||
// 测试账号数据
|
||||
const TEST_ACCOUNT = {
|
||||
username: 'test_telegram_' + Date.now(),
|
||||
nickname: '测试账号',
|
||||
phone: '13800138000',
|
||||
email: 'test@example.com',
|
||||
status: 'active',
|
||||
remark: '这是一个测试账号',
|
||||
};
|
||||
|
||||
test.describe('账号管理功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
// 登录系统
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到账号管理页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
});
|
||||
|
||||
test('应该正确显示账号列表页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/账号列表|Telegram账号管理/,
|
||||
);
|
||||
|
||||
// 检查搜索表单
|
||||
await expect(page.locator('[data-testid="search-username"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-status"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-button"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="reset-button"]')).toBeVisible();
|
||||
|
||||
// 检查操作按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="add-account-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="batch-delete-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="export-button"]')).toBeVisible();
|
||||
|
||||
// 检查表格
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
await expect(page.locator('.ant-table-thead')).toBeVisible();
|
||||
});
|
||||
|
||||
test('搜索功能应该正常工作', async ({ page }) => {
|
||||
// 输入搜索条件
|
||||
await page.fill('[data-testid="search-username"]', 'admin');
|
||||
await page.selectOption('[data-testid="search-status"]', 'active');
|
||||
|
||||
// 点击搜索
|
||||
await page.click('[data-testid="search-button"]');
|
||||
|
||||
// 等待搜索结果加载
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否显示了搜索结果
|
||||
await expect(page.locator('.ant-table-tbody tr')).toHaveCount({ min: 1 });
|
||||
|
||||
// 检查搜索结果是否包含关键词
|
||||
const firstRowUsername = await page
|
||||
.locator('.ant-table-tbody tr:first-child td:nth-child(2)')
|
||||
.textContent();
|
||||
expect(firstRowUsername).toContain('admin');
|
||||
|
||||
// 测试重置功能
|
||||
await page.click('[data-testid="reset-button"]');
|
||||
await expect(page.locator('[data-testid="search-username"]')).toHaveValue(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
test('应该能创建新账号', async ({ page }) => {
|
||||
// 点击添加账号按钮
|
||||
await page.click('[data-testid="add-account-button"]');
|
||||
|
||||
// 等待弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/添加账号|新增Telegram账号/,
|
||||
);
|
||||
|
||||
// 填写表单数据
|
||||
await page.fill('[data-testid="form-username"]', TEST_ACCOUNT.username);
|
||||
await page.fill('[data-testid="form-nickname"]', TEST_ACCOUNT.nickname);
|
||||
await page.fill('[data-testid="form-phone"]', TEST_ACCOUNT.phone);
|
||||
await page.fill('[data-testid="form-email"]', TEST_ACCOUNT.email);
|
||||
await page.selectOption('[data-testid="form-status"]', TEST_ACCOUNT.status);
|
||||
await page.fill('[data-testid="form-remark"]', TEST_ACCOUNT.remark);
|
||||
|
||||
// 提交表单
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/创建成功|添加成功/,
|
||||
);
|
||||
|
||||
// 弹窗应该关闭
|
||||
await expect(page.locator('.ant-modal')).not.toBeVisible();
|
||||
|
||||
// 在列表中应该能找到新创建的账号
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const foundAccount = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username });
|
||||
await expect(foundAccount).toBeVisible();
|
||||
});
|
||||
|
||||
test('表单验证应该正常工作', async ({ page }) => {
|
||||
await page.click('[data-testid="add-account-button"]');
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
|
||||
// 不填写必填字段直接提交
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 应该显示验证错误
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText(
|
||||
/请输入用户名|用户名不能为空/,
|
||||
);
|
||||
|
||||
// 输入无效的邮箱格式
|
||||
await page.fill('[data-testid="form-username"]', 'test_user');
|
||||
await page.fill('[data-testid="form-email"]', 'invalid-email');
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 应该显示邮箱格式错误
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText(
|
||||
/邮箱格式不正确|请输入有效的邮箱地址/,
|
||||
);
|
||||
|
||||
// 关闭弹窗
|
||||
await page.click('.ant-modal-close');
|
||||
});
|
||||
|
||||
test('应该能编辑账号信息', async ({ page }) => {
|
||||
// 先搜索刚才创建的账号
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击编辑按钮
|
||||
const editButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username })
|
||||
.locator('[data-testid="edit-button"]');
|
||||
await editButton.click();
|
||||
|
||||
// 等待编辑弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/编辑账号|修改账号信息/,
|
||||
);
|
||||
|
||||
// 修改昵称
|
||||
const newNickname = '已修改的测试账号';
|
||||
await page.fill('[data-testid="form-nickname"]', newNickname);
|
||||
|
||||
// 提交修改
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/修改成功|更新成功/,
|
||||
);
|
||||
|
||||
// 验证修改是否生效
|
||||
await page.reload();
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const updatedRow = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username });
|
||||
await expect(updatedRow).toContainText(newNickname);
|
||||
});
|
||||
|
||||
test('应该能查看账号详情', async ({ page }) => {
|
||||
// 搜索账号
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击查看按钮
|
||||
const viewButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username })
|
||||
.locator('[data-testid="view-button"]');
|
||||
await viewButton.click();
|
||||
|
||||
// 等待详情弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/账号详情|查看账号信息/,
|
||||
);
|
||||
|
||||
// 检查详情信息是否正确显示
|
||||
await expect(page.locator('[data-testid="detail-username"]')).toContainText(
|
||||
TEST_ACCOUNT.username,
|
||||
);
|
||||
await expect(page.locator('[data-testid="detail-phone"]')).toContainText(
|
||||
TEST_ACCOUNT.phone,
|
||||
);
|
||||
await expect(page.locator('[data-testid="detail-email"]')).toContainText(
|
||||
TEST_ACCOUNT.email,
|
||||
);
|
||||
|
||||
// 关闭详情弹窗
|
||||
await page.click('.ant-modal-close');
|
||||
});
|
||||
|
||||
test('账号状态切换应该正常工作', async ({ page }) => {
|
||||
// 搜索账号
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const accountRow = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username });
|
||||
|
||||
// 点击状态切换按钮
|
||||
const statusSwitch = accountRow.locator('[data-testid="status-switch"]');
|
||||
await statusSwitch.click();
|
||||
|
||||
// 确认状态变更
|
||||
await page.click('[data-testid="confirm-status-change"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/状态更新成功|操作成功/,
|
||||
);
|
||||
});
|
||||
|
||||
test('批量操作应该正常工作', async ({ page }) => {
|
||||
// 选择多个账号
|
||||
const checkboxes = page.locator('.ant-table-tbody .ant-checkbox-input');
|
||||
await checkboxes.first().check();
|
||||
await checkboxes.nth(1).check();
|
||||
|
||||
// 批量删除按钮应该变为可用状态
|
||||
await expect(
|
||||
page.locator('[data-testid="batch-delete-button"]'),
|
||||
).not.toBeDisabled();
|
||||
|
||||
// 点击批量删除
|
||||
await page.click('[data-testid="batch-delete-button"]');
|
||||
|
||||
// 确认删除操作
|
||||
await expect(page.locator('.ant-modal-confirm')).toBeVisible();
|
||||
await page.click('.ant-btn-primary');
|
||||
|
||||
// 等待操作完成
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('导出功能应该正常工作', async ({ page }) => {
|
||||
// 设置下载事件监听
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// 点击导出按钮
|
||||
await page.click('[data-testid="export-button"]');
|
||||
|
||||
// 等待下载开始
|
||||
const download = await downloadPromise;
|
||||
|
||||
// 验证下载文件名
|
||||
expect(download.suggestedFilename()).toMatch(/账号列表.*\.xlsx$/);
|
||||
});
|
||||
|
||||
test('分页功能应该正常工作', async ({ page }) => {
|
||||
// 检查分页组件是否存在
|
||||
await expect(page.locator('.ant-pagination')).toBeVisible();
|
||||
|
||||
// 如果有多页数据,测试分页切换
|
||||
const nextButton = page.locator('.ant-pagination-next');
|
||||
const isNextEnabled = await nextButton.isEnabled();
|
||||
|
||||
if (isNextEnabled) {
|
||||
// 点击下一页
|
||||
await nextButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查页码是否改变
|
||||
await expect(page.locator('.ant-pagination-item-active')).toContainText(
|
||||
'2',
|
||||
);
|
||||
|
||||
// 点击上一页
|
||||
await page.click('.ant-pagination-prev');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否回到第一页
|
||||
await expect(page.locator('.ant-pagination-item-active')).toContainText(
|
||||
'1',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('表格排序功能应该正常工作', async ({ page }) => {
|
||||
// 点击用户名列的排序按钮
|
||||
const usernameHeader = page
|
||||
.locator('.ant-table-thead th')
|
||||
.filter({ hasText: /用户名|Username/ });
|
||||
await usernameHeader.locator('.ant-table-column-sorter').click();
|
||||
|
||||
// 等待排序完成
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否有排序指示器
|
||||
await expect(
|
||||
usernameHeader.locator(
|
||||
'.ant-table-column-sorter-up.active, .ant-table-column-sorter-down.active',
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能删除账号', async ({ page }) => {
|
||||
// 搜索要删除的测试账号
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击删除按钮
|
||||
const deleteButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ACCOUNT.username })
|
||||
.locator('[data-testid="delete-button"]');
|
||||
await deleteButton.click();
|
||||
|
||||
// 确认删除
|
||||
await expect(page.locator('.ant-modal-confirm')).toBeVisible();
|
||||
await page.click('.ant-btn-primary');
|
||||
|
||||
// 等待删除成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/删除成功|操作成功/,
|
||||
);
|
||||
|
||||
// 验证账号已被删除
|
||||
await page.reload();
|
||||
await page.fill('[data-testid="search-username"]', TEST_ACCOUNT.username);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('.ant-empty')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('账号导入功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到账号导入页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-import"]');
|
||||
await page.waitForURL(/\/account\/import/);
|
||||
});
|
||||
|
||||
test('应该正确显示导入页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/账号导入|批量导入/,
|
||||
);
|
||||
|
||||
// 检查上传组件
|
||||
await expect(page.locator('.ant-upload-dragger')).toBeVisible();
|
||||
|
||||
// 检查模板下载链接
|
||||
await expect(
|
||||
page.locator('[data-testid="download-template"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查导入记录表格
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能下载导入模板', async ({ page }) => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// 点击下载模板
|
||||
await page.click('[data-testid="download-template"]');
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/账号导入模板.*\.xlsx$/);
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:管理员登录
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.adminUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.adminUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
357
frontend-vben/apps/web-antd/tests/e2e/auth.test.ts
Normal file
357
frontend-vben/apps/web-antd/tests/e2e/auth.test.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 登录和认证流程端到端测试
|
||||
* 使用 Playwright 进行自动化测试
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
// 测试配置
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
// 测试账号信息
|
||||
testUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
// 超级管理员账号
|
||||
superAdmin: {
|
||||
username: 'super_admin',
|
||||
password: 'super123456',
|
||||
},
|
||||
// 普通用户账号
|
||||
normalUser: {
|
||||
username: 'test_user',
|
||||
password: 'test123456',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('登录和认证流程测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 设置较长的超时时间
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
// 访问登录页面
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('应该正确显示登录页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page).toHaveTitle(/Telegram Management System|登录/);
|
||||
|
||||
// 检查登录表单元素是否存在
|
||||
await expect(page.locator('[data-testid="username-input"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="password-input"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="login-button"]')).toBeVisible();
|
||||
|
||||
// 检查记住密码复选框
|
||||
await expect(page.locator('[data-testid="remember-me"]')).toBeVisible();
|
||||
|
||||
// 检查语言切换按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="language-switcher"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('空用户名密码应该显示验证错误', async ({ page }) => {
|
||||
// 直接点击登录按钮
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// 等待验证错误消息出现
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText(
|
||||
'请输入用户名',
|
||||
);
|
||||
});
|
||||
|
||||
test('错误的用户名密码应该显示错误信息', async ({ page }) => {
|
||||
// 输入错误的用户名和密码
|
||||
await page.fill('[data-testid="username-input"]', 'wrong_user');
|
||||
await page.fill('[data-testid="password-input"]', 'wrong_password');
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// 等待错误消息出现
|
||||
await expect(page.locator('.ant-message-error')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-error')).toContainText(
|
||||
/用户名或密码错误|登录失败/,
|
||||
);
|
||||
});
|
||||
|
||||
test('正确的用户名密码应该成功登录', async ({ page }) => {
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
|
||||
// 验证登录成功后跳转到首页
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
// 检查用户信息是否正确显示
|
||||
await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="username-display"]'),
|
||||
).toContainText(TEST_CONFIG.testUser.username);
|
||||
});
|
||||
|
||||
test('记住密码功能应该正常工作', async ({ page }) => {
|
||||
// 输入用户名密码
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.testUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.testUser.password,
|
||||
);
|
||||
|
||||
// 勾选记住密码
|
||||
await page.check('[data-testid="remember-me"]');
|
||||
|
||||
// 登录
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// 等待登录完成
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
|
||||
// 退出登录
|
||||
await page.click('[data-testid="user-dropdown"]');
|
||||
await page.click('[data-testid="logout-button"]');
|
||||
|
||||
// 回到登录页面,检查是否记住了用户名
|
||||
await page.waitForURL(/\/login/);
|
||||
await expect(page.locator('[data-testid="username-input"]')).toHaveValue(
|
||||
TEST_CONFIG.testUser.username,
|
||||
);
|
||||
});
|
||||
|
||||
test('语言切换功能应该正常工作', async ({ page }) => {
|
||||
// 点击语言切换按钮
|
||||
await page.click('[data-testid="language-switcher"]');
|
||||
|
||||
// 选择英文
|
||||
await page.click('[data-testid="lang-en"]');
|
||||
|
||||
// 检查页面是否切换到英文
|
||||
await expect(page.locator('[data-testid="login-title"]')).toContainText(
|
||||
/Login|Sign In/,
|
||||
);
|
||||
|
||||
// 切换回中文
|
||||
await page.click('[data-testid="language-switcher"]');
|
||||
await page.click('[data-testid="lang-zh"]');
|
||||
|
||||
// 检查页面是否切换回中文
|
||||
await expect(page.locator('[data-testid="login-title"]')).toContainText(
|
||||
/登录|用户登录/,
|
||||
);
|
||||
});
|
||||
|
||||
test('Token过期应该自动跳转到登录页', async ({ page }) => {
|
||||
// 先正常登录
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
// 模拟Token过期 - 清除localStorage中的token
|
||||
await page.evaluate(() => {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
});
|
||||
|
||||
// 尝试访问需要认证的页面
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/account/list`);
|
||||
|
||||
// 应该自动跳转到登录页
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// 应该显示登录过期提示
|
||||
await expect(page.locator('.ant-message-warning')).toContainText(
|
||||
/登录过期|请重新登录/,
|
||||
);
|
||||
});
|
||||
|
||||
test('退出登录应该清除认证状态', async ({ page }) => {
|
||||
// 先登录
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
// 退出登录
|
||||
await page.click('[data-testid="user-dropdown"]');
|
||||
await page.click('[data-testid="logout-button"]');
|
||||
|
||||
// 应该跳转到登录页
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// 验证Token已被清除
|
||||
const accessToken = await page.evaluate(() =>
|
||||
localStorage.getItem('access_token'),
|
||||
);
|
||||
expect(accessToken).toBeNull();
|
||||
|
||||
// 尝试直接访问受保护页面应该被重定向
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/account/list`);
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('刷新页面应该保持登录状态', async ({ page }) => {
|
||||
// 登录
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
|
||||
// 应该仍然保持登录状态
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('多标签页登录状态应该同步', async ({ context }) => {
|
||||
const page1 = await context.newPage();
|
||||
const page2 = await context.newPage();
|
||||
|
||||
// 在第一个标签页登录
|
||||
await page1.goto(TEST_CONFIG.baseURL);
|
||||
await loginWithCredentials(page1, TEST_CONFIG.testUser);
|
||||
await expect(page1).toHaveURL(/\/dashboard/);
|
||||
|
||||
// 在第二个标签页访问应用
|
||||
await page2.goto(TEST_CONFIG.baseURL);
|
||||
|
||||
// 第二个标签页应该也是登录状态
|
||||
await expect(page2).toHaveURL(/\/dashboard/);
|
||||
await expect(page2.locator('[data-testid="user-avatar"]')).toBeVisible();
|
||||
|
||||
// 在第一个标签页退出登录
|
||||
await page1.click('[data-testid="user-dropdown"]');
|
||||
await page1.click('[data-testid="logout-button"]');
|
||||
|
||||
// 刷新第二个标签页,应该也被退出登录
|
||||
await page2.reload();
|
||||
await expect(page2).toHaveURL(/\/login/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('权限和角色测试', () => {
|
||||
test('超级管理员应该能访问所有页面', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginWithCredentials(page, TEST_CONFIG.superAdmin);
|
||||
|
||||
// 检查菜单项是否完整显示
|
||||
const menuItems = [
|
||||
'[data-testid="menu-dashboard"]',
|
||||
'[data-testid="menu-account"]',
|
||||
'[data-testid="menu-system"]',
|
||||
'[data-testid="menu-logs"]',
|
||||
'[data-testid="menu-marketing"]',
|
||||
];
|
||||
|
||||
for (const menuItem of menuItems) {
|
||||
await expect(page.locator(menuItem)).toBeVisible();
|
||||
}
|
||||
|
||||
// 测试访问系统管理页面
|
||||
await page.click('[data-testid="menu-system"]');
|
||||
await page.click('[data-testid="menu-permission-management"]');
|
||||
await expect(page).toHaveURL(/\/system\/permission/);
|
||||
});
|
||||
|
||||
test('普通用户应该只能访问授权页面', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginWithCredentials(page, TEST_CONFIG.normalUser);
|
||||
|
||||
// 检查某些管理功能不可见
|
||||
await expect(page.locator('[data-testid="menu-system"]')).not.toBeVisible();
|
||||
|
||||
// 尝试直接访问受限页面应该被拒绝
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/system/permission`);
|
||||
await expect(page.locator('.ant-result-403')).toBeVisible();
|
||||
await expect(page.locator('.ant-result-title')).toContainText('403');
|
||||
});
|
||||
|
||||
test('按钮级权限控制应该正常工作', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginWithCredentials(page, TEST_CONFIG.normalUser);
|
||||
|
||||
// 访问账号列表页面
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/account/list`);
|
||||
|
||||
// 检查删除按钮是否被隐藏(假设普通用户没有删除权限)
|
||||
await expect(
|
||||
page.locator('[data-testid="delete-button"]'),
|
||||
).not.toBeVisible();
|
||||
|
||||
// 但查看按钮应该可见
|
||||
await expect(page.locator('[data-testid="view-button"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('国际化功能测试', () => {
|
||||
test('语言切换应该影响整个应用', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
|
||||
// 切换到英文
|
||||
await page.click('[data-testid="language-switcher"]');
|
||||
await page.click('[data-testid="lang-en"]');
|
||||
|
||||
// 检查导航菜单是否切换到英文
|
||||
await expect(page.locator('[data-testid="menu-dashboard"]')).toContainText(
|
||||
/Dashboard/,
|
||||
);
|
||||
await expect(page.locator('[data-testid="menu-account"]')).toContainText(
|
||||
/Account/,
|
||||
);
|
||||
|
||||
// 切换回中文
|
||||
await page.click('[data-testid="language-switcher"]');
|
||||
await page.click('[data-testid="lang-zh"]');
|
||||
|
||||
// 检查是否切换回中文
|
||||
await expect(page.locator('[data-testid="menu-dashboard"]')).toContainText(
|
||||
/仪表盘|首页/,
|
||||
);
|
||||
await expect(page.locator('[data-testid="menu-account"]')).toContainText(
|
||||
/账号管理/,
|
||||
);
|
||||
});
|
||||
|
||||
test('语言偏好应该被持久化', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
|
||||
// 在登录页切换到英文
|
||||
await page.click('[data-testid="language-switcher"]');
|
||||
await page.click('[data-testid="lang-en"]');
|
||||
|
||||
// 登录
|
||||
await loginWithCredentials(page, TEST_CONFIG.testUser);
|
||||
|
||||
// 刷新页面,语言设置应该保持
|
||||
await page.reload();
|
||||
await expect(page.locator('[data-testid="menu-dashboard"]')).toContainText(
|
||||
/Dashboard/,
|
||||
);
|
||||
|
||||
// 退出重新登录,语言设置也应该保持
|
||||
await page.click('[data-testid="user-dropdown"]');
|
||||
await page.click('[data-testid="logout-button"]');
|
||||
|
||||
await page.waitForURL(/\/login/);
|
||||
await expect(page.locator('[data-testid="login-title"]')).toContainText(
|
||||
/Login|Sign In/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:使用指定凭据登录
|
||||
async function loginWithCredentials(
|
||||
page: Page,
|
||||
credentials: { username: string; password: string },
|
||||
) {
|
||||
await page.fill('[data-testid="username-input"]', credentials.username);
|
||||
await page.fill('[data-testid="password-input"]', credentials.password);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// 等待登录完成(等待跳转到仪表盘页面)
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
50
frontend-vben/apps/web-antd/tests/e2e/debug-login.test.ts
Normal file
50
frontend-vben/apps/web-antd/tests/e2e/debug-login.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('调试登录页面', async ({ page }) => {
|
||||
// 访问登录页面
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 截图查看页面内容
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/debug-login-page.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log('当前页面URL:', page.url());
|
||||
console.log('页面标题:', await page.title());
|
||||
|
||||
// 查找所有输入框
|
||||
const inputs = await page.locator('input').all();
|
||||
console.log(`找到 ${inputs.length} 个输入框`);
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i];
|
||||
const placeholder = await input.getAttribute('placeholder');
|
||||
const type = await input.getAttribute('type');
|
||||
const name = await input.getAttribute('name');
|
||||
console.log(
|
||||
`输入框 ${i + 1}: placeholder="${placeholder}", type="${type}", name="${name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// 查找登录相关的按钮
|
||||
const buttons = await page.locator('button').all();
|
||||
console.log(`找到 ${buttons.length} 个按钮`);
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const button = buttons[i];
|
||||
const text = await button.textContent();
|
||||
const type = await button.getAttribute('type');
|
||||
console.log(`按钮 ${i + 1}: text="${text}", type="${type}"`);
|
||||
}
|
||||
|
||||
// 检查页面内容
|
||||
const pageContent = await page.content();
|
||||
console.log(
|
||||
'页面是否包含登录相关内容:',
|
||||
pageContent.includes('登录') ||
|
||||
pageContent.includes('用户名') ||
|
||||
pageContent.includes('密码'),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('详细菜单检查', async ({ page }) => {
|
||||
// 设置视口大小
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
|
||||
console.log('🚀 开始菜单检查测试...');
|
||||
|
||||
// 访问登录页面
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('📝 填写登录信息...');
|
||||
// 登录
|
||||
await page.fill('input[placeholder="请输入用户名"]', 'admin');
|
||||
await page.fill('input[placeholder="密码"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
// 等待登录成功
|
||||
console.log('⏳ 等待登录成功...');
|
||||
await page.waitForURL(/dashboard/, { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // 额外等待确保页面完全加载
|
||||
|
||||
console.log('✅ 登录成功,当前URL:', page.url());
|
||||
|
||||
// 截图查看登录后的页面
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/after-login-full.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 查找侧边栏菜单
|
||||
console.log('🔍 查找侧边栏菜单...');
|
||||
|
||||
// 尝试不同的菜单选择器
|
||||
const menuSelectors = [
|
||||
'.ant-menu',
|
||||
'.ant-layout-sider .ant-menu',
|
||||
'[class*="menu"]',
|
||||
'[class*="sidebar"]',
|
||||
'[class*="nav"]',
|
||||
'.vben-layout-menu',
|
||||
'.layout-menu',
|
||||
'aside',
|
||||
'.ant-layout-sider',
|
||||
];
|
||||
|
||||
for (const selector of menuSelectors) {
|
||||
const elements = await page.locator(selector).count();
|
||||
console.log(`选择器 "${selector}": 找到 ${elements} 个元素`);
|
||||
|
||||
if (elements > 0) {
|
||||
const element = page.locator(selector).first();
|
||||
const isVisible = await element.isVisible();
|
||||
console.log(` - 第一个元素可见: ${isVisible}`);
|
||||
|
||||
if (isVisible) {
|
||||
const text = await element.textContent();
|
||||
const preview = text ? text.substring(0, 200) + '...' : '无文本内容';
|
||||
console.log(` - 内容预览: ${preview}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找所有可能的菜单项
|
||||
console.log('\n🎯 查找所有可能的菜单项...');
|
||||
|
||||
const menuItemSelectors = [
|
||||
'.ant-menu-item',
|
||||
'.ant-menu-submenu',
|
||||
'.ant-menu-submenu-title',
|
||||
'[role="menuitem"]',
|
||||
'[class*="menu-item"]',
|
||||
'a[href*="/"]',
|
||||
'li[class*="menu"]',
|
||||
];
|
||||
|
||||
let allMenuItems: string[] = [];
|
||||
|
||||
for (const selector of menuItemSelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
console.log(`\n选择器 "${selector}": 找到 ${elements.length} 个元素`);
|
||||
|
||||
for (let i = 0; i < Math.min(elements.length, 20); i++) {
|
||||
// 限制输出数量
|
||||
const element = elements[i];
|
||||
const isVisible = await element.isVisible();
|
||||
if (isVisible) {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim()) {
|
||||
const trimmedText = text.trim();
|
||||
if (!allMenuItems.includes(trimmedText)) {
|
||||
allMenuItems.push(trimmedText);
|
||||
console.log(` ${i + 1}. "${trimmedText}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`选择器 "${selector}" 出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 总共收集到 ${allMenuItems.length} 个不重复的菜单项`);
|
||||
|
||||
// 查找页面中的所有文本,看是否包含我们期望的菜单
|
||||
console.log('\n🔍 在页面中搜索特定菜单名称...');
|
||||
const pageContent = await page.content();
|
||||
|
||||
const expectedMenus = [
|
||||
'工具箱',
|
||||
'文件上传',
|
||||
'Excel导入导出',
|
||||
'WebSocket调试',
|
||||
'帮助中心',
|
||||
'系统文档',
|
||||
'权限示例',
|
||||
'群发任务',
|
||||
'群发日志',
|
||||
'账号管理',
|
||||
'智能姓名管理',
|
||||
];
|
||||
|
||||
expectedMenus.forEach((menuName) => {
|
||||
const found = pageContent.includes(menuName);
|
||||
console.log(
|
||||
`${found ? '✅' : '❌'} "${menuName}": ${found ? '在页面中找到' : '未找到'}`,
|
||||
);
|
||||
});
|
||||
|
||||
// 尝试点击可能的菜单展开按钮
|
||||
console.log('\n🖱️ 尝试展开菜单...');
|
||||
|
||||
const expandSelectors = [
|
||||
'.ant-menu-submenu-arrow',
|
||||
'.ant-menu-submenu-title',
|
||||
'[class*="collapse"]',
|
||||
'[class*="expand"]',
|
||||
'button[aria-expanded]',
|
||||
];
|
||||
|
||||
for (const selector of expandSelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
console.log(`展开选择器 "${selector}": 找到 ${elements.length} 个元素`);
|
||||
|
||||
for (let i = 0; i < Math.min(elements.length, 5); i++) {
|
||||
const element = elements[i];
|
||||
const isVisible = await element.isVisible();
|
||||
if (isVisible) {
|
||||
console.log(` 点击第 ${i + 1} 个展开元素`);
|
||||
await element.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`展开选择器 "${selector}" 出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次截图查看展开后的效果
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/after-expand-menus.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 最后再次收集菜单项
|
||||
console.log('\n📝 展开后重新收集菜单项...');
|
||||
|
||||
const finalMenuItems: string[] = [];
|
||||
|
||||
for (const selector of menuItemSelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
|
||||
for (const element of elements) {
|
||||
const isVisible = await element.isVisible();
|
||||
if (isVisible) {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim()) {
|
||||
const trimmedText = text.trim();
|
||||
if (!finalMenuItems.includes(trimmedText)) {
|
||||
finalMenuItems.push(trimmedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`最终收集选择器 "${selector}" 出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n📋 最终菜单列表:');
|
||||
finalMenuItems.sort().forEach((item, index) => {
|
||||
console.log(`${index + 1}. ${item}`);
|
||||
});
|
||||
|
||||
console.log(`\n🎉 最终统计: 共找到 ${finalMenuItems.length} 个菜单项`);
|
||||
|
||||
// 检查新增菜单
|
||||
const foundNewMenus = expectedMenus.filter((menu) =>
|
||||
finalMenuItems.some((item) => item.includes(menu)),
|
||||
);
|
||||
|
||||
console.log('\n✨ 新增菜单检查结果:');
|
||||
console.log(
|
||||
`找到的新菜单 (${foundNewMenus.length}/${expectedMenus.length}): ${foundNewMenus.join(', ')}`,
|
||||
);
|
||||
|
||||
const missingMenus = expectedMenus.filter(
|
||||
(menu) => !finalMenuItems.some((item) => item.includes(menu)),
|
||||
);
|
||||
|
||||
if (missingMenus.length > 0) {
|
||||
console.log(`❌ 缺失的菜单: ${missingMenus.join(', ')}`);
|
||||
}
|
||||
});
|
||||
254
frontend-vben/apps/web-antd/tests/e2e/icon-fixes-simple.test.ts
Normal file
254
frontend-vben/apps/web-antd/tests/e2e/icon-fixes-simple.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* 简化版图标修复验证测试
|
||||
* 只测试已确认修复的页面:
|
||||
* 1. /group-broadcast/log - CalendarOutlined 和 FileTextOutlined 图标
|
||||
* 2. /group-broadcast/task - ClockCircleOutlined 图标
|
||||
*/
|
||||
|
||||
test.describe('简化版图标修复验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 监听控制台错误
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 控制台错误: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 登录到系统
|
||||
await page.goto('/');
|
||||
|
||||
// 等待登录页面加载
|
||||
await page.waitForSelector(
|
||||
'input[placeholder*="用户名"], input[placeholder*="邮箱"], input[placeholder*="账号"]',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
// 填写登录信息
|
||||
await page.fill(
|
||||
'input[placeholder*="用户名"], input[placeholder*="邮箱"], input[placeholder*="账号"]',
|
||||
'admin',
|
||||
);
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click(
|
||||
'button[type="submit"], button:has-text("登录"), button:has-text("登入")',
|
||||
);
|
||||
|
||||
// 等待登录成功,跳转到主页
|
||||
await page.waitForURL(/dashboard|home|工作台/, { timeout: 15000 });
|
||||
|
||||
console.log('✅ 登录成功');
|
||||
});
|
||||
|
||||
test('验证群发日志页面的图标', async ({ page }) => {
|
||||
console.log('🧪 开始测试群发日志页面...');
|
||||
|
||||
// 记录图标相关的控制台错误
|
||||
const iconErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (
|
||||
msg.type() === 'error' &&
|
||||
(msg.text().includes('icon') ||
|
||||
msg.text().includes('import') ||
|
||||
msg.text().includes('export'))
|
||||
) {
|
||||
iconErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到群发日志页面
|
||||
await page.goto('/group-broadcast/log');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
|
||||
console.log('📄 群发日志页面已加载');
|
||||
|
||||
// 等待一段时间让所有资源加载完成
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 验证页面正常加载(使用更精确的选择器)
|
||||
await expect(
|
||||
page.locator('.ant-card-head-title').filter({ hasText: '群发日志' }),
|
||||
).toBeVisible();
|
||||
console.log('✅ 页面标题正确显示');
|
||||
|
||||
// 验证 FileTextOutlined 图标存在
|
||||
const fileTextIcon = page
|
||||
.locator(
|
||||
'.anticon-file-text, [data-icon="file-text"], svg[data-icon="file-text"]',
|
||||
)
|
||||
.first();
|
||||
await expect(fileTextIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ FileTextOutlined 图标显示正常');
|
||||
|
||||
// 验证 CalendarOutlined 图标存在(在日志项中)
|
||||
const calendarIcon = page
|
||||
.locator(
|
||||
'.anticon-calendar, [data-icon="calendar"], svg[data-icon="calendar"]',
|
||||
)
|
||||
.first();
|
||||
await expect(calendarIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ CalendarOutlined 图标显示正常');
|
||||
|
||||
// 检查是否有图标相关的控制台错误
|
||||
if (iconErrors.length > 0) {
|
||||
console.log('❌ 发现图标相关的控制台错误:', iconErrors);
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/group-broadcast-log-simple.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存群发日志页面截图');
|
||||
});
|
||||
|
||||
test('验证群发任务页面的图标', async ({ page }) => {
|
||||
console.log('🧪 开始测试群发任务页面...');
|
||||
|
||||
// 记录图标相关的控制台错误
|
||||
const iconErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (
|
||||
msg.type() === 'error' &&
|
||||
(msg.text().includes('icon') ||
|
||||
msg.text().includes('import') ||
|
||||
msg.text().includes('export'))
|
||||
) {
|
||||
iconErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到群发任务页面
|
||||
await page.goto('/group-broadcast/task');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
|
||||
console.log('📄 群发任务页面已加载');
|
||||
|
||||
// 等待一段时间让所有资源加载完成
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 验证页面正常加载(使用更精确的选择器)
|
||||
await expect(
|
||||
page.locator('.ant-card-head-title').filter({ hasText: '群发任务' }),
|
||||
).toBeVisible();
|
||||
console.log('✅ 页面标题正确显示');
|
||||
|
||||
// 验证 ClockCircleOutlined 图标存在(在表格中的发送频率列)
|
||||
const clockIcon = page
|
||||
.locator(
|
||||
'.anticon-clock-circle, [data-icon="clock-circle"], svg[data-icon="clock-circle"]',
|
||||
)
|
||||
.first();
|
||||
|
||||
// 等待表格数据加载
|
||||
await page.waitForSelector('.ant-table-tbody tr', { timeout: 10000 });
|
||||
|
||||
// 如果表格中有数据,应该能看到时钟图标
|
||||
const tableRows = await page.locator('.ant-table-tbody tr').count();
|
||||
if (tableRows > 0) {
|
||||
await expect(clockIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ ClockCircleOutlined 图标显示正常');
|
||||
} else {
|
||||
console.log('ℹ️ 表格暂无数据,无法验证ClockCircleOutlined图标');
|
||||
}
|
||||
|
||||
// 检查是否有图标相关的控制台错误
|
||||
if (iconErrors.length > 0) {
|
||||
console.log('❌ 发现图标相关的控制台错误:', iconErrors);
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/group-broadcast-task-simple.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存群发任务页面截图');
|
||||
});
|
||||
|
||||
test('检查页面是否可以正常导航到智能姓名管理页面', async ({ page }) => {
|
||||
console.log('🧪 测试智能姓名管理页面可访问性...');
|
||||
|
||||
// 记录控制台错误
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push(`${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到智能姓名管理页面
|
||||
await page.goto('/name-management/unified');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 等待一段时间让组件加载
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 检查页面是否加载成功
|
||||
const pageLoaded = (await page.locator('.ant-card').count()) > 0;
|
||||
|
||||
if (pageLoaded) {
|
||||
console.log('✅ 智能姓名管理页面可以正常访问');
|
||||
|
||||
// 检查图标
|
||||
const userAddIcon = page
|
||||
.locator(
|
||||
'.anticon-user-add, [data-icon="user-add"], svg[data-icon="user-add"]',
|
||||
)
|
||||
.first();
|
||||
const thunderboltIcon = page
|
||||
.locator(
|
||||
'.anticon-thunderbolt, [data-icon="thunderbolt"], svg[data-icon="thunderbolt"]',
|
||||
)
|
||||
.first();
|
||||
|
||||
const userAddVisible = await userAddIcon.isVisible();
|
||||
const thunderboltVisible = await thunderboltIcon.isVisible();
|
||||
|
||||
if (userAddVisible) {
|
||||
console.log('✅ UserAddOutlined 图标显示正常');
|
||||
} else {
|
||||
console.log('⚠️ UserAddOutlined 图标未找到');
|
||||
}
|
||||
|
||||
if (thunderboltVisible) {
|
||||
console.log('✅ ThunderboltOutlined 图标显示正常');
|
||||
} else {
|
||||
console.log('⚠️ ThunderboltOutlined 图标未找到');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 智能姓名管理页面加载失败');
|
||||
}
|
||||
|
||||
// 报告控制台错误
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log(`⚠️ 发现 ${consoleErrors.length} 个控制台错误:`);
|
||||
consoleErrors.forEach((error, index) => {
|
||||
console.log(` ${index + 1}. ${error}`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ 没有发现控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/name-management-unified-check.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存智能姓名管理页面截图');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,377 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* 图标修复验证测试
|
||||
* 验证以下页面的图标显示和控制台错误:
|
||||
* 1. /group-broadcast/log - CalendarOutlined 和 FileTextOutlined 图标
|
||||
* 2. /group-broadcast/task - ClockCircleOutlined 图标
|
||||
* 3. /name-management/unified - UserAddOutlined 和 ThunderboltOutlined 图标
|
||||
*/
|
||||
|
||||
test.describe('图标修复验证测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 监听控制台错误
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 控制台错误: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听页面错误
|
||||
page.on('pageerror', (error) => {
|
||||
console.log(`❌ 页面错误: ${error.message}`);
|
||||
});
|
||||
|
||||
// 登录到系统
|
||||
await page.goto('/');
|
||||
|
||||
// 等待登录页面加载
|
||||
await page.waitForSelector(
|
||||
'input[placeholder*="用户名"], input[placeholder*="邮箱"], input[placeholder*="账号"]',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
// 填写登录信息
|
||||
await page.fill(
|
||||
'input[placeholder*="用户名"], input[placeholder*="邮箱"], input[placeholder*="账号"]',
|
||||
'admin',
|
||||
);
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click(
|
||||
'button[type="submit"], button:has-text("登录"), button:has-text("登入")',
|
||||
);
|
||||
|
||||
// 等待登录成功,跳转到主页
|
||||
await page.waitForURL(/dashboard|home|工作台/, { timeout: 15000 });
|
||||
|
||||
console.log('✅ 登录成功');
|
||||
});
|
||||
|
||||
test('验证群发日志页面的图标', async ({ page }) => {
|
||||
console.log('🧪 开始测试群发日志页面...');
|
||||
|
||||
// 记录导航前的控制台错误
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && msg.text().includes('icon')) {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到群发日志页面
|
||||
await page.goto('/group-broadcast/log');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
|
||||
console.log('📄 群发日志页面已加载');
|
||||
|
||||
// 等待一段时间让所有资源加载完成
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 验证页面标题
|
||||
await expect(page.locator('text=群发日志')).toBeVisible();
|
||||
console.log('✅ 页面标题正确显示');
|
||||
|
||||
// 验证 FileTextOutlined 图标存在
|
||||
const fileTextIcon = page
|
||||
.locator(
|
||||
'.anticon-file-text, [data-icon="file-text"], svg[data-icon="file-text"]',
|
||||
)
|
||||
.first();
|
||||
await expect(fileTextIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ FileTextOutlined 图标显示正常');
|
||||
|
||||
// 验证 CalendarOutlined 图标存在(在日志项中)
|
||||
const calendarIcon = page
|
||||
.locator(
|
||||
'.anticon-calendar, [data-icon="calendar"], svg[data-icon="calendar"]',
|
||||
)
|
||||
.first();
|
||||
await expect(calendarIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ CalendarOutlined 图标显示正常');
|
||||
|
||||
// 检查是否有图标相关的控制台错误
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('❌ 发现图标相关的控制台错误:', consoleErrors);
|
||||
throw new Error(`发现 ${consoleErrors.length} 个图标相关的控制台错误`);
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/group-broadcast-log.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存群发日志页面截图');
|
||||
});
|
||||
|
||||
test('验证群发任务页面的图标', async ({ page }) => {
|
||||
console.log('🧪 开始测试群发任务页面...');
|
||||
|
||||
// 记录导航前的控制台错误
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && msg.text().includes('icon')) {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到群发任务页面
|
||||
await page.goto('/group-broadcast/task');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
|
||||
console.log('📄 群发任务页面已加载');
|
||||
|
||||
// 等待一段时间让所有资源加载完成
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 验证页面标题
|
||||
await expect(page.locator('text=群发任务')).toBeVisible();
|
||||
console.log('✅ 页面标题正确显示');
|
||||
|
||||
// 验证 ClockCircleOutlined 图标存在(在表格中的发送频率列)
|
||||
const clockIcon = page
|
||||
.locator(
|
||||
'.anticon-clock-circle, [data-icon="clock-circle"], svg[data-icon="clock-circle"]',
|
||||
)
|
||||
.first();
|
||||
|
||||
// 等待表格数据加载
|
||||
await page.waitForSelector('.ant-table-tbody tr', { timeout: 10000 });
|
||||
|
||||
// 如果表格中有数据,应该能看到时钟图标
|
||||
const tableRows = await page.locator('.ant-table-tbody tr').count();
|
||||
if (tableRows > 0) {
|
||||
await expect(clockIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ ClockCircleOutlined 图标显示正常');
|
||||
} else {
|
||||
console.log('ℹ️ 表格暂无数据,无法验证ClockCircleOutlined图标');
|
||||
}
|
||||
|
||||
// 检查是否有图标相关的控制台错误
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('❌ 发现图标相关的控制台错误:', consoleErrors);
|
||||
throw new Error(`发现 ${consoleErrors.length} 个图标相关的控制台错误`);
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/group-broadcast-task.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存群发任务页面截图');
|
||||
});
|
||||
|
||||
test('验证智能姓名管理页面的图标', async ({ page }) => {
|
||||
console.log('🧪 开始测试智能姓名管理页面...');
|
||||
|
||||
// 记录导航前的控制台错误
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && msg.text().includes('icon')) {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 导航到智能姓名管理页面
|
||||
await page.goto('/name-management/unified');
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
|
||||
console.log('📄 智能姓名管理页面已加载');
|
||||
|
||||
// 等待一段时间让所有资源加载完成
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 验证页面标题
|
||||
await expect(page.locator('text=智能姓名管理系统')).toBeVisible();
|
||||
console.log('✅ 页面标题正确显示');
|
||||
|
||||
// 验证 UserAddOutlined 图标存在(在卡片标题的extra区域)
|
||||
const userAddIcon = page
|
||||
.locator(
|
||||
'.anticon-user-add, [data-icon="user-add"], svg[data-icon="user-add"]',
|
||||
)
|
||||
.first();
|
||||
await expect(userAddIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ UserAddOutlined 图标显示正常');
|
||||
|
||||
// 验证 ThunderboltOutlined 图标存在(在生成姓名按钮中)
|
||||
const thunderboltIcon = page
|
||||
.locator(
|
||||
'.anticon-thunderbolt, [data-icon="thunderbolt"], svg[data-icon="thunderbolt"]',
|
||||
)
|
||||
.first();
|
||||
await expect(thunderboltIcon).toBeVisible({ timeout: 5000 });
|
||||
console.log('✅ ThunderboltOutlined 图标显示正常');
|
||||
|
||||
// 检查是否有图标相关的控制台错误
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('❌ 发现图标相关的控制台错误:', consoleErrors);
|
||||
throw new Error(`发现 ${consoleErrors.length} 个图标相关的控制台错误`);
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/name-management-unified.png',
|
||||
fullPage: true,
|
||||
});
|
||||
console.log('📸 已保存智能姓名管理页面截图');
|
||||
});
|
||||
|
||||
test('综合图标验证测试', async ({ page }) => {
|
||||
console.log('🧪 开始综合图标验证测试...');
|
||||
|
||||
const allConsoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
allConsoleErrors.push(`[${new Date().toISOString()}] ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const testPages = [
|
||||
{
|
||||
url: '/group-broadcast/log',
|
||||
name: '群发日志',
|
||||
icons: ['file-text', 'calendar'],
|
||||
},
|
||||
{
|
||||
url: '/group-broadcast/task',
|
||||
name: '群发任务',
|
||||
icons: ['clock-circle'],
|
||||
},
|
||||
{
|
||||
url: '/name-management/unified',
|
||||
name: '智能姓名管理',
|
||||
icons: ['user-add', 'thunderbolt'],
|
||||
},
|
||||
];
|
||||
|
||||
const testResults = [];
|
||||
|
||||
for (const testPage of testPages) {
|
||||
console.log(`\n📋 测试页面: ${testPage.name} (${testPage.url})`);
|
||||
|
||||
try {
|
||||
// 导航到页面
|
||||
await page.goto(testPage.url);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.ant-card', { timeout: 10000 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 验证每个图标
|
||||
const iconResults = [];
|
||||
for (const iconName of testPage.icons) {
|
||||
try {
|
||||
const iconSelector = `.anticon-${iconName}, [data-icon="${iconName}"], svg[data-icon="${iconName}"]`;
|
||||
await expect(page.locator(iconSelector).first()).toBeVisible({
|
||||
timeout: 3000,
|
||||
});
|
||||
iconResults.push({ icon: iconName, status: '✅ 正常' });
|
||||
console.log(` ✅ ${iconName} 图标正常`);
|
||||
} catch (error) {
|
||||
iconResults.push({
|
||||
icon: iconName,
|
||||
status: '❌ 异常',
|
||||
error: error.message,
|
||||
});
|
||||
console.log(` ❌ ${iconName} 图标异常: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
testResults.push({
|
||||
page: testPage.name,
|
||||
url: testPage.url,
|
||||
icons: iconResults,
|
||||
status: iconResults.every((r) => r.status.includes('✅'))
|
||||
? '✅ 通过'
|
||||
: '❌ 失败',
|
||||
});
|
||||
|
||||
// 截图
|
||||
await page.screenshot({
|
||||
path: `test-results/screenshots/comprehensive-${testPage.name.replace(/\s+/g, '-')}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ❌ 页面加载失败: ${error.message}`);
|
||||
testResults.push({
|
||||
page: testPage.name,
|
||||
url: testPage.url,
|
||||
icons: [],
|
||||
status: '❌ 页面加载失败',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 生成测试报告
|
||||
console.log('\n📊 测试结果汇总:');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
let allPassed = true;
|
||||
for (const result of testResults) {
|
||||
console.log(`\n📄 ${result.page} (${result.url})`);
|
||||
console.log(` 状态: ${result.status}`);
|
||||
|
||||
if (result.icons.length > 0) {
|
||||
for (const iconResult of result.icons) {
|
||||
console.log(` ${iconResult.status} ${iconResult.icon}`);
|
||||
if (iconResult.error) {
|
||||
console.log(` 错误: ${iconResult.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
console.log(` 错误: ${result.error}`);
|
||||
allPassed = false;
|
||||
} else if (result.status.includes('❌')) {
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 控制台错误汇总
|
||||
if (allConsoleErrors.length > 0) {
|
||||
console.log('\n🚨 控制台错误汇总:');
|
||||
const iconErrors = allConsoleErrors.filter(
|
||||
(error) =>
|
||||
error.toLowerCase().includes('icon') ||
|
||||
error.toLowerCase().includes('import') ||
|
||||
error.toLowerCase().includes('module'),
|
||||
);
|
||||
|
||||
if (iconErrors.length > 0) {
|
||||
console.log(`❌ 发现 ${iconErrors.length} 个可能与图标相关的错误:`);
|
||||
iconErrors.forEach((error, index) => {
|
||||
console.log(` ${index + 1}. ${error}`);
|
||||
});
|
||||
allPassed = false;
|
||||
} else {
|
||||
console.log('✅ 没有发现图标相关的控制台错误');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
if (allPassed) {
|
||||
console.log('🎉 所有图标验证测试通过!');
|
||||
} else {
|
||||
console.log('❌ 部分图标验证测试失败,请检查上述错误');
|
||||
throw new Error('图标验证测试失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
121
frontend-vben/apps/web-antd/tests/e2e/login-debug.test.ts
Normal file
121
frontend-vben/apps/web-antd/tests/e2e/login-debug.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('登录调试测试', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
|
||||
console.log('🚀 访问登录页面...');
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('📝 填写登录信息...');
|
||||
|
||||
// 填写用户名
|
||||
const usernameInput = page.locator('input[placeholder="请输入用户名"]');
|
||||
await usernameInput.clear();
|
||||
await usernameInput.fill('admin');
|
||||
console.log('✅ 用户名已填写');
|
||||
|
||||
// 填写密码
|
||||
const passwordInput = page.locator('input[placeholder="密码"]');
|
||||
await passwordInput.clear();
|
||||
await passwordInput.fill('111111');
|
||||
console.log('✅ 密码已填写');
|
||||
|
||||
// 截图查看填写后的状态
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/before-login-click.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log('🖱️ 点击登录按钮...');
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
await loginButton.click();
|
||||
|
||||
console.log('⏳ 等待登录处理...');
|
||||
|
||||
// 等待一段时间观察变化
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
console.log('📍 当前URL:', page.url());
|
||||
console.log('📍 页面标题:', await page.title());
|
||||
|
||||
// 截图查看登录后的状态
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/after-login-click.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 检查是否有错误消息
|
||||
const errorMessages = await page
|
||||
.locator('.ant-notification, .ant-message, [class*="error"]')
|
||||
.all();
|
||||
console.log(`🔍 找到 ${errorMessages.length} 个可能的错误消息`);
|
||||
|
||||
for (let i = 0; i < errorMessages.length; i++) {
|
||||
const message = errorMessages[i];
|
||||
const isVisible = await message.isVisible();
|
||||
if (isVisible) {
|
||||
const text = await message.textContent();
|
||||
console.log(`❌ 错误消息 ${i + 1}: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有成功消息
|
||||
const successMessages = await page
|
||||
.locator(
|
||||
'.ant-notification-success, .ant-message-success, [class*="success"]',
|
||||
)
|
||||
.all();
|
||||
console.log(`🔍 找到 ${successMessages.length} 个可能的成功消息`);
|
||||
|
||||
for (let i = 0; i < successMessages.length; i++) {
|
||||
const message = successMessages[i];
|
||||
const isVisible = await message.isVisible();
|
||||
if (isVisible) {
|
||||
const text = await message.textContent();
|
||||
console.log(`✅ 成功消息 ${i + 1}: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查URL变化
|
||||
if (
|
||||
page.url().includes('dashboard') ||
|
||||
page.url().includes('home') ||
|
||||
page.url().includes('workspace')
|
||||
) {
|
||||
console.log('🎉 登录成功,已跳转到主页面');
|
||||
|
||||
// 等待页面完全加载
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 截图查看主页面
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/main-dashboard.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 查找菜单
|
||||
console.log('🔍 在主页面查找菜单...');
|
||||
|
||||
const menuElements = await page
|
||||
.locator(
|
||||
'.ant-menu, .ant-layout-sider, [class*="menu"], [class*="sidebar"]',
|
||||
)
|
||||
.all();
|
||||
console.log(`找到 ${menuElements.length} 个可能的菜单容器`);
|
||||
|
||||
for (let i = 0; i < menuElements.length; i++) {
|
||||
const element = menuElements[i];
|
||||
const isVisible = await element.isVisible();
|
||||
if (isVisible) {
|
||||
const text = await element.textContent();
|
||||
const preview = text ? text.substring(0, 300) + '...' : '无文本内容';
|
||||
console.log(`📋 菜单容器 ${i + 1}: ${preview}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 登录可能失败,未跳转到主页面');
|
||||
console.log('当前URL:', page.url());
|
||||
}
|
||||
});
|
||||
567
frontend-vben/apps/web-antd/tests/e2e/marketing-center.test.ts
Normal file
567
frontend-vben/apps/web-antd/tests/e2e/marketing-center.test.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* 营销中心功能端到端测试
|
||||
* 测试智能活动、客户分析、效果统计等功能
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
adminUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
};
|
||||
|
||||
// 测试营销活动数据
|
||||
const TEST_CAMPAIGN = {
|
||||
name: '测试营销活动_' + Date.now(),
|
||||
type: 'promotion',
|
||||
budget: '10000',
|
||||
startDate: '2024-02-01',
|
||||
endDate: '2024-02-28',
|
||||
description: '这是一个测试营销活动,用于验证系统功能',
|
||||
targetAudience: 'all',
|
||||
channels: ['telegram', 'email'],
|
||||
};
|
||||
|
||||
test.describe('营销中心 - 智能活动管理', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到智能活动页面
|
||||
await page.click('[data-testid="menu-marketing"]');
|
||||
await page.click('[data-testid="menu-smart-campaign"]');
|
||||
await page.waitForURL(/\/marketing\/smart-campaign/);
|
||||
});
|
||||
|
||||
test('应该正确显示智能活动页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/智能活动|营销活动/,
|
||||
);
|
||||
|
||||
// 检查活动概览卡片
|
||||
await expect(page.locator('.campaign-overview')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="total-campaigns"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="active-campaigns"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="total-reach"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="conversion-rate"]')).toBeVisible();
|
||||
|
||||
// 检查创建活动按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="create-campaign-button"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查活动列表
|
||||
await expect(page.locator('.campaign-list, .ant-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能创建新的营销活动', async ({ page }) => {
|
||||
// 点击创建活动按钮
|
||||
await page.click('[data-testid="create-campaign-button"]');
|
||||
|
||||
// 等待创建页面加载
|
||||
await page.waitForURL(/\/marketing\/smart-campaign\/create/);
|
||||
|
||||
// 第一步:基本信息
|
||||
await page.fill('[data-testid="campaign-name"]', TEST_CAMPAIGN.name);
|
||||
await page.selectOption(
|
||||
'[data-testid="campaign-type"]',
|
||||
TEST_CAMPAIGN.type,
|
||||
);
|
||||
await page.fill('[data-testid="campaign-budget"]', TEST_CAMPAIGN.budget);
|
||||
await page.fill(
|
||||
'[data-testid="campaign-description"]',
|
||||
TEST_CAMPAIGN.description,
|
||||
);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第二步:目标受众
|
||||
await page.click(
|
||||
`[data-testid="audience-${TEST_CAMPAIGN.targetAudience}"]`,
|
||||
);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第三步:推广渠道
|
||||
for (const channel of TEST_CAMPAIGN.channels) {
|
||||
await page.check(`[data-testid="channel-${channel}"]`);
|
||||
}
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第四步:时间设置
|
||||
await page.fill('[data-testid="start-date"]', TEST_CAMPAIGN.startDate);
|
||||
await page.fill('[data-testid="end-date"]', TEST_CAMPAIGN.endDate);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第五步:确认创建
|
||||
await expect(page.locator('.campaign-summary')).toBeVisible();
|
||||
await expect(page.locator('.campaign-summary')).toContainText(
|
||||
TEST_CAMPAIGN.name,
|
||||
);
|
||||
await page.click('[data-testid="create-campaign"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/创建成功|添加成功/,
|
||||
);
|
||||
|
||||
// 应该跳转回活动列表
|
||||
await page.waitForURL(/\/marketing\/smart-campaign$/);
|
||||
});
|
||||
|
||||
test('活动状态管理应该正常工作', async ({ page }) => {
|
||||
// 找到刚创建的活动
|
||||
const campaignRow = page
|
||||
.locator('.campaign-item, .ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_CAMPAIGN.name });
|
||||
|
||||
if ((await campaignRow.count()) > 0) {
|
||||
// 点击启动活动按钮
|
||||
await campaignRow.locator('[data-testid="start-campaign"]').click();
|
||||
|
||||
// 确认启动
|
||||
await page.click('[data-testid="confirm-start"]');
|
||||
|
||||
// 等待状态更新
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 检查活动状态是否变为进行中
|
||||
await page.reload();
|
||||
await expect(campaignRow.locator('.campaign-status')).toContainText(
|
||||
/进行中|active/,
|
||||
);
|
||||
|
||||
// 测试暂停功能
|
||||
await campaignRow.locator('[data-testid="pause-campaign"]').click();
|
||||
await page.click('[data-testid="confirm-pause"]');
|
||||
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('活动详情页面应该正常显示', async ({ page }) => {
|
||||
// 点击第一个活动的详情按钮
|
||||
const detailButton = page
|
||||
.locator('.campaign-item, .ant-table-tbody tr')
|
||||
.first()
|
||||
.locator('[data-testid="view-detail"]');
|
||||
await detailButton.click();
|
||||
|
||||
// 等待详情页面加载
|
||||
await page.waitForURL(/\/marketing\/smart-campaign\/\d+/);
|
||||
|
||||
// 检查活动基本信息
|
||||
await expect(page.locator('.campaign-info')).toBeVisible();
|
||||
await expect(page.locator('.campaign-progress')).toBeVisible();
|
||||
|
||||
// 检查效果统计图表
|
||||
await expect(page.locator('.performance-charts')).toBeVisible();
|
||||
await expect(page.locator('.echarts-container')).toBeVisible();
|
||||
|
||||
// 检查目标受众分析
|
||||
await expect(page.locator('.audience-analysis')).toBeVisible();
|
||||
|
||||
// 检查渠道效果对比
|
||||
await expect(page.locator('.channel-comparison')).toBeVisible();
|
||||
});
|
||||
|
||||
test('智能推荐功能应该正常工作', async ({ page }) => {
|
||||
// 点击智能推荐按钮
|
||||
await page.click('[data-testid="ai-recommendations"]');
|
||||
|
||||
// 等待推荐结果加载
|
||||
await expect(page.locator('.ai-recommendations-panel')).toBeVisible();
|
||||
|
||||
// 检查推荐内容
|
||||
await expect(page.locator('.recommendation-item')).toHaveCount({ min: 1 });
|
||||
|
||||
// 点击采纳推荐
|
||||
const firstRecommendation = page.locator('.recommendation-item').first();
|
||||
await firstRecommendation
|
||||
.locator('[data-testid="adopt-recommendation"]')
|
||||
.click();
|
||||
|
||||
// 确认采纳
|
||||
await page.click('[data-testid="confirm-adopt"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('活动复制功能应该正常工作', async ({ page }) => {
|
||||
// 找到一个活动并点击复制
|
||||
const campaignRow = page
|
||||
.locator('.campaign-item, .ant-table-tbody tr')
|
||||
.first();
|
||||
await campaignRow.locator('[data-testid="copy-campaign"]').click();
|
||||
|
||||
// 等待复制弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/复制活动|克隆活动/,
|
||||
);
|
||||
|
||||
// 修改活动名称
|
||||
const newName = '复制的营销活动_' + Date.now();
|
||||
await page.fill('[data-testid="copy-campaign-name"]', newName);
|
||||
|
||||
// 确认复制
|
||||
await page.click('[data-testid="confirm-copy"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 验证复制的活动出现在列表中
|
||||
await page.reload();
|
||||
await expect(
|
||||
page
|
||||
.locator('.campaign-item, .ant-table-tbody tr')
|
||||
.filter({ hasText: newName }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('营销中心 - 客户分析', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到客户分析页面
|
||||
await page.click('[data-testid="menu-marketing"]');
|
||||
await page.click('[data-testid="menu-customer-analysis"]');
|
||||
await page.waitForURL(/\/marketing\/customer-analysis/);
|
||||
});
|
||||
|
||||
test('应该正确显示客户分析页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/客户分析|用户分析/,
|
||||
);
|
||||
|
||||
// 检查分析概览
|
||||
await expect(page.locator('.analysis-overview')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="total-customers"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="active-customers"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="customer-growth"]')).toBeVisible();
|
||||
|
||||
// 检查分析图表
|
||||
await expect(page.locator('.analysis-charts')).toBeVisible();
|
||||
await expect(page.locator('.echarts-container')).toHaveCount({ min: 2 });
|
||||
|
||||
// 检查客户分群
|
||||
await expect(page.locator('.customer-segments')).toBeVisible();
|
||||
});
|
||||
|
||||
test('客户分群功能应该正常工作', async ({ page }) => {
|
||||
// 点击创建分群按钮
|
||||
await page.click('[data-testid="create-segment"]');
|
||||
|
||||
// 等待分群创建弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/创建分群|新建分群/,
|
||||
);
|
||||
|
||||
// 填写分群信息
|
||||
const segmentName = '测试客户分群_' + Date.now();
|
||||
await page.fill('[data-testid="segment-name"]', segmentName);
|
||||
|
||||
// 设置分群条件
|
||||
await page.selectOption('[data-testid="condition-field"]', 'last_login');
|
||||
await page.selectOption('[data-testid="condition-operator"]', 'within');
|
||||
await page.fill('[data-testid="condition-value"]', '30');
|
||||
await page.selectOption('[data-testid="condition-unit"]', 'days');
|
||||
|
||||
// 添加更多条件
|
||||
await page.click('[data-testid="add-condition"]');
|
||||
await page.selectOption(
|
||||
'[data-testid="condition-field-2"]',
|
||||
'registration_date',
|
||||
);
|
||||
await page.selectOption('[data-testid="condition-operator-2"]', 'after');
|
||||
await page.fill('[data-testid="condition-value-2"]', '2024-01-01');
|
||||
|
||||
// 预览分群结果
|
||||
await page.click('[data-testid="preview-segment"]');
|
||||
await expect(page.locator('.segment-preview')).toBeVisible();
|
||||
|
||||
// 保存分群
|
||||
await page.click('[data-testid="save-segment"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('客户行为分析应该正常工作', async ({ page }) => {
|
||||
// 切换到行为分析标签
|
||||
await page.click('[data-testid="tab-behavior-analysis"]');
|
||||
|
||||
// 检查行为分析图表
|
||||
await expect(page.locator('.behavior-analysis')).toBeVisible();
|
||||
await expect(page.locator('.user-journey')).toBeVisible();
|
||||
await expect(page.locator('.conversion-funnel')).toBeVisible();
|
||||
|
||||
// 选择分析时间范围
|
||||
await page.click('[data-testid="time-range-selector"]');
|
||||
await page.click('[data-testid="range-30days"]');
|
||||
|
||||
// 等待图表更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查图表是否重新加载
|
||||
await expect(page.locator('.echarts-container')).toBeVisible();
|
||||
});
|
||||
|
||||
test('客户价值分析应该正常工作', async ({ page }) => {
|
||||
// 切换到价值分析标签
|
||||
await page.click('[data-testid="tab-value-analysis"]');
|
||||
|
||||
// 检查价值分析内容
|
||||
await expect(page.locator('.value-analysis')).toBeVisible();
|
||||
await expect(page.locator('.ltv-chart')).toBeVisible(); // 客户生命周期价值
|
||||
await expect(page.locator('.rfm-analysis')).toBeVisible(); // RFM分析
|
||||
|
||||
// 检查高价值客户列表
|
||||
await expect(page.locator('.high-value-customers')).toBeVisible();
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
|
||||
// 点击导出高价值客户
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.click('[data-testid="export-high-value-customers"]');
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/高价值客户.*\.xlsx$/);
|
||||
});
|
||||
|
||||
test('客户流失分析应该正常工作', async ({ page }) => {
|
||||
// 切换到流失分析标签
|
||||
await page.click('[data-testid="tab-churn-analysis"]');
|
||||
|
||||
// 检查流失分析内容
|
||||
await expect(page.locator('.churn-analysis')).toBeVisible();
|
||||
await expect(page.locator('.churn-prediction')).toBeVisible();
|
||||
await expect(page.locator('.churn-reasons')).toBeVisible();
|
||||
|
||||
// 检查流失预警列表
|
||||
await expect(page.locator('.churn-alerts')).toBeVisible();
|
||||
|
||||
// 点击查看流失预警详情
|
||||
const alertButton = page
|
||||
.locator('.churn-alert-item')
|
||||
.first()
|
||||
.locator('[data-testid="view-alert"]');
|
||||
if ((await alertButton.count()) > 0) {
|
||||
await alertButton.click();
|
||||
|
||||
// 检查预警详情弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.alert-details')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('营销中心 - 效果统计', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到效果统计页面
|
||||
await page.click('[data-testid="menu-marketing"]');
|
||||
await page.click('[data-testid="menu-performance-stats"]');
|
||||
await page.waitForURL(/\/marketing\/performance/);
|
||||
});
|
||||
|
||||
test('应该正确显示效果统计页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/效果统计|营销效果/,
|
||||
);
|
||||
|
||||
// 检查统计卡片
|
||||
await expect(page.locator('.stats-cards')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="total-impressions"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="total-clicks"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="total-conversions"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="total-revenue"]')).toBeVisible();
|
||||
|
||||
// 检查趋势图表
|
||||
await expect(page.locator('.trend-charts')).toBeVisible();
|
||||
await expect(page.locator('.echarts-container')).toHaveCount({ min: 2 });
|
||||
});
|
||||
|
||||
test('时间范围筛选应该正常工作', async ({ page }) => {
|
||||
// 点击时间范围选择器
|
||||
await page.click('[data-testid="date-range-picker"]');
|
||||
|
||||
// 选择最近7天
|
||||
await page.click('[data-testid="preset-7days"]');
|
||||
|
||||
// 等待数据更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查数据是否更新
|
||||
await expect(page.locator('.stats-cards')).toBeVisible();
|
||||
|
||||
// 选择自定义时间范围
|
||||
await page.click('[data-testid="date-range-picker"]');
|
||||
await page.click('[data-testid="custom-range"]');
|
||||
|
||||
// 选择开始日期
|
||||
await page.click('.ant-picker-input:first-child');
|
||||
await page.click('.ant-picker-today-btn');
|
||||
|
||||
// 选择结束日期
|
||||
await page.click('.ant-picker-input:last-child');
|
||||
await page.click('.ant-picker-today-btn');
|
||||
|
||||
// 确认选择
|
||||
await page.click('.ant-picker-ok');
|
||||
|
||||
// 等待数据更新
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test('渠道对比分析应该正常工作', async ({ page }) => {
|
||||
// 切换到渠道对比标签
|
||||
await page.click('[data-testid="tab-channel-comparison"]');
|
||||
|
||||
// 检查渠道对比图表
|
||||
await expect(page.locator('.channel-comparison')).toBeVisible();
|
||||
await expect(page.locator('.channel-performance-table')).toBeVisible();
|
||||
|
||||
// 检查渠道效果表格
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
|
||||
// 检查渠道排序功能
|
||||
const channelHeader = page
|
||||
.locator('.ant-table-thead th')
|
||||
.filter({ hasText: /转化率|Conversion Rate/ });
|
||||
await channelHeader.click();
|
||||
|
||||
// 等待排序完成
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查排序指示器
|
||||
await expect(
|
||||
channelHeader.locator(
|
||||
'.ant-table-column-sorter-up.active, .ant-table-column-sorter-down.active',
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('活动效果对比应该正常工作', async ({ page }) => {
|
||||
// 切换到活动对比标签
|
||||
await page.click('[data-testid="tab-campaign-comparison"]');
|
||||
|
||||
// 检查活动对比内容
|
||||
await expect(page.locator('.campaign-comparison')).toBeVisible();
|
||||
|
||||
// 选择要对比的活动
|
||||
await page.click('[data-testid="select-campaigns"]');
|
||||
await page.check('.campaign-option:first-child input');
|
||||
await page.check('.campaign-option:nth-child(2) input');
|
||||
await page.click('[data-testid="confirm-selection"]');
|
||||
|
||||
// 等待对比结果加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查对比图表
|
||||
await expect(page.locator('.comparison-chart')).toBeVisible();
|
||||
await expect(page.locator('.comparison-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('转化漏斗分析应该正常工作', async ({ page }) => {
|
||||
// 切换到转化漏斗标签
|
||||
await page.click('[data-testid="tab-conversion-funnel"]');
|
||||
|
||||
// 检查转化漏斗图
|
||||
await expect(page.locator('.conversion-funnel')).toBeVisible();
|
||||
await expect(page.locator('.funnel-chart')).toBeVisible();
|
||||
|
||||
// 检查漏斗各阶段数据
|
||||
await expect(page.locator('.funnel-stage')).toHaveCount({ min: 3 });
|
||||
|
||||
// 点击漏斗阶段查看详情
|
||||
const firstStage = page.locator('.funnel-stage').first();
|
||||
await firstStage.click();
|
||||
|
||||
// 检查阶段详情
|
||||
await expect(page.locator('.stage-details')).toBeVisible();
|
||||
});
|
||||
|
||||
test('ROI分析应该正常工作', async ({ page }) => {
|
||||
// 切换到ROI分析标签
|
||||
await page.click('[data-testid="tab-roi-analysis"]');
|
||||
|
||||
// 检查ROI分析内容
|
||||
await expect(page.locator('.roi-analysis')).toBeVisible();
|
||||
await expect(page.locator('.roi-chart')).toBeVisible();
|
||||
await expect(page.locator('.cost-revenue-comparison')).toBeVisible();
|
||||
|
||||
// 检查ROI计算器
|
||||
await expect(page.locator('.roi-calculator')).toBeVisible();
|
||||
|
||||
// 使用ROI计算器
|
||||
await page.fill('[data-testid="investment-amount"]', '10000');
|
||||
await page.fill('[data-testid="revenue-amount"]', '15000');
|
||||
await page.click('[data-testid="calculate-roi"]');
|
||||
|
||||
// 检查计算结果
|
||||
await expect(page.locator('[data-testid="roi-result"]')).toContainText(
|
||||
'50%',
|
||||
);
|
||||
});
|
||||
|
||||
test('报告导出功能应该正常工作', async ({ page }) => {
|
||||
// 点击导出报告按钮
|
||||
await page.click('[data-testid="export-report"]');
|
||||
|
||||
// 等待导出选项弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/导出报告|Export Report/,
|
||||
);
|
||||
|
||||
// 选择导出格式
|
||||
await page.click('[data-testid="format-pdf"]');
|
||||
|
||||
// 选择导出内容
|
||||
await page.check('[data-testid="include-charts"]');
|
||||
await page.check('[data-testid="include-tables"]');
|
||||
|
||||
// 确认导出
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.click('[data-testid="confirm-export"]');
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/营销效果报告.*\.pdf$/);
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:管理员登录
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.adminUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.adminUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
305
frontend-vben/apps/web-antd/tests/e2e/menu-validation.test.ts
Normal file
305
frontend-vben/apps/web-antd/tests/e2e/menu-validation.test.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
test.describe('菜单验证测试', () => {
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
|
||||
// 设置视口大小以确保菜单完全可见
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
|
||||
// 访问登录页面
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 登录
|
||||
await page.fill('input[placeholder="请输入用户名"]', 'admin');
|
||||
await page.fill('input[placeholder="密码"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
// 等待登录成功,跳转到主页
|
||||
await page.waitForURL(/dashboard/, { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('✅ 登录成功');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('验证菜单总数和新增菜单项', async () => {
|
||||
// 等待页面完全加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 展开所有菜单项的函数
|
||||
const expandAllMenus = async () => {
|
||||
console.log('🔍 开始展开所有菜单项...');
|
||||
|
||||
// 找到所有可展开的菜单项
|
||||
const menuItems = await page.locator('.ant-menu-submenu').all();
|
||||
console.log(`发现 ${menuItems.length} 个子菜单`);
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
try {
|
||||
const menuItem = menuItems[i];
|
||||
const isOpen =
|
||||
(await menuItem.locator('.ant-menu-submenu-open').count()) > 0;
|
||||
|
||||
if (!isOpen) {
|
||||
const menuTitle = await menuItem
|
||||
.locator('.ant-menu-submenu-title')
|
||||
.first();
|
||||
const titleText = await menuTitle.textContent();
|
||||
console.log(`📂 展开菜单: ${titleText}`);
|
||||
|
||||
await menuTitle.click();
|
||||
await page.waitForTimeout(500); // 等待动画完成
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ 展开菜单项 ${i} 时出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有菜单展开完成
|
||||
await page.waitForTimeout(1000);
|
||||
};
|
||||
|
||||
// 展开所有菜单
|
||||
await expandAllMenus();
|
||||
|
||||
// 收集所有菜单项
|
||||
const collectAllMenuItems = async () => {
|
||||
console.log('📝 开始收集菜单项...');
|
||||
const menuItems: string[] = [];
|
||||
|
||||
// 获取所有菜单项(包含子菜单标题和叶子菜单项)
|
||||
const allMenuElements = await page
|
||||
.locator('.ant-menu-item, .ant-menu-submenu-title')
|
||||
.all();
|
||||
|
||||
for (const element of allMenuElements) {
|
||||
try {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim() && !menuItems.includes(text.trim())) {
|
||||
menuItems.push(text.trim());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('收集菜单项时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return menuItems.sort();
|
||||
};
|
||||
|
||||
const allMenuItems = await collectAllMenuItems();
|
||||
|
||||
console.log('📊 所有菜单项列表:');
|
||||
allMenuItems.forEach((item, index) => {
|
||||
console.log(`${index + 1}. ${item}`);
|
||||
});
|
||||
|
||||
console.log(`\n📈 菜单总数: ${allMenuItems.length}`);
|
||||
|
||||
// 验证新增的菜单项
|
||||
const expectedNewMenus = [
|
||||
'工具箱',
|
||||
'文件上传',
|
||||
'Excel导入导出',
|
||||
'WebSocket调试',
|
||||
'帮助中心',
|
||||
'系统文档',
|
||||
'权限示例',
|
||||
];
|
||||
|
||||
console.log('\n🔍 验证新增菜单项:');
|
||||
const foundNewMenus: string[] = [];
|
||||
const missingMenus: string[] = [];
|
||||
|
||||
expectedNewMenus.forEach((menuName) => {
|
||||
const found = allMenuItems.some((item) => item.includes(menuName));
|
||||
if (found) {
|
||||
foundNewMenus.push(menuName);
|
||||
console.log(`✅ 找到: ${menuName}`);
|
||||
} else {
|
||||
missingMenus.push(menuName);
|
||||
console.log(`❌ 缺失: ${menuName}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 统计结果
|
||||
console.log('\n📊 验证结果总结:');
|
||||
console.log(`菜单总数: ${allMenuItems.length}`);
|
||||
console.log(
|
||||
`新增菜单找到: ${foundNewMenus.length}/${expectedNewMenus.length}`,
|
||||
);
|
||||
console.log(`找到的新菜单: ${foundNewMenus.join(', ')}`);
|
||||
|
||||
if (missingMenus.length > 0) {
|
||||
console.log(`❌ 缺失的菜单: ${missingMenus.join(', ')}`);
|
||||
}
|
||||
|
||||
// 特别检查工具箱相关菜单
|
||||
const toolboxMenus = allMenuItems.filter(
|
||||
(item) =>
|
||||
item.includes('工具箱') ||
|
||||
item.includes('文件上传') ||
|
||||
item.includes('Excel') ||
|
||||
item.includes('WebSocket'),
|
||||
);
|
||||
|
||||
console.log('\n🛠️ 工具箱相关菜单:');
|
||||
toolboxMenus.forEach((menu) => console.log(` - ${menu}`));
|
||||
|
||||
// 特别检查帮助中心相关菜单
|
||||
const helpMenus = allMenuItems.filter(
|
||||
(item) =>
|
||||
item.includes('帮助中心') ||
|
||||
item.includes('系统文档') ||
|
||||
item.includes('权限示例'),
|
||||
);
|
||||
|
||||
console.log('\n❓ 帮助中心相关菜单:');
|
||||
helpMenus.forEach((menu) => console.log(` - ${menu}`));
|
||||
|
||||
// 断言验证
|
||||
expect(allMenuItems.length).toBeGreaterThan(49); // 应该比49个更多
|
||||
expect(foundNewMenus.length).toBeGreaterThanOrEqual(5); // 至少找到5个新菜单
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/menu-validation-full.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log('✅ 菜单验证测试完成');
|
||||
});
|
||||
|
||||
test('详细菜单结构分析', async () => {
|
||||
// 等待页面加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('🔍 开始详细菜单结构分析...');
|
||||
|
||||
// 获取菜单结构
|
||||
const menuStructure = await page.evaluate(() => {
|
||||
const getMenuStructure = (element: Element, level = 0): any => {
|
||||
const result: any = {};
|
||||
|
||||
if (element.classList.contains('ant-menu-submenu')) {
|
||||
const titleElement = element.querySelector('.ant-menu-submenu-title');
|
||||
const title = titleElement?.textContent?.trim() || '';
|
||||
|
||||
result.title = title;
|
||||
result.type = 'submenu';
|
||||
result.level = level;
|
||||
result.children = [];
|
||||
|
||||
const childMenu = element.querySelector('.ant-menu-sub');
|
||||
if (childMenu) {
|
||||
const childItems = childMenu.children;
|
||||
for (let i = 0; i < childItems.length; i++) {
|
||||
const child = childItems[i];
|
||||
if (
|
||||
child.classList.contains('ant-menu-item') ||
|
||||
child.classList.contains('ant-menu-submenu')
|
||||
) {
|
||||
result.children.push(getMenuStructure(child, level + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element.classList.contains('ant-menu-item')) {
|
||||
const title = element.textContent?.trim() || '';
|
||||
result.title = title;
|
||||
result.type = 'item';
|
||||
result.level = level;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const menuContainer = document.querySelector('.ant-menu-root');
|
||||
const structure: any[] = [];
|
||||
|
||||
if (menuContainer) {
|
||||
const topLevelItems = menuContainer.children;
|
||||
for (let i = 0; i < topLevelItems.length; i++) {
|
||||
const item = topLevelItems[i];
|
||||
if (
|
||||
item.classList.contains('ant-menu-item') ||
|
||||
item.classList.contains('ant-menu-submenu')
|
||||
) {
|
||||
structure.push(getMenuStructure(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return structure;
|
||||
});
|
||||
|
||||
// 打印菜单结构
|
||||
const printMenuStructure = (items: any[], indent = '') => {
|
||||
items.forEach((item) => {
|
||||
console.log(
|
||||
`${indent}${item.type === 'submenu' ? '📁' : '📄'} ${item.title}`,
|
||||
);
|
||||
if (item.children && item.children.length > 0) {
|
||||
printMenuStructure(item.children, indent + ' ');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
console.log('\n📋 完整菜单结构:');
|
||||
printMenuStructure(menuStructure);
|
||||
|
||||
// 统计菜单数量
|
||||
const countMenuItems = (items: any[]): number => {
|
||||
let count = 0;
|
||||
items.forEach((item) => {
|
||||
count += 1;
|
||||
if (item.children && item.children.length > 0) {
|
||||
count += countMenuItems(item.children);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const totalMenuCount = countMenuItems(menuStructure);
|
||||
console.log(`\n📊 菜单结构统计: ${totalMenuCount} 个菜单项`);
|
||||
|
||||
// 验证特定菜单的存在
|
||||
const findMenuInStructure = (
|
||||
items: any[],
|
||||
searchTitle: string,
|
||||
): boolean => {
|
||||
return items.some((item) => {
|
||||
if (item.title.includes(searchTitle)) {
|
||||
return true;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
return findMenuInStructure(item.children, searchTitle);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const menuChecks = [
|
||||
'工具箱',
|
||||
'文件上传',
|
||||
'Excel导入导出',
|
||||
'WebSocket调试',
|
||||
'帮助中心',
|
||||
'系统文档',
|
||||
'权限示例',
|
||||
];
|
||||
|
||||
console.log('\n🔍 菜单存在性检查:');
|
||||
menuChecks.forEach((menuName) => {
|
||||
const exists = findMenuInStructure(menuStructure, menuName);
|
||||
console.log(
|
||||
`${exists ? '✅' : '❌'} ${menuName}: ${exists ? '存在' : '不存在'}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
592
frontend-vben/apps/web-antd/tests/e2e/permission-control.test.ts
Normal file
592
frontend-vben/apps/web-antd/tests/e2e/permission-control.test.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* 权限控制功能端到端测试
|
||||
* 测试权限管理、角色管理、权限验证等功能
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
superAdmin: {
|
||||
username: 'super_admin',
|
||||
password: 'super123456',
|
||||
},
|
||||
testUser: {
|
||||
username: 'test_user',
|
||||
password: 'test123456',
|
||||
},
|
||||
};
|
||||
|
||||
// 测试权限数据
|
||||
const TEST_PERMISSION = {
|
||||
name: '测试权限_' + Date.now(),
|
||||
code: 'test:permission:' + Date.now(),
|
||||
type: 'button',
|
||||
parentId: null,
|
||||
description: '这是一个测试权限',
|
||||
resource: '/test/resource',
|
||||
actions: ['view', 'create'],
|
||||
};
|
||||
|
||||
// 测试角色数据
|
||||
const TEST_ROLE = {
|
||||
name: '测试角色_' + Date.now(),
|
||||
code: 'test_role_' + Date.now(),
|
||||
description: '这是一个测试角色',
|
||||
status: 'active',
|
||||
level: 2,
|
||||
};
|
||||
|
||||
test.describe('权限管理功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsSuperAdmin(page);
|
||||
|
||||
// 导航到权限管理页面
|
||||
await page.click('[data-testid="menu-system"]');
|
||||
await page.click('[data-testid="menu-permission-management"]');
|
||||
await page.waitForURL(/\/system\/permission/);
|
||||
});
|
||||
|
||||
test('应该正确显示权限管理页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/权限管理|Permission Management/,
|
||||
);
|
||||
|
||||
// 检查权限树结构
|
||||
await expect(page.locator('.permission-tree')).toBeVisible();
|
||||
await expect(page.locator('.ant-tree')).toBeVisible();
|
||||
|
||||
// 检查操作按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="add-permission-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="expand-all-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="collapse-all-button"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查权限详情面板
|
||||
await expect(page.locator('.permission-details')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能创建新权限', async ({ page }) => {
|
||||
// 点击添加权限按钮
|
||||
await page.click('[data-testid="add-permission-button"]');
|
||||
|
||||
// 等待创建弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/添加权限|新增权限/,
|
||||
);
|
||||
|
||||
// 填写权限信息
|
||||
await page.fill('[data-testid="form-name"]', TEST_PERMISSION.name);
|
||||
await page.fill('[data-testid="form-code"]', TEST_PERMISSION.code);
|
||||
await page.selectOption('[data-testid="form-type"]', TEST_PERMISSION.type);
|
||||
await page.fill(
|
||||
'[data-testid="form-description"]',
|
||||
TEST_PERMISSION.description,
|
||||
);
|
||||
await page.fill('[data-testid="form-resource"]', TEST_PERMISSION.resource);
|
||||
|
||||
// 选择操作权限
|
||||
for (const action of TEST_PERMISSION.actions) {
|
||||
await page.check(`[data-testid="action-${action}"]`);
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/创建成功|添加成功/,
|
||||
);
|
||||
|
||||
// 弹窗应该关闭
|
||||
await expect(page.locator('.ant-modal')).not.toBeVisible();
|
||||
|
||||
// 在权限树中应该能找到新创建的权限
|
||||
await expect(
|
||||
page
|
||||
.locator('.ant-tree-node-content-wrapper')
|
||||
.filter({ hasText: TEST_PERMISSION.name }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('权限树展开折叠功能应该正常工作', async ({ page }) => {
|
||||
// 点击展开所有
|
||||
await page.click('[data-testid="expand-all-button"]');
|
||||
|
||||
// 等待展开完成
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否有展开的节点
|
||||
const expandedNodes = page.locator('.ant-tree-switcher-open');
|
||||
expect(await expandedNodes.count()).toBeGreaterThan(0);
|
||||
|
||||
// 点击折叠所有
|
||||
await page.click('[data-testid="collapse-all-button"]');
|
||||
|
||||
// 等待折叠完成
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否所有节点都被折叠
|
||||
const collapsedNodes = page.locator('.ant-tree-switcher-close');
|
||||
expect(await collapsedNodes.count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('权限详情查看应该正常工作', async ({ page }) => {
|
||||
// 点击权限树中的一个节点
|
||||
const firstPermissionNode = page
|
||||
.locator('.ant-tree-node-content-wrapper')
|
||||
.first();
|
||||
await firstPermissionNode.click();
|
||||
|
||||
// 检查权限详情面板是否更新
|
||||
await expect(page.locator('.permission-details')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="detail-name"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="detail-code"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="detail-type"]')).toBeVisible();
|
||||
|
||||
// 检查操作按钮
|
||||
await expect(page.locator('[data-testid="edit-permission"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="delete-permission"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能编辑权限信息', async ({ page }) => {
|
||||
// 点击刚创建的权限节点
|
||||
const testPermissionNode = page
|
||||
.locator('.ant-tree-node-content-wrapper')
|
||||
.filter({ hasText: TEST_PERMISSION.name });
|
||||
await testPermissionNode.click();
|
||||
|
||||
// 点击编辑按钮
|
||||
await page.click('[data-testid="edit-permission"]');
|
||||
|
||||
// 等待编辑弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/编辑权限|修改权限/,
|
||||
);
|
||||
|
||||
// 修改描述
|
||||
const newDescription = '已修改的测试权限描述';
|
||||
await page.fill('[data-testid="form-description"]', newDescription);
|
||||
|
||||
// 提交修改
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 验证修改是否生效
|
||||
await testPermissionNode.click();
|
||||
await expect(
|
||||
page.locator('[data-testid="detail-description"]'),
|
||||
).toContainText(newDescription);
|
||||
});
|
||||
|
||||
test('权限搜索功能应该正常工作', async ({ page }) => {
|
||||
// 输入搜索关键词
|
||||
await page.fill('[data-testid="permission-search"]', TEST_PERMISSION.name);
|
||||
|
||||
// 等待搜索结果
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查搜索结果
|
||||
const visibleNodes = page.locator('.ant-tree-node-content-wrapper:visible');
|
||||
const searchResults = visibleNodes.filter({
|
||||
hasText: TEST_PERMISSION.name,
|
||||
});
|
||||
await expect(searchResults).toHaveCount({ min: 1 });
|
||||
|
||||
// 清空搜索
|
||||
await page.fill('[data-testid="permission-search"]', '');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否显示所有权限
|
||||
expect(await visibleNodes.count()).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test('应该能删除权限', async ({ page }) => {
|
||||
// 点击要删除的权限节点
|
||||
const testPermissionNode = page
|
||||
.locator('.ant-tree-node-content-wrapper')
|
||||
.filter({ hasText: TEST_PERMISSION.name });
|
||||
await testPermissionNode.click();
|
||||
|
||||
// 点击删除按钮
|
||||
await page.click('[data-testid="delete-permission"]');
|
||||
|
||||
// 确认删除
|
||||
await expect(page.locator('.ant-modal-confirm')).toBeVisible();
|
||||
await page.click('.ant-btn-primary');
|
||||
|
||||
// 等待删除成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 验证权限已被删除
|
||||
await expect(testPermissionNode).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('角色管理功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsSuperAdmin(page);
|
||||
|
||||
// 导航到角色管理页面
|
||||
await page.click('[data-testid="menu-system"]');
|
||||
await page.click('[data-testid="menu-role-management"]');
|
||||
await page.waitForURL(/\/system\/role/);
|
||||
});
|
||||
|
||||
test('应该正确显示角色管理页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/角色管理|Role Management/,
|
||||
);
|
||||
|
||||
// 检查搜索表单
|
||||
await expect(page.locator('[data-testid="search-name"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-status"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-button"]')).toBeVisible();
|
||||
|
||||
// 检查操作按钮
|
||||
await expect(page.locator('[data-testid="add-role-button"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="batch-delete-button"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查角色列表表格
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能创建新角色', async ({ page }) => {
|
||||
// 点击添加角色按钮
|
||||
await page.click('[data-testid="add-role-button"]');
|
||||
|
||||
// 等待创建弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/添加角色|新增角色/,
|
||||
);
|
||||
|
||||
// 填写角色信息
|
||||
await page.fill('[data-testid="form-name"]', TEST_ROLE.name);
|
||||
await page.fill('[data-testid="form-code"]', TEST_ROLE.code);
|
||||
await page.fill('[data-testid="form-description"]', TEST_ROLE.description);
|
||||
await page.selectOption('[data-testid="form-status"]', TEST_ROLE.status);
|
||||
await page.fill('[data-testid="form-level"]', TEST_ROLE.level.toString());
|
||||
|
||||
// 提交表单
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 在列表中搜索新创建的角色
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const roleRow = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ROLE.name });
|
||||
await expect(roleRow).toBeVisible();
|
||||
});
|
||||
|
||||
test('角色权限分配应该正常工作', async ({ page }) => {
|
||||
// 搜索刚创建的角色
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击权限分配按钮
|
||||
const assignButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ROLE.name })
|
||||
.locator('[data-testid="assign-permissions"]');
|
||||
await assignButton.click();
|
||||
|
||||
// 等待权限分配弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/权限分配|分配权限/,
|
||||
);
|
||||
|
||||
// 检查权限树
|
||||
await expect(page.locator('.permission-tree')).toBeVisible();
|
||||
|
||||
// 选择一些权限
|
||||
const permissionCheckboxes = page.locator('.ant-tree-checkbox');
|
||||
await permissionCheckboxes.nth(0).click();
|
||||
await permissionCheckboxes.nth(1).click();
|
||||
|
||||
// 保存权限分配
|
||||
await page.click('[data-testid="save-permissions"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('角色状态切换应该正常工作', async ({ page }) => {
|
||||
// 搜索角色
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const roleRow = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ROLE.name });
|
||||
|
||||
// 点击状态切换开关
|
||||
const statusSwitch = roleRow.locator('[data-testid="status-switch"]');
|
||||
await statusSwitch.click();
|
||||
|
||||
// 确认状态变更
|
||||
await page.click('[data-testid="confirm-status-change"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('角色复制功能应该正常工作', async ({ page }) => {
|
||||
// 搜索角色
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击复制按钮
|
||||
const copyButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ROLE.name })
|
||||
.locator('[data-testid="copy-role"]');
|
||||
await copyButton.click();
|
||||
|
||||
// 等待复制弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/复制角色|克隆角色/,
|
||||
);
|
||||
|
||||
// 修改角色名称和编码
|
||||
const newName = '复制的测试角色_' + Date.now();
|
||||
const newCode = 'copied_role_' + Date.now();
|
||||
await page.fill('[data-testid="form-name"]', newName);
|
||||
await page.fill('[data-testid="form-code"]', newCode);
|
||||
|
||||
// 确认复制
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 验证复制的角色出现在列表中
|
||||
await page.fill('[data-testid="search-name"]', newName);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(
|
||||
page.locator('.ant-table-tbody tr').filter({ hasText: newName }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能删除角色', async ({ page }) => {
|
||||
// 搜索要删除的角色
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击删除按钮
|
||||
const deleteButton = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_ROLE.name })
|
||||
.locator('[data-testid="delete-button"]');
|
||||
await deleteButton.click();
|
||||
|
||||
// 确认删除
|
||||
await expect(page.locator('.ant-modal-confirm')).toBeVisible();
|
||||
await page.click('.ant-btn-primary');
|
||||
|
||||
// 等待删除成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 验证角色已被删除
|
||||
await page.reload();
|
||||
await page.fill('[data-testid="search-name"]', TEST_ROLE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('.ant-empty')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('权限验证功能测试', () => {
|
||||
test('路由权限验证应该正常工作', async ({ page }) => {
|
||||
// 使用普通用户登录
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 尝试访问系统管理页面(假设普通用户没有权限)
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/system/permission`);
|
||||
|
||||
// 应该显示403错误页面或被重定向
|
||||
await expect(page.locator('.ant-result-403, .error-403')).toBeVisible();
|
||||
|
||||
// 或者检查是否被重定向到无权限页面
|
||||
await expect(page.url()).toMatch(/\/403|\/unauthorized/);
|
||||
});
|
||||
|
||||
test('菜单权限过滤应该正常工作', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 检查系统管理菜单是否被隐藏
|
||||
await expect(page.locator('[data-testid="menu-system"]')).not.toBeVisible();
|
||||
|
||||
// 检查用户有权限的菜单是否显示
|
||||
await expect(page.locator('[data-testid="menu-dashboard"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="menu-account"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('按钮权限控制应该正常工作', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 访问账号列表页面
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/account/list`);
|
||||
|
||||
// 检查删除按钮是否被隐藏(假设普通用户没有删除权限)
|
||||
await expect(
|
||||
page.locator('[data-testid="delete-button"]'),
|
||||
).not.toBeVisible();
|
||||
|
||||
// 但查看按钮应该可见
|
||||
await expect(page.locator('[data-testid="view-button"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('API权限验证应该正常工作', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 监听网络请求
|
||||
page.on('response', async (response) => {
|
||||
if (
|
||||
response.url().includes('/api/system/permission') &&
|
||||
response.request().method() === 'POST'
|
||||
) {
|
||||
// 检查是否返回403权限不足错误
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试通过开发者工具发送受限API请求
|
||||
await page.evaluate(() => {
|
||||
fetch('/api/system/permission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
},
|
||||
body: JSON.stringify({ name: 'test', code: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
// 等待请求完成
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test('权限缓存和实时更新应该正常工作', async ({ page }) => {
|
||||
// 使用超级管理员登录
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsSuperAdmin(page);
|
||||
|
||||
// 创建一个测试用户并分配有限权限
|
||||
await page.click('[data-testid="menu-system"]');
|
||||
await page.click('[data-testid="menu-user-management"]');
|
||||
|
||||
// 找到测试用户并修改其权限
|
||||
const testUserRow = page
|
||||
.locator('.ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_CONFIG.testUser.username });
|
||||
if ((await testUserRow.count()) > 0) {
|
||||
await testUserRow.locator('[data-testid="assign-role"]').click();
|
||||
|
||||
// 分配新的角色权限
|
||||
await page.check('[data-testid="role-admin"]');
|
||||
await page.click('[data-testid="save-user-roles"]');
|
||||
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
}
|
||||
|
||||
// 切换到测试用户账号(模拟用户刷新或重新登录)
|
||||
await page.click('[data-testid="user-dropdown"]');
|
||||
await page.click('[data-testid="logout-button"]');
|
||||
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 检查权限是否已更新
|
||||
await expect(page.locator('[data-testid="menu-system"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('权限指令v-permission应该正常工作', async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// 访问有使用v-permission指令的页面
|
||||
await page.goto(`${TEST_CONFIG.baseURL}/account/list`);
|
||||
|
||||
// 检查使用v-permission指令的元素是否按权限显示/隐藏
|
||||
const createButton = page.locator('[v-permission="account:create"]');
|
||||
const viewButton = page.locator('[v-permission="account:view"]');
|
||||
|
||||
if ((await createButton.count()) > 0) {
|
||||
// 如果用户没有创建权限,按钮应该被隐藏
|
||||
await expect(createButton).not.toBeVisible();
|
||||
}
|
||||
|
||||
if ((await viewButton.count()) > 0) {
|
||||
// 如果用户有查看权限,按钮应该可见
|
||||
await expect(viewButton).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:超级管理员登录
|
||||
async function loginAsSuperAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.superAdmin.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.superAdmin.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
|
||||
// 辅助函数:测试用户登录
|
||||
async function loginAsTestUser(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.testUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.testUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
565
frontend-vben/apps/web-antd/tests/e2e/private-message.test.ts
Normal file
565
frontend-vben/apps/web-antd/tests/e2e/private-message.test.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* 私信群发功能端到端测试
|
||||
* 测试私信模板、发送记录、任务管理等功能
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
adminUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
};
|
||||
|
||||
// 测试消息模板数据
|
||||
const TEST_TEMPLATE = {
|
||||
name: '测试模板_' + Date.now(),
|
||||
title: '测试消息标题',
|
||||
content: '这是一条测试私信内容,用于验证系统功能。',
|
||||
type: 'text',
|
||||
category: 'marketing',
|
||||
variables: '${username},${date}',
|
||||
};
|
||||
|
||||
// 测试群发任务数据
|
||||
const TEST_TASK = {
|
||||
name: '测试群发任务_' + Date.now(),
|
||||
targetType: 'all',
|
||||
sendTime: 'immediate',
|
||||
priority: 'normal',
|
||||
};
|
||||
|
||||
test.describe('私信群发 - 消息模板管理', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到消息模板页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-message-template"]');
|
||||
await page.waitForURL(/\/private-message\/template/);
|
||||
});
|
||||
|
||||
test('应该正确显示消息模板页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/消息模板|模板管理/,
|
||||
);
|
||||
|
||||
// 检查搜索表单
|
||||
await expect(page.locator('[data-testid="search-name"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-type"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-button"]')).toBeVisible();
|
||||
|
||||
// 检查操作按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="add-template-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="batch-delete-button"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查模板列表
|
||||
await expect(page.locator('.template-card, .ant-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能创建新的消息模板', async ({ page }) => {
|
||||
// 点击添加模板按钮
|
||||
await page.click('[data-testid="add-template-button"]');
|
||||
|
||||
// 等待弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/添加模板|新建模板/,
|
||||
);
|
||||
|
||||
// 填写模板信息
|
||||
await page.fill('[data-testid="form-name"]', TEST_TEMPLATE.name);
|
||||
await page.fill('[data-testid="form-title"]', TEST_TEMPLATE.title);
|
||||
await page.fill('[data-testid="form-content"]', TEST_TEMPLATE.content);
|
||||
await page.selectOption('[data-testid="form-type"]', TEST_TEMPLATE.type);
|
||||
await page.selectOption(
|
||||
'[data-testid="form-category"]',
|
||||
TEST_TEMPLATE.category,
|
||||
);
|
||||
await page.fill('[data-testid="form-variables"]', TEST_TEMPLATE.variables);
|
||||
|
||||
// 提交表单
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/创建成功|添加成功/,
|
||||
);
|
||||
|
||||
// 验证模板是否出现在列表中
|
||||
await page.fill('[data-testid="search-name"]', TEST_TEMPLATE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const templateCard = page
|
||||
.locator('.template-card, .ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_TEMPLATE.name });
|
||||
await expect(templateCard).toBeVisible();
|
||||
});
|
||||
|
||||
test('消息模板预览功能应该正常工作', async ({ page }) => {
|
||||
// 搜索刚创建的模板
|
||||
await page.fill('[data-testid="search-name"]', TEST_TEMPLATE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击预览按钮
|
||||
const previewButton = page
|
||||
.locator('.template-card, .ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_TEMPLATE.name })
|
||||
.locator('[data-testid="preview-button"]');
|
||||
await previewButton.click();
|
||||
|
||||
// 等待预览弹窗出现
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/模板预览|消息预览/,
|
||||
);
|
||||
|
||||
// 检查预览内容
|
||||
await expect(page.locator('[data-testid="preview-title"]')).toContainText(
|
||||
TEST_TEMPLATE.title,
|
||||
);
|
||||
await expect(page.locator('[data-testid="preview-content"]')).toContainText(
|
||||
TEST_TEMPLATE.content,
|
||||
);
|
||||
|
||||
// 关闭预览
|
||||
await page.click('.ant-modal-close');
|
||||
});
|
||||
|
||||
test('应该能编辑消息模板', async ({ page }) => {
|
||||
// 搜索模板
|
||||
await page.fill('[data-testid="search-name"]', TEST_TEMPLATE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击编辑按钮
|
||||
const editButton = page
|
||||
.locator('.template-card, .ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_TEMPLATE.name })
|
||||
.locator('[data-testid="edit-button"]');
|
||||
await editButton.click();
|
||||
|
||||
// 等待编辑弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/编辑模板|修改模板/,
|
||||
);
|
||||
|
||||
// 修改内容
|
||||
const newContent = '已修改的测试私信内容';
|
||||
await page.fill('[data-testid="form-content"]', newContent);
|
||||
|
||||
// 提交修改
|
||||
await page.click('[data-testid="form-submit"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
});
|
||||
|
||||
test('变量替换功能应该正常工作', async ({ page }) => {
|
||||
// 搜索模板
|
||||
await page.fill('[data-testid="search-name"]', TEST_TEMPLATE.name);
|
||||
await page.click('[data-testid="search-button"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击测试变量按钮
|
||||
const testButton = page
|
||||
.locator('.template-card, .ant-table-tbody tr')
|
||||
.filter({ hasText: TEST_TEMPLATE.name })
|
||||
.locator('[data-testid="test-variables-button"]');
|
||||
await testButton.click();
|
||||
|
||||
// 等待测试弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/变量测试|测试变量/,
|
||||
);
|
||||
|
||||
// 输入测试数据
|
||||
await page.fill('[data-testid="test-username"]', '张三');
|
||||
await page.fill('[data-testid="test-date"]', '2024-01-01');
|
||||
|
||||
// 点击预览
|
||||
await page.click('[data-testid="preview-with-variables"]');
|
||||
|
||||
// 检查变量是否被正确替换
|
||||
await expect(page.locator('[data-testid="preview-result"]')).toContainText(
|
||||
'张三',
|
||||
);
|
||||
await expect(page.locator('[data-testid="preview-result"]')).toContainText(
|
||||
'2024-01-01',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('私信群发 - 群发任务管理', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到群发任务页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-send-task"]');
|
||||
await page.waitForURL(/\/private-message\/task/);
|
||||
});
|
||||
|
||||
test('应该正确显示群发任务页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/群发任务|发送任务/,
|
||||
);
|
||||
|
||||
// 检查创建任务按钮
|
||||
await expect(
|
||||
page.locator('[data-testid="create-task-button"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查任务列表
|
||||
await expect(page.locator('.task-list, .ant-table')).toBeVisible();
|
||||
|
||||
// 检查筛选器
|
||||
await expect(page.locator('[data-testid="filter-status"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="filter-priority"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能创建新的群发任务', async ({ page }) => {
|
||||
// 点击创建任务按钮
|
||||
await page.click('[data-testid="create-task-button"]');
|
||||
|
||||
// 等待任务创建页面加载
|
||||
await page.waitForURL(/\/private-message\/task\/create/);
|
||||
|
||||
// 第一步:基本信息
|
||||
await page.fill('[data-testid="task-name"]', TEST_TASK.name);
|
||||
await page.selectOption(
|
||||
'[data-testid="task-priority"]',
|
||||
TEST_TASK.priority,
|
||||
);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第二步:选择模板
|
||||
await page.click('[data-testid="template-selector"]');
|
||||
await page.click('.template-option:first-child');
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第三步:选择目标用户
|
||||
await page.click(`[data-testid="target-${TEST_TASK.targetType}"]`);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第四步:发送设置
|
||||
await page.click(`[data-testid="send-${TEST_TASK.sendTime}"]`);
|
||||
await page.click('[data-testid="next-step"]');
|
||||
|
||||
// 第五步:确认并创建
|
||||
await expect(page.locator('.task-summary')).toBeVisible();
|
||||
await page.click('[data-testid="create-task"]');
|
||||
|
||||
// 等待成功消息
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
await expect(page.locator('.ant-message-success')).toContainText(
|
||||
/任务创建成功|创建成功/,
|
||||
);
|
||||
|
||||
// 应该跳转回任务列表
|
||||
await page.waitForURL(/\/private-message\/task$/);
|
||||
});
|
||||
|
||||
test('任务状态管理应该正常工作', async ({ page }) => {
|
||||
// 找到一个待发送的任务
|
||||
const taskRow = page
|
||||
.locator('.task-item, .ant-table-tbody tr')
|
||||
.filter({ hasText: /待发送|pending/ })
|
||||
.first();
|
||||
|
||||
if ((await taskRow.count()) > 0) {
|
||||
// 点击开始发送按钮
|
||||
await taskRow.locator('[data-testid="start-task"]').click();
|
||||
|
||||
// 确认操作
|
||||
await page.click('[data-testid="confirm-start"]');
|
||||
|
||||
// 等待状态更新
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 检查任务状态是否变为发送中
|
||||
await page.reload();
|
||||
await expect(taskRow.locator('.task-status')).toContainText(
|
||||
/发送中|sending/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('任务详情页面应该正常显示', async ({ page }) => {
|
||||
// 点击第一个任务的详情按钮
|
||||
const detailButton = page
|
||||
.locator('.task-item, .ant-table-tbody tr')
|
||||
.first()
|
||||
.locator('[data-testid="view-detail"]');
|
||||
await detailButton.click();
|
||||
|
||||
// 等待详情页面加载
|
||||
await page.waitForURL(/\/private-message\/task\/\d+/);
|
||||
|
||||
// 检查任务基本信息
|
||||
await expect(page.locator('.task-info')).toBeVisible();
|
||||
await expect(page.locator('.task-progress')).toBeVisible();
|
||||
|
||||
// 检查发送记录表格
|
||||
await expect(page.locator('.send-records .ant-table')).toBeVisible();
|
||||
|
||||
// 检查统计信息
|
||||
await expect(page.locator('.task-statistics')).toBeVisible();
|
||||
});
|
||||
|
||||
test('任务暂停和恢复功能应该正常工作', async ({ page }) => {
|
||||
// 找到一个正在发送的任务
|
||||
const sendingTask = page
|
||||
.locator('.task-item, .ant-table-tbody tr')
|
||||
.filter({ hasText: /发送中|sending/ })
|
||||
.first();
|
||||
|
||||
if ((await sendingTask.count()) > 0) {
|
||||
// 点击暂停按钮
|
||||
await sendingTask.locator('[data-testid="pause-task"]').click();
|
||||
|
||||
// 确认暂停
|
||||
await page.click('[data-testid="confirm-pause"]');
|
||||
|
||||
// 等待状态更新
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
|
||||
// 检查状态是否变为已暂停
|
||||
await page.reload();
|
||||
await expect(sendingTask.locator('.task-status')).toContainText(
|
||||
/已暂停|paused/,
|
||||
);
|
||||
|
||||
// 测试恢复功能
|
||||
await sendingTask.locator('[data-testid="resume-task"]').click();
|
||||
await page.click('[data-testid="confirm-resume"]');
|
||||
|
||||
// 等待状态更新
|
||||
await expect(page.locator('.ant-message-success')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('私信群发 - 发送记录和统计', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到发送记录页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-send-record"]');
|
||||
await page.waitForURL(/\/private-message\/record/);
|
||||
});
|
||||
|
||||
test('应该正确显示发送记录页面', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/发送记录|消息记录/,
|
||||
);
|
||||
|
||||
// 检查搜索表单
|
||||
await expect(page.locator('[data-testid="search-keyword"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-status"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="search-date-range"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 检查记录列表
|
||||
await expect(page.locator('.ant-table')).toBeVisible();
|
||||
|
||||
// 检查统计卡片
|
||||
await expect(page.locator('.statistics-cards')).toBeVisible();
|
||||
});
|
||||
|
||||
test('记录搜索和筛选应该正常工作', async ({ page }) => {
|
||||
// 选择发送状态
|
||||
await page.selectOption('[data-testid="search-status"]', 'success');
|
||||
|
||||
// 选择日期范围
|
||||
await page.click('[data-testid="search-date-range"]');
|
||||
await page.click('.ant-picker-today-btn'); // 今天
|
||||
|
||||
// 点击搜索
|
||||
await page.click('[data-testid="search-button"]');
|
||||
|
||||
// 等待搜索结果
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查搜索结果
|
||||
const records = page.locator('.ant-table-tbody tr');
|
||||
if ((await records.count()) > 0) {
|
||||
// 验证搜索结果状态是否正确
|
||||
const statusCells = records
|
||||
.locator('td')
|
||||
.filter({ hasText: /成功|success/ });
|
||||
expect(await statusCells.count()).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('记录详情查看应该正常工作', async ({ page }) => {
|
||||
// 点击第一条记录的详情按钮
|
||||
const firstRecord = page.locator('.ant-table-tbody tr').first();
|
||||
const detailButton = firstRecord.locator('[data-testid="view-detail"]');
|
||||
|
||||
if ((await detailButton.count()) > 0) {
|
||||
await detailButton.click();
|
||||
|
||||
// 等待详情弹窗
|
||||
await expect(page.locator('.ant-modal')).toBeVisible();
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
/消息详情|发送详情/,
|
||||
);
|
||||
|
||||
// 检查详情内容
|
||||
await expect(
|
||||
page.locator('[data-testid="detail-recipient"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="detail-content"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="detail-send-time"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="detail-status"]')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('记录导出功能应该正常工作', async ({ page }) => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// 点击导出按钮
|
||||
await page.click('[data-testid="export-records"]');
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/发送记录.*\.xlsx$/);
|
||||
});
|
||||
|
||||
test('统计图表应该正常显示', async ({ page }) => {
|
||||
// 导航到统计页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-statistics"]');
|
||||
await page.waitForURL(/\/private-message\/statistics/);
|
||||
|
||||
// 检查统计图表
|
||||
await expect(page.locator('.statistics-chart')).toBeVisible();
|
||||
await expect(page.locator('.echarts-container')).toBeVisible();
|
||||
|
||||
// 检查时间范围选择器
|
||||
await expect(
|
||||
page.locator('[data-testid="time-range-selector"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// 测试时间范围切换
|
||||
await page.click('[data-testid="range-7days"]');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 图表应该重新加载
|
||||
await expect(page.locator('.statistics-chart')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('私信群发 - 实时监控', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到实时监控页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-monitor"]');
|
||||
await page.waitForURL(/\/private-message\/monitor/);
|
||||
});
|
||||
|
||||
test('实时监控页面应该正常显示', async ({ page }) => {
|
||||
// 检查页面标题
|
||||
await expect(page.locator('.page-title')).toContainText(
|
||||
/实时监控|发送监控/,
|
||||
);
|
||||
|
||||
// 检查实时状态卡片
|
||||
await expect(page.locator('.status-cards')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="active-tasks-count"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="sending-rate"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="success-rate"]')).toBeVisible();
|
||||
|
||||
// 检查实时日志
|
||||
await expect(page.locator('.realtime-logs')).toBeVisible();
|
||||
|
||||
// 检查性能图表
|
||||
await expect(page.locator('.performance-chart')).toBeVisible();
|
||||
});
|
||||
|
||||
test('实时数据更新应该正常工作', async ({ page }) => {
|
||||
// 记录初始的发送数量
|
||||
const initialCount = await page
|
||||
.locator('[data-testid="sent-count"]')
|
||||
.textContent();
|
||||
|
||||
// 等待数据更新(假设有WebSocket连接)
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查数据是否可能发生变化(在有实际发送任务的情况下)
|
||||
const currentCount = await page
|
||||
.locator('[data-testid="sent-count"]')
|
||||
.textContent();
|
||||
|
||||
// 至少UI应该是响应的
|
||||
await expect(page.locator('[data-testid="sent-count"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('日志查看和筛选应该正常工作', async ({ page }) => {
|
||||
// 检查日志级别筛选
|
||||
await page.selectOption('[data-testid="log-level-filter"]', 'error');
|
||||
|
||||
// 等待筛选结果
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查日志内容
|
||||
const logEntries = page.locator('.log-entry');
|
||||
if ((await logEntries.count()) > 0) {
|
||||
// 验证筛选结果
|
||||
const errorLogs = logEntries.filter({ hasText: /错误|error/i });
|
||||
expect(await errorLogs.count()).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// 测试清空日志功能
|
||||
await page.click('[data-testid="clear-logs"]');
|
||||
await page.click('[data-testid="confirm-clear"]');
|
||||
|
||||
// 日志应该被清空
|
||||
await expect(page.locator('.log-entry')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:管理员登录
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.adminUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.adminUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
529
frontend-vben/apps/web-antd/tests/e2e/responsive-layout.test.ts
Normal file
529
frontend-vben/apps/web-antd/tests/e2e/responsive-layout.test.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
/**
|
||||
* 响应式布局端到端测试
|
||||
* 测试不同屏幕尺寸下的布局适配
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
timeout: 30000,
|
||||
adminUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
// 不同设备视口尺寸
|
||||
viewports: {
|
||||
mobile: { width: 375, height: 667 }, // iPhone SE
|
||||
tablet: { width: 768, height: 1024 }, // iPad
|
||||
desktop: { width: 1280, height: 720 }, // 桌面
|
||||
largeDesktop: { width: 1920, height: 1080 }, // 大屏桌面
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('桌面端响应式测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
// 设置桌面端视口
|
||||
await page.setViewportSize(TEST_CONFIG.viewports.desktop);
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('桌面端布局应该正确显示', async ({ page }) => {
|
||||
// 检查侧边栏是否完全展开
|
||||
const sidebar = page.locator('.layout-sidebar');
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(sidebar).toHaveClass(/expanded|full/);
|
||||
|
||||
// 检查侧边栏菜单项是否显示文字
|
||||
const menuItem = page.locator('.menu-item').first();
|
||||
await expect(menuItem.locator('.menu-title')).toBeVisible();
|
||||
|
||||
// 检查主内容区域
|
||||
const mainContent = page.locator('.layout-content');
|
||||
await expect(mainContent).toBeVisible();
|
||||
|
||||
// 检查头部导航
|
||||
const header = page.locator('.layout-header');
|
||||
await expect(header).toBeVisible();
|
||||
await expect(header.locator('.breadcrumb')).toBeVisible();
|
||||
await expect(header.locator('.user-dropdown')).toBeVisible();
|
||||
});
|
||||
|
||||
test('侧边栏折叠展开功能应该正常工作', async ({ page }) => {
|
||||
const sidebar = page.locator('.layout-sidebar');
|
||||
const toggleButton = page.locator('[data-testid="sidebar-toggle"]');
|
||||
|
||||
// 点击折叠按钮
|
||||
await toggleButton.click();
|
||||
|
||||
// 等待动画完成
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 检查侧边栏是否折叠
|
||||
await expect(sidebar).toHaveClass(/collapsed|mini/);
|
||||
|
||||
// 检查菜单项文字是否隐藏
|
||||
const menuTitle = page.locator('.menu-item .menu-title').first();
|
||||
await expect(menuTitle).not.toBeVisible();
|
||||
|
||||
// 再次点击展开
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 检查侧边栏是否展开
|
||||
await expect(sidebar).toHaveClass(/expanded|full/);
|
||||
await expect(menuTitle).toBeVisible();
|
||||
});
|
||||
|
||||
test('桌面端表格应该显示所有列', async ({ page }) => {
|
||||
// 导航到账号列表页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查表格列数
|
||||
const tableHeaders = page.locator('.ant-table-thead th');
|
||||
const headerCount = await tableHeaders.count();
|
||||
|
||||
// 桌面端应该显示所有列(至少6列)
|
||||
expect(headerCount).toBeGreaterThanOrEqual(6);
|
||||
|
||||
// 检查操作列是否显示完整
|
||||
const actionColumn = page
|
||||
.locator('.ant-table-thead th')
|
||||
.filter({ hasText: /操作|Actions/ });
|
||||
await expect(actionColumn).toBeVisible();
|
||||
|
||||
// 检查操作按钮是否都可见
|
||||
const firstRow = page.locator('.ant-table-tbody tr').first();
|
||||
await expect(firstRow.locator('[data-testid="view-button"]')).toBeVisible();
|
||||
await expect(firstRow.locator('[data-testid="edit-button"]')).toBeVisible();
|
||||
await expect(
|
||||
firstRow.locator('[data-testid="delete-button"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('平板端响应式测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 设置平板端视口
|
||||
await page.setViewportSize(TEST_CONFIG.viewports.tablet);
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('平板端布局应该适配正确', async ({ page }) => {
|
||||
// 检查侧边栏状态
|
||||
const sidebar = page.locator('.layout-sidebar');
|
||||
await expect(sidebar).toBeVisible();
|
||||
|
||||
// 平板端侧边栏可能默认折叠
|
||||
const isCollapsed = await sidebar.evaluate(
|
||||
(el) =>
|
||||
el.classList.contains('collapsed') || el.classList.contains('mini'),
|
||||
);
|
||||
|
||||
if (isCollapsed) {
|
||||
// 检查折叠状态下的菜单
|
||||
const menuIcons = page.locator('.menu-item .menu-icon');
|
||||
await expect(menuIcons.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// 检查主内容区域是否合理布局
|
||||
const mainContent = page.locator('.layout-content');
|
||||
await expect(mainContent).toBeVisible();
|
||||
|
||||
// 检查头部导航是否适配
|
||||
const header = page.locator('.layout-header');
|
||||
await expect(header).toBeVisible();
|
||||
});
|
||||
|
||||
test('平板端表格应该隐藏次要列', async ({ page }) => {
|
||||
// 导航到账号列表页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查表格是否使用响应式设计
|
||||
const table = page.locator('.ant-table');
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
// 平板端可能隐藏一些次要列
|
||||
const visibleHeaders = page.locator('.ant-table-thead th:visible');
|
||||
const visibleHeaderCount = await visibleHeaders.count();
|
||||
|
||||
// 平板端显示的列数应该少于桌面端
|
||||
expect(visibleHeaderCount).toBeLessThan(8);
|
||||
expect(visibleHeaderCount).toBeGreaterThan(3);
|
||||
});
|
||||
|
||||
test('平板端搜索表单应该优化布局', async ({ page }) => {
|
||||
// 导航到有搜索表单的页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查搜索表单布局
|
||||
const searchForm = page.locator('.search-form');
|
||||
await expect(searchForm).toBeVisible();
|
||||
|
||||
// 平板端搜索表单可能采用不同的布局
|
||||
const formItems = page.locator('.ant-form-item');
|
||||
const itemCount = await formItems.count();
|
||||
|
||||
// 检查表单项是否合理排列
|
||||
expect(itemCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('手机端响应式测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 设置手机端视口
|
||||
await page.setViewportSize(TEST_CONFIG.viewports.mobile);
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('手机端应该显示移动端导航', async ({ page }) => {
|
||||
// 检查移动端汉堡菜单按钮
|
||||
const hamburgerButton = page.locator('[data-testid="mobile-menu-toggle"]');
|
||||
await expect(hamburgerButton).toBeVisible();
|
||||
|
||||
// 检查侧边栏是否隐藏
|
||||
const sidebar = page.locator('.layout-sidebar');
|
||||
const isHidden = await sidebar.evaluate(
|
||||
(el) =>
|
||||
getComputedStyle(el).display === 'none' ||
|
||||
el.classList.contains('mobile-hidden'),
|
||||
);
|
||||
|
||||
expect(isHidden).toBe(true);
|
||||
});
|
||||
|
||||
test('手机端侧边栏抽屉应该正常工作', async ({ page }) => {
|
||||
// 点击汉堡菜单按钮
|
||||
const hamburgerButton = page.locator('[data-testid="mobile-menu-toggle"]');
|
||||
await hamburgerButton.click();
|
||||
|
||||
// 等待抽屉动画
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 检查抽屉是否打开
|
||||
const drawer = page.locator('.ant-drawer, .mobile-sidebar-drawer');
|
||||
await expect(drawer).toBeVisible();
|
||||
|
||||
// 检查菜单项是否显示
|
||||
const menuItems = drawer.locator('.menu-item');
|
||||
await expect(menuItems.first()).toBeVisible();
|
||||
|
||||
// 点击遮罩层关闭抽屉
|
||||
const mask = page.locator('.ant-drawer-mask');
|
||||
if ((await mask.count()) > 0) {
|
||||
await mask.click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(drawer).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('手机端表格应该使用卡片布局', async ({ page }) => {
|
||||
// 导航到账号列表页面
|
||||
await page.click('[data-testid="mobile-menu-toggle"]');
|
||||
await page.waitForTimeout(300);
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 手机端可能使用卡片列表而不是表格
|
||||
const cardList = page.locator('.mobile-card-list, .account-cards');
|
||||
const table = page.locator('.ant-table');
|
||||
|
||||
// 检查是否使用了移动端适配的布局
|
||||
if ((await cardList.count()) > 0) {
|
||||
await expect(cardList).toBeVisible();
|
||||
|
||||
// 检查卡片项
|
||||
const cardItems = cardList.locator('.card-item');
|
||||
if ((await cardItems.count()) > 0) {
|
||||
await expect(cardItems.first()).toBeVisible();
|
||||
}
|
||||
} else {
|
||||
// 如果仍使用表格,检查是否为响应式表格
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
// 检查表格是否可以横向滚动
|
||||
const tableWrapper = page.locator('.ant-table-wrapper');
|
||||
const hasScroll = await tableWrapper.evaluate(
|
||||
(el) => el.scrollWidth > el.clientWidth,
|
||||
);
|
||||
|
||||
if (hasScroll) {
|
||||
// 表格应该可以滚动
|
||||
expect(hasScroll).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('手机端搜索应该使用折叠形式', async ({ page }) => {
|
||||
// 导航到有搜索功能的页面
|
||||
await page.click('[data-testid="mobile-menu-toggle"]');
|
||||
await page.waitForTimeout(300);
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查搜索按钮或搜索图标
|
||||
const searchTrigger = page.locator(
|
||||
'[data-testid="search-trigger"], [data-testid="mobile-search-button"]',
|
||||
);
|
||||
|
||||
if ((await searchTrigger.count()) > 0) {
|
||||
await expect(searchTrigger).toBeVisible();
|
||||
|
||||
// 点击展开搜索
|
||||
await searchTrigger.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 检查搜索表单是否展开
|
||||
const searchForm = page.locator('.search-form, .mobile-search-form');
|
||||
await expect(searchForm).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('手机端表单应该全宽显示', async ({ page }) => {
|
||||
// 导航到表单页面(如创建账号)
|
||||
await page.click('[data-testid="mobile-menu-toggle"]');
|
||||
await page.waitForTimeout(300);
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 点击添加按钮
|
||||
const addButton = page.locator('[data-testid="add-account-button"]');
|
||||
await addButton.click();
|
||||
|
||||
// 等待弹窗或新页面打开
|
||||
const modal = page.locator('.ant-modal');
|
||||
const formPage = page.locator('.form-page');
|
||||
|
||||
if ((await modal.count()) > 0) {
|
||||
// 如果是弹窗,检查弹窗是否适配移动端
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// 检查弹窗宽度是否适配
|
||||
const modalWidth = await modal.evaluate(
|
||||
(el) => getComputedStyle(el).width,
|
||||
);
|
||||
expect(modalWidth).toMatch(/100%|90%|95%/);
|
||||
} else if ((await formPage.count()) > 0) {
|
||||
// 如果是新页面,检查表单布局
|
||||
await expect(formPage).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('大屏幕响应式测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 设置大屏幕视口
|
||||
await page.setViewportSize(TEST_CONFIG.viewports.largeDesktop);
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('大屏幕应该显示更多内容', async ({ page }) => {
|
||||
// 检查布局是否充分利用屏幕空间
|
||||
const mainContent = page.locator('.layout-content');
|
||||
await expect(mainContent).toBeVisible();
|
||||
|
||||
// 检查内容区域宽度
|
||||
const contentWidth = await mainContent.evaluate((el) => el.offsetWidth);
|
||||
expect(contentWidth).toBeGreaterThan(1000);
|
||||
|
||||
// 检查是否有合理的最大宽度限制
|
||||
expect(contentWidth).toBeLessThan(1800);
|
||||
});
|
||||
|
||||
test('大屏幕表格应该显示所有列', async ({ page }) => {
|
||||
// 导航到表格页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查表格列数
|
||||
const tableHeaders = page.locator('.ant-table-thead th');
|
||||
const headerCount = await tableHeaders.count();
|
||||
|
||||
// 大屏幕应该显示最多的列
|
||||
expect(headerCount).toBeGreaterThanOrEqual(8);
|
||||
|
||||
// 检查表格是否有合理的宽度
|
||||
const table = page.locator('.ant-table');
|
||||
const tableWidth = await table.evaluate((el) => el.offsetWidth);
|
||||
expect(tableWidth).toBeGreaterThan(800);
|
||||
});
|
||||
|
||||
test('大屏幕仪表盘应该显示更多组件', async ({ page }) => {
|
||||
// 导航到仪表盘
|
||||
await page.click('[data-testid="menu-dashboard"]');
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
|
||||
// 检查仪表盘组件
|
||||
const dashboardCards = page.locator('.dashboard-card, .statistics-card');
|
||||
const cardCount = await dashboardCards.count();
|
||||
|
||||
// 大屏幕应该显示更多卡片
|
||||
expect(cardCount).toBeGreaterThan(4);
|
||||
|
||||
// 检查图表容器
|
||||
const chartContainers = page.locator(
|
||||
'.chart-container, .echarts-container',
|
||||
);
|
||||
const chartCount = await chartContainers.count();
|
||||
|
||||
if (chartCount > 0) {
|
||||
// 检查图表是否充分利用空间
|
||||
const firstChart = chartContainers.first();
|
||||
const chartWidth = await firstChart.evaluate((el) => el.offsetWidth);
|
||||
expect(chartWidth).toBeGreaterThan(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('响应式断点测试', () => {
|
||||
const breakpoints = [
|
||||
{ name: '超小屏', width: 320 },
|
||||
{ name: '小屏', width: 576 },
|
||||
{ name: '中屏', width: 768 },
|
||||
{ name: '大屏', width: 992 },
|
||||
{ name: '超大屏', width: 1200 },
|
||||
{ name: '巨屏', width: 1600 },
|
||||
];
|
||||
|
||||
breakpoints.forEach(({ name, width }) => {
|
||||
test(`${name}(${width}px)断点应该正确适配`, async ({ page }) => {
|
||||
// 设置视口尺寸
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 检查基本布局元素
|
||||
const header = page.locator('.layout-header');
|
||||
const content = page.locator('.layout-content');
|
||||
|
||||
await expect(header).toBeVisible();
|
||||
await expect(content).toBeVisible();
|
||||
|
||||
// 检查布局是否适配当前尺寸
|
||||
const contentWidth = await content.evaluate((el) => el.offsetWidth);
|
||||
expect(contentWidth).toBeLessThanOrEqual(width);
|
||||
expect(contentWidth).toBeGreaterThan(width * 0.8); // 至少使用80%的宽度
|
||||
|
||||
// 导航到列表页面测试表格适配
|
||||
if (width > 576) {
|
||||
// 大于576px时才测试表格
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
const table = page.locator('.ant-table, .mobile-card-list');
|
||||
await expect(table).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('触摸设备交互测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 模拟触摸设备
|
||||
await page.setViewportSize(TEST_CONFIG.viewports.mobile);
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('触摸滑动应该正常工作', async ({ page }) => {
|
||||
// 导航到有滚动内容的页面
|
||||
await page.click('[data-testid="mobile-menu-toggle"]');
|
||||
await page.waitForTimeout(300);
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查是否可以滚动
|
||||
const scrollContainer = page.locator(
|
||||
'.ant-table-wrapper, .mobile-card-list',
|
||||
);
|
||||
|
||||
if ((await scrollContainer.count()) > 0) {
|
||||
// 模拟触摸滚动
|
||||
await scrollContainer.hover();
|
||||
await page.mouse.wheel(0, 100);
|
||||
|
||||
// 等待滚动完成
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 检查滚动是否生效
|
||||
const scrollTop = await scrollContainer.evaluate((el) => el.scrollTop);
|
||||
expect(scrollTop).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('触摸点击区域应该足够大', async ({ page }) => {
|
||||
// 检查按钮点击区域
|
||||
const buttons = page.locator('button, .ant-btn');
|
||||
|
||||
if ((await buttons.count()) > 0) {
|
||||
const firstButton = buttons.first();
|
||||
|
||||
// 检查按钮尺寸(触摸友好的最小尺寸是44x44px)
|
||||
const buttonBox = await firstButton.boundingBox();
|
||||
|
||||
if (buttonBox) {
|
||||
expect(buttonBox.height).toBeGreaterThanOrEqual(40);
|
||||
expect(buttonBox.width).toBeGreaterThanOrEqual(40);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('长按菜单应该正常工作', async ({ page }) => {
|
||||
// 导航到有右键菜单的区域
|
||||
await page.click('[data-testid="mobile-menu-toggle"]');
|
||||
await page.waitForTimeout(300);
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 查找表格行或卡片项
|
||||
const listItem = page.locator('.ant-table-tbody tr, .card-item').first();
|
||||
|
||||
if ((await listItem.count()) > 0) {
|
||||
// 模拟长按
|
||||
await listItem.hover();
|
||||
await page.mouse.down();
|
||||
await page.waitForTimeout(1000); // 长按1秒
|
||||
await page.mouse.up();
|
||||
|
||||
// 检查是否显示上下文菜单
|
||||
const contextMenu = page.locator('.ant-dropdown, .context-menu');
|
||||
|
||||
if ((await contextMenu.count()) > 0) {
|
||||
await expect(contextMenu).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:管理员登录
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.adminUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.adminUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
531
frontend-vben/apps/web-antd/tests/e2e/websocket-realtime.test.ts
Normal file
531
frontend-vben/apps/web-antd/tests/e2e/websocket-realtime.test.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* WebSocket 实时通信功能端到端测试
|
||||
* 测试实时消息推送、状态同步、连接管理等功能
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const TEST_CONFIG = {
|
||||
baseURL: 'http://localhost:5173',
|
||||
wsURL: 'ws://localhost:3001/ws',
|
||||
timeout: 30000,
|
||||
adminUser: {
|
||||
username: 'admin',
|
||||
password: '111111',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('WebSocket 连接管理测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(TEST_CONFIG.timeout);
|
||||
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('WebSocket连接应该自动建立', async ({ page }) => {
|
||||
// 等待WebSocket连接建立
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查连接状态指示器
|
||||
const connectionStatus = page.locator(
|
||||
'[data-testid="ws-connection-status"]',
|
||||
);
|
||||
await expect(connectionStatus).toBeVisible();
|
||||
await expect(connectionStatus).toContainText(/已连接|Connected/);
|
||||
|
||||
// 检查连接状态图标
|
||||
const statusIcon = page.locator('[data-testid="ws-status-icon"]');
|
||||
await expect(statusIcon).toHaveClass(/connected|success/);
|
||||
});
|
||||
|
||||
test('WebSocket断线重连应该正常工作', async ({ page }) => {
|
||||
// 等待连接建立
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 模拟网络中断(通过开发者工具)
|
||||
await page.evaluate(() => {
|
||||
// 关闭WebSocket连接
|
||||
if (window.wsConnection) {
|
||||
window.wsConnection.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 检查连接状态变为断开
|
||||
const connectionStatus = page.locator(
|
||||
'[data-testid="ws-connection-status"]',
|
||||
);
|
||||
await expect(connectionStatus).toContainText(/连接中断|Disconnected/, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// 等待自动重连
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查是否重新连接
|
||||
await expect(connectionStatus).toContainText(/已连接|Connected/, {
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('手动重连功能应该正常工作', async ({ page }) => {
|
||||
// 模拟连接断开
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
window.wsConnection.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 等待状态更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 点击手动重连按钮
|
||||
const reconnectButton = page.locator('[data-testid="ws-reconnect-button"]');
|
||||
await reconnectButton.click();
|
||||
|
||||
// 检查重连状态
|
||||
const connectionStatus = page.locator(
|
||||
'[data-testid="ws-connection-status"]',
|
||||
);
|
||||
await expect(connectionStatus).toContainText(/连接中|Connecting/);
|
||||
|
||||
// 等待重连完成
|
||||
await expect(connectionStatus).toContainText(/已连接|Connected/, {
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('连接心跳机制应该正常工作', async ({ page }) => {
|
||||
// 监听WebSocket消息
|
||||
const heartbeatMessages: string[] = [];
|
||||
|
||||
await page.evaluateHandle(() => {
|
||||
return new Promise((resolve) => {
|
||||
const originalSend = WebSocket.prototype.send;
|
||||
WebSocket.prototype.send = function (data) {
|
||||
if (data.includes('heartbeat') || data.includes('ping')) {
|
||||
window.heartbeatSent = true;
|
||||
}
|
||||
return originalSend.call(this, data);
|
||||
};
|
||||
|
||||
setTimeout(resolve, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否发送了心跳消息
|
||||
const heartbeatSent = await page.evaluate(() => window.heartbeatSent);
|
||||
expect(heartbeatSent).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('实时消息推送测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 等待WebSocket连接建立
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test('系统通知应该实时接收', async ({ page }) => {
|
||||
// 监听通知消息
|
||||
const notifications: any[] = [];
|
||||
|
||||
await page.exposeFunction('onNotification', (data: any) => {
|
||||
notifications.push(data);
|
||||
});
|
||||
|
||||
// 注册通知监听器
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
window.wsConnection.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'notification') {
|
||||
window.onNotification(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟发送系统通知
|
||||
await page.evaluate(() => {
|
||||
// 这里应该触发一个会产生系统通知的操作
|
||||
// 例如:创建一个新的任务或发送一条消息
|
||||
});
|
||||
|
||||
// 等待通知到达
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 检查通知是否显示
|
||||
const notificationElement = page.locator('.ant-notification-notice');
|
||||
if ((await notificationElement.count()) > 0) {
|
||||
await expect(notificationElement).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('消息状态更新应该实时同步', async ({ page }) => {
|
||||
// 导航到私信群发页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-send-record"]');
|
||||
await page.waitForURL(/\/private-message\/record/);
|
||||
|
||||
// 记录初始状态
|
||||
const initialStats = await page
|
||||
.locator('[data-testid="sending-count"]')
|
||||
.textContent();
|
||||
|
||||
// 模拟消息状态变更(通过WebSocket)
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
// 模拟接收状态更新消息
|
||||
const mockMessage = {
|
||||
type: 'message_status_update',
|
||||
data: {
|
||||
taskId: 'test-task-123',
|
||||
messageId: 'msg-456',
|
||||
status: 'sent',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockMessage),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待状态更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查统计数据是否更新
|
||||
const updatedStats = await page
|
||||
.locator('[data-testid="sending-count"]')
|
||||
.textContent();
|
||||
|
||||
// 至少UI应该是响应的
|
||||
await expect(page.locator('[data-testid="sending-count"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('用户在线状态应该实时更新', async ({ page }) => {
|
||||
// 导航到用户列表页面
|
||||
await page.click('[data-testid="menu-account"]');
|
||||
await page.click('[data-testid="menu-account-list"]');
|
||||
await page.waitForURL(/\/account\/list/);
|
||||
|
||||
// 检查用户在线状态指示器
|
||||
const onlineIndicators = page.locator('[data-testid="user-online-status"]');
|
||||
|
||||
if ((await onlineIndicators.count()) > 0) {
|
||||
// 模拟用户状态变更
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockMessage = {
|
||||
type: 'user_status_update',
|
||||
data: {
|
||||
userId: 'user-123',
|
||||
status: 'online',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockMessage),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待状态更新
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查状态指示器
|
||||
await expect(onlineIndicators.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('任务进度应该实时更新', async ({ page }) => {
|
||||
// 导航到任务列表页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-send-task"]');
|
||||
await page.waitForURL(/\/private-message\/task/);
|
||||
|
||||
// 找到一个进行中的任务
|
||||
const taskProgress = page.locator('[data-testid="task-progress"]').first();
|
||||
|
||||
if ((await taskProgress.count()) > 0) {
|
||||
// 记录初始进度
|
||||
const initialProgress = await taskProgress.getAttribute('data-percent');
|
||||
|
||||
// 模拟进度更新
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockMessage = {
|
||||
type: 'task_progress_update',
|
||||
data: {
|
||||
taskId: 'task-123',
|
||||
progress: 75,
|
||||
completed: 750,
|
||||
total: 1000,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockMessage),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待进度更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查进度条是否更新
|
||||
await expect(taskProgress).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('实时监控功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// 导航到实时监控页面
|
||||
await page.click('[data-testid="menu-private-message"]');
|
||||
await page.click('[data-testid="menu-monitor"]');
|
||||
await page.waitForURL(/\/private-message\/monitor/);
|
||||
});
|
||||
|
||||
test('实时性能指标应该正常更新', async ({ page }) => {
|
||||
// 检查性能指标卡片
|
||||
const performanceMetrics = [
|
||||
'[data-testid="cpu-usage"]',
|
||||
'[data-testid="memory-usage"]',
|
||||
'[data-testid="active-connections"]',
|
||||
'[data-testid="message-rate"]',
|
||||
];
|
||||
|
||||
for (const metric of performanceMetrics) {
|
||||
await expect(page.locator(metric)).toBeVisible();
|
||||
}
|
||||
|
||||
// 记录初始值
|
||||
const initialCpu = await page
|
||||
.locator('[data-testid="cpu-usage"]')
|
||||
.textContent();
|
||||
|
||||
// 等待数据更新
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查数据是否可能发生变化
|
||||
const currentCpu = await page
|
||||
.locator('[data-testid="cpu-usage"]')
|
||||
.textContent();
|
||||
|
||||
// 至少UI应该是响应的
|
||||
await expect(page.locator('[data-testid="cpu-usage"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('实时日志应该正常显示', async ({ page }) => {
|
||||
// 检查日志显示区域
|
||||
await expect(page.locator('.realtime-logs')).toBeVisible();
|
||||
|
||||
// 检查日志条目
|
||||
const logEntries = page.locator('.log-entry');
|
||||
|
||||
// 如果有日志条目,检查其结构
|
||||
if ((await logEntries.count()) > 0) {
|
||||
const firstLog = logEntries.first();
|
||||
await expect(firstLog.locator('.log-timestamp')).toBeVisible();
|
||||
await expect(firstLog.locator('.log-level')).toBeVisible();
|
||||
await expect(firstLog.locator('.log-message')).toBeVisible();
|
||||
}
|
||||
|
||||
// 模拟新日志产生
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockLogMessage = {
|
||||
type: 'log_message',
|
||||
data: {
|
||||
level: 'info',
|
||||
message: '测试日志消息',
|
||||
timestamp: Date.now(),
|
||||
source: 'test-component',
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockLogMessage),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待新日志显示
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查是否有新的日志条目
|
||||
await expect(page.locator('.log-entry')).toHaveCount({ min: 1 });
|
||||
});
|
||||
|
||||
test('告警信息应该实时显示', async ({ page }) => {
|
||||
// 检查告警区域
|
||||
const alertsSection = page.locator('.alerts-section');
|
||||
await expect(alertsSection).toBeVisible();
|
||||
|
||||
// 模拟告警消息
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockAlert = {
|
||||
type: 'alert',
|
||||
data: {
|
||||
level: 'warning',
|
||||
title: '发送频率过高',
|
||||
message: '检测到发送频率超过限制,请注意调整',
|
||||
timestamp: Date.now(),
|
||||
id: 'alert-' + Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockAlert),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待告警显示
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查告警是否显示
|
||||
const alertItems = page.locator('.alert-item');
|
||||
if ((await alertItems.count()) > 0) {
|
||||
await expect(alertItems.first()).toBeVisible();
|
||||
await expect(alertItems.first().locator('.alert-title')).toContainText(
|
||||
'发送频率过高',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('实时图表应该正常更新', async ({ page }) => {
|
||||
// 检查实时图表容器
|
||||
const chartContainer = page.locator('.realtime-chart .echarts-container');
|
||||
await expect(chartContainer).toBeVisible();
|
||||
|
||||
// 等待图表渲染
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 检查图表是否有数据
|
||||
const chartCanvas = chartContainer.locator('canvas').first();
|
||||
await expect(chartCanvas).toBeVisible();
|
||||
|
||||
// 模拟新的性能数据
|
||||
await page.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockPerformanceData = {
|
||||
type: 'performance_data',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
cpu: Math.random() * 100,
|
||||
memory: Math.random() * 100,
|
||||
connections: Math.floor(Math.random() * 1000),
|
||||
messageRate: Math.floor(Math.random() * 500),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockPerformanceData),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待图表更新
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 图表应该仍然可见且响应
|
||||
await expect(chartCanvas).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('多标签页同步测试', () => {
|
||||
test('多标签页状态应该保持同步', async ({ context }) => {
|
||||
// 创建两个标签页
|
||||
const page1 = await context.newPage();
|
||||
const page2 = await context.newPage();
|
||||
|
||||
// 在两个标签页都登录
|
||||
await page1.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page1);
|
||||
|
||||
await page2.goto(TEST_CONFIG.baseURL);
|
||||
await loginAsAdmin(page2);
|
||||
|
||||
// 等待WebSocket连接建立
|
||||
await Promise.all([page1.waitForTimeout(2000), page2.waitForTimeout(2000)]);
|
||||
|
||||
// 在第一个标签页触发一个操作
|
||||
await page1.click('[data-testid="menu-private-message"]');
|
||||
await page1.click('[data-testid="menu-send-task"]');
|
||||
|
||||
// 模拟在第一个标签页创建任务
|
||||
await page1.evaluate(() => {
|
||||
if (window.wsConnection) {
|
||||
const mockTaskCreated = {
|
||||
type: 'task_created',
|
||||
data: {
|
||||
taskId: 'new-task-123',
|
||||
name: '新创建的测试任务',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: JSON.stringify(mockTaskCreated),
|
||||
});
|
||||
|
||||
window.wsConnection.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 切换到第二个标签页的相同页面
|
||||
await page2.click('[data-testid="menu-private-message"]');
|
||||
await page2.click('[data-testid="menu-send-task"]');
|
||||
|
||||
// 等待数据同步
|
||||
await page2.waitForTimeout(3000);
|
||||
|
||||
// 检查两个标签页的数据是否一致
|
||||
const page1TaskCount = await page1.locator('.task-item').count();
|
||||
const page2TaskCount = await page2.locator('.task-item').count();
|
||||
|
||||
// 至少UI应该是同步的
|
||||
expect(Math.abs(page1TaskCount - page2TaskCount)).toBeLessThanOrEqual(1);
|
||||
|
||||
await page1.close();
|
||||
await page2.close();
|
||||
});
|
||||
});
|
||||
|
||||
// 辅助函数:管理员登录
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.fill(
|
||||
'[data-testid="username-input"]',
|
||||
TEST_CONFIG.adminUser.username,
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="password-input"]',
|
||||
TEST_CONFIG.adminUser.password,
|
||||
);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||||
}
|
||||
77
frontend-vben/apps/web-antd/tests/global-setup.ts
Normal file
77
frontend-vben/apps/web-antd/tests/global-setup.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Playwright 全局测试设置
|
||||
* 在所有测试开始前执行
|
||||
*/
|
||||
|
||||
import { chromium, FullConfig } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
console.log('🚀 开始全局测试设置...');
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// 检查应用是否可访问
|
||||
console.log('📡 检查应用可访问性...');
|
||||
await page.goto('http://localhost:5173', { timeout: 30000 });
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 检查登录页面是否正常显示(更灵活的选择器)
|
||||
try {
|
||||
await page.waitForSelector(
|
||||
'input[placeholder*="用户名"], input[placeholder*="邮箱"], input[placeholder*="账号"], [data-testid="username-input"]',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
console.log('✅ 应用可正常访问');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 登录页面检测超时,但应用已启动');
|
||||
}
|
||||
|
||||
// 创建测试数据目录
|
||||
const testDataDir = path.join(process.cwd(), 'test-results', 'test-data');
|
||||
if (!fs.existsSync(testDataDir)) {
|
||||
fs.mkdirSync(testDataDir, { recursive: true });
|
||||
console.log('📁 测试数据目录已创建');
|
||||
}
|
||||
|
||||
// 创建截图目录
|
||||
const screenshotDir = path.join(
|
||||
process.cwd(),
|
||||
'test-results',
|
||||
'screenshots',
|
||||
);
|
||||
if (!fs.existsSync(screenshotDir)) {
|
||||
fs.mkdirSync(screenshotDir, { recursive: true });
|
||||
console.log('📸 截图目录已创建');
|
||||
}
|
||||
|
||||
// 初始化测试计数器
|
||||
const testMetrics = {
|
||||
startTime: new Date().toISOString(),
|
||||
setupCompleted: true,
|
||||
testEnvironment: process.env.NODE_ENV || 'test',
|
||||
baseURL: 'http://localhost:5173',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(testDataDir, 'test-metrics.json'),
|
||||
JSON.stringify(testMetrics, null, 2),
|
||||
);
|
||||
|
||||
console.log('🎉 全局测试设置完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 全局测试设置失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
105
frontend-vben/apps/web-antd/tests/global-teardown.ts
Normal file
105
frontend-vben/apps/web-antd/tests/global-teardown.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Playwright 全局测试清理
|
||||
* 在所有测试结束后执行
|
||||
*/
|
||||
|
||||
import { FullConfig } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function globalTeardown(config: FullConfig) {
|
||||
console.log('🧹 开始全局测试清理...');
|
||||
|
||||
try {
|
||||
// 更新测试指标
|
||||
const testDataDir = path.join(process.cwd(), 'test-results', 'test-data');
|
||||
const metricsFile = path.join(testDataDir, 'test-metrics.json');
|
||||
|
||||
if (fs.existsSync(metricsFile)) {
|
||||
const metrics = JSON.parse(fs.readFileSync(metricsFile, 'utf-8'));
|
||||
metrics.endTime = new Date().toISOString();
|
||||
metrics.duration =
|
||||
new Date(metrics.endTime).getTime() -
|
||||
new Date(metrics.startTime).getTime();
|
||||
metrics.teardownCompleted = true;
|
||||
|
||||
fs.writeFileSync(metricsFile, JSON.stringify(metrics, null, 2));
|
||||
console.log(`⏱️ 测试总耗时: ${(metrics.duration / 1000).toFixed(2)}秒`);
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
const authStateFile = path.join(process.cwd(), 'tests', 'auth-state.json');
|
||||
if (fs.existsSync(authStateFile)) {
|
||||
fs.unlinkSync(authStateFile);
|
||||
console.log('🗑️ 认证状态文件已清理');
|
||||
}
|
||||
|
||||
// 生成测试结果摘要
|
||||
const testResultsDir = path.join(process.cwd(), 'test-results');
|
||||
if (fs.existsSync(testResultsDir)) {
|
||||
const resultsFile = path.join(testResultsDir, 'results.json');
|
||||
|
||||
if (fs.existsSync(resultsFile)) {
|
||||
const results = JSON.parse(fs.readFileSync(resultsFile, 'utf-8'));
|
||||
|
||||
console.log('📊 测试结果摘要:');
|
||||
console.log(` - 总测试数: ${results.stats?.total || 0}`);
|
||||
console.log(` - 通过: ${results.stats?.passed || 0}`);
|
||||
console.log(` - 失败: ${results.stats?.failed || 0}`);
|
||||
console.log(` - 跳过: ${results.stats?.skipped || 0}`);
|
||||
|
||||
// 如果有失败的测试,列出来
|
||||
if (results.stats?.failed > 0) {
|
||||
console.log('❌ 失败的测试:');
|
||||
results.suites?.forEach((suite: any) => {
|
||||
suite.specs?.forEach((spec: any) => {
|
||||
spec.tests?.forEach((test: any) => {
|
||||
if (
|
||||
test.results?.some(
|
||||
(result: any) => result.status === 'failed',
|
||||
)
|
||||
) {
|
||||
console.log(
|
||||
` - ${suite.title} > ${spec.title} > ${test.title}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 生成简化的测试报告
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
stats: results.stats,
|
||||
duration: results.stats?.duration || 0,
|
||||
environment: process.env.NODE_ENV || 'test',
|
||||
browser: process.env.BROWSER || 'chromium',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(testResultsDir, 'test-summary.json'),
|
||||
JSON.stringify(summary, null, 2),
|
||||
);
|
||||
|
||||
console.log('📋 测试摘要已保存到 test-results/test-summary.json');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要保留测试数据
|
||||
if (process.env.KEEP_TEST_DATA !== 'true') {
|
||||
// 清理测试数据(根据需要)
|
||||
console.log('🧹 清理测试数据...');
|
||||
|
||||
// 这里可以添加清理测试时创建的数据的逻辑
|
||||
// 例如:删除测试用户、测试内容等
|
||||
}
|
||||
|
||||
console.log('✅ 全局测试清理完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 全局测试清理失败:', error);
|
||||
// 不抛出错误,避免影响测试结果
|
||||
}
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
384
frontend-vben/apps/web-antd/tests/menu-comparison.test.ts
Normal file
384
frontend-vben/apps/web-antd/tests/menu-comparison.test.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
interface MenuStats {
|
||||
totalCount: number;
|
||||
menuItems: string[];
|
||||
categories: string[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface MenuComparison {
|
||||
backend: MenuStats;
|
||||
mock: MenuStats;
|
||||
differences: {
|
||||
missingInMock: string[];
|
||||
onlyInMock: string[];
|
||||
totalDifference: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单对比测试
|
||||
* 测试前后端菜单显示差异,找出不一致的地方
|
||||
*/
|
||||
test.describe('菜单显示对比测试', () => {
|
||||
let comparison: MenuComparison;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
console.log('🚀 开始菜单对比测试');
|
||||
});
|
||||
|
||||
test('1️⃣ 后端启动状态下的菜单测试', async ({ page }) => {
|
||||
console.log('📊 测试后端启动状态下的菜单');
|
||||
|
||||
// 访问应用
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查是否在登录页面
|
||||
const isLoginPage = await page
|
||||
.locator('input[placeholder*="用户名"], input[placeholder*="admin"]')
|
||||
.isVisible();
|
||||
|
||||
if (isLoginPage) {
|
||||
console.log('🔐 检测到登录页面,开始登录');
|
||||
|
||||
// 填写登录信息
|
||||
await page.fill(
|
||||
'input[placeholder*="用户名"], input[placeholder*="admin"]',
|
||||
'admin',
|
||||
);
|
||||
await page.fill(
|
||||
'input[placeholder*="密码"], input[type="password"]',
|
||||
'111111',
|
||||
);
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click('button[type="submit"], button:has-text("登录")');
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// 等待菜单加载
|
||||
await page.waitForSelector(
|
||||
'aside[class*="sidebar"], nav[class*="menu"], .ant-menu',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 获取所有菜单项
|
||||
const menuStats = await extractMenuItems(page);
|
||||
|
||||
// 截图保存
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/backend-menu-full.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log(`✅ 后端模式菜单统计:`);
|
||||
console.log(` - 总菜单项: ${menuStats.totalCount}`);
|
||||
console.log(` - 主要分类: ${menuStats.categories.length}`);
|
||||
console.log(
|
||||
` - 菜单列表: ${menuStats.menuItems.slice(0, 10).join(', ')}${menuStats.menuItems.length > 10 ? '...' : ''}`,
|
||||
);
|
||||
|
||||
comparison = {
|
||||
backend: menuStats,
|
||||
mock: { totalCount: 0, menuItems: [], categories: [], timestamp: '' },
|
||||
differences: { missingInMock: [], onlyInMock: [], totalDifference: 0 },
|
||||
};
|
||||
});
|
||||
|
||||
test('2️⃣ Mock模式下的菜单测试', async ({ page, browser }) => {
|
||||
console.log('🎭 测试Mock模式下的菜单');
|
||||
|
||||
// 创建新的浏览器上下文来模拟无后端状态
|
||||
const context = await browser.newContext();
|
||||
const mockPage = await context.newPage();
|
||||
|
||||
// 拦截所有API请求,模拟后端不可用
|
||||
await mockPage.route('**/api/**', (route) => {
|
||||
route.abort('failed');
|
||||
});
|
||||
|
||||
await mockPage.route('**/auth/**', (route) => {
|
||||
route.abort('failed');
|
||||
});
|
||||
|
||||
await mockPage.route('**/system/**', (route) => {
|
||||
route.abort('failed');
|
||||
});
|
||||
|
||||
// 访问应用
|
||||
await mockPage.goto('http://localhost:5174');
|
||||
await mockPage.waitForTimeout(3000);
|
||||
|
||||
// 尝试登录(使用Mock数据)
|
||||
try {
|
||||
const isLoginPage = await mockPage
|
||||
.locator('input[placeholder*="用户名"], input[placeholder*="admin"]')
|
||||
.isVisible();
|
||||
|
||||
if (isLoginPage) {
|
||||
console.log('🔐 Mock模式登录测试');
|
||||
|
||||
// 使用Mock用户登录
|
||||
await mockPage.fill(
|
||||
'input[placeholder*="用户名"], input[placeholder*="admin"]',
|
||||
'admin',
|
||||
);
|
||||
await mockPage.fill(
|
||||
'input[placeholder*="密码"], input[type="password"]',
|
||||
'123456',
|
||||
);
|
||||
|
||||
await mockPage.click('button[type="submit"], button:has-text("登录")');
|
||||
await mockPage.waitForTimeout(3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Mock模式登录失败,尝试直接访问主页');
|
||||
await mockPage.goto('http://localhost:5174/dashboard');
|
||||
await mockPage.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// 等待菜单加载或超时
|
||||
try {
|
||||
await mockPage.waitForSelector(
|
||||
'aside[class*="sidebar"], nav[class*="menu"], .ant-menu',
|
||||
{ timeout: 8000 },
|
||||
);
|
||||
await mockPage.waitForTimeout(2000);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Mock模式菜单加载超时');
|
||||
}
|
||||
|
||||
// 获取Mock模式菜单项
|
||||
const mockMenuStats = await extractMenuItems(mockPage);
|
||||
|
||||
// 截图保存
|
||||
await mockPage.screenshot({
|
||||
path: 'test-results/screenshots/mock-menu-full.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log(`🎭 Mock模式菜单统计:`);
|
||||
console.log(` - 总菜单项: ${mockMenuStats.totalCount}`);
|
||||
console.log(` - 主要分类: ${mockMenuStats.categories.length}`);
|
||||
console.log(
|
||||
` - 菜单列表: ${mockMenuStats.menuItems.slice(0, 10).join(', ')}${mockMenuStats.menuItems.length > 10 ? '...' : ''}`,
|
||||
);
|
||||
|
||||
// 更新对比数据
|
||||
comparison.mock = mockMenuStats;
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('3️⃣ 生成菜单对比报告', async () => {
|
||||
console.log('📋 生成详细对比报告');
|
||||
|
||||
// 计算差异
|
||||
const backendMenus = new Set(comparison.backend.menuItems);
|
||||
const mockMenus = new Set(comparison.mock.menuItems);
|
||||
|
||||
const missingInMock = Array.from(backendMenus).filter(
|
||||
(item) => !mockMenus.has(item),
|
||||
);
|
||||
const onlyInMock = Array.from(mockMenus).filter(
|
||||
(item) => !backendMenus.has(item),
|
||||
);
|
||||
|
||||
comparison.differences = {
|
||||
missingInMock,
|
||||
onlyInMock,
|
||||
totalDifference: Math.abs(
|
||||
comparison.backend.totalCount - comparison.mock.totalCount,
|
||||
),
|
||||
};
|
||||
|
||||
// 生成报告
|
||||
const report = generateComparisonReport(comparison);
|
||||
|
||||
// 保存报告到文件
|
||||
const fs = await import('fs');
|
||||
await fs.promises.writeFile(
|
||||
'test-results/menu-comparison-report.json',
|
||||
JSON.stringify(comparison, null, 2),
|
||||
);
|
||||
|
||||
// 输出控制台报告
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('🔍 菜单对比分析报告');
|
||||
console.log('='.repeat(80));
|
||||
console.log(report);
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 断言检查
|
||||
expect(comparison.backend.totalCount).toBeGreaterThan(0);
|
||||
// 注意:Mock模式可能完全没有菜单,这是正常的测试结果
|
||||
console.log(
|
||||
`⚠️ 发现重大差异: Mock模式缺失 ${comparison.differences.totalDifference} 个菜单项`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 提取页面菜单项信息
|
||||
*/
|
||||
async function extractMenuItems(page: Page): Promise<MenuStats> {
|
||||
// 等待菜单容器加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
try {
|
||||
// 尝试多种菜单选择器
|
||||
const menuSelectors = [
|
||||
'.ant-menu-item',
|
||||
'.ant-menu-submenu-title',
|
||||
'[role="menuitem"]',
|
||||
'li[class*="menu"]',
|
||||
'a[class*="menu"]',
|
||||
'.sidebar-menu-item',
|
||||
'.nav-item',
|
||||
];
|
||||
|
||||
let allMenuItems: string[] = [];
|
||||
let categories: string[] = [];
|
||||
|
||||
for (const selector of menuSelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
|
||||
for (const element of elements) {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim()) {
|
||||
const cleanText = text.trim();
|
||||
if (!allMenuItems.includes(cleanText)) {
|
||||
allMenuItems.push(cleanText);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略选择器错误,继续下一个
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试获取主要分类
|
||||
try {
|
||||
const categorySelectors = [
|
||||
'.ant-menu-submenu-title',
|
||||
'.menu-group-title',
|
||||
'[class*="category"]',
|
||||
'.sidebar-title',
|
||||
];
|
||||
|
||||
for (const selector of categorySelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (const element of elements) {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim()) {
|
||||
const cleanText = text.trim();
|
||||
if (!categories.includes(cleanText)) {
|
||||
categories.push(cleanText);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略选择器错误
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ 获取分类失败:', error.message);
|
||||
}
|
||||
|
||||
// 如果没有找到菜单项,尝试其他方法
|
||||
if (allMenuItems.length === 0) {
|
||||
console.log('⚠️ 使用备用方法获取菜单');
|
||||
|
||||
const fallbackSelectors = [
|
||||
'span:has-text("仪表板")',
|
||||
'span:has-text("账号管理")',
|
||||
'span:has-text("群组管理")',
|
||||
'span:has-text("私信群发")',
|
||||
'span:has-text("系统管理")',
|
||||
'a[href*="/dashboard"]',
|
||||
'a[href*="/account"]',
|
||||
'a[href*="/system"]',
|
||||
];
|
||||
|
||||
for (const selector of fallbackSelectors) {
|
||||
try {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (const element of elements) {
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim()) {
|
||||
const cleanText = text.trim();
|
||||
if (!allMenuItems.includes(cleanText)) {
|
||||
allMenuItems.push(cleanText);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalCount: allMenuItems.length,
|
||||
menuItems: allMenuItems,
|
||||
categories: categories,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('❌ 菜单提取失败:', error.message);
|
||||
return {
|
||||
totalCount: 0,
|
||||
menuItems: [],
|
||||
categories: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对比报告
|
||||
*/
|
||||
function generateComparisonReport(comparison: MenuComparison): string {
|
||||
const { backend, mock, differences } = comparison;
|
||||
|
||||
let report = '';
|
||||
|
||||
report += `📊 菜单统计对比:\n`;
|
||||
report += ` 后端模式: ${backend.totalCount} 个菜单项\n`;
|
||||
report += ` Mock模式: ${mock.totalCount} 个菜单项\n`;
|
||||
report += ` 差异数量: ${differences.totalDifference} 个\n\n`;
|
||||
|
||||
if (differences.missingInMock.length > 0) {
|
||||
report += `❌ Mock模式中缺失的菜单项 (${differences.missingInMock.length}个):\n`;
|
||||
differences.missingInMock.forEach((item) => {
|
||||
report += ` - ${item}\n`;
|
||||
});
|
||||
report += '\n';
|
||||
}
|
||||
|
||||
if (differences.onlyInMock.length > 0) {
|
||||
report += `➕ 仅在Mock模式中存在的菜单项 (${differences.onlyInMock.length}个):\n`;
|
||||
differences.onlyInMock.forEach((item) => {
|
||||
report += ` - ${item}\n`;
|
||||
});
|
||||
report += '\n';
|
||||
}
|
||||
|
||||
report += `🔧 建议修复方案:\n`;
|
||||
if (differences.missingInMock.length > 0) {
|
||||
report += ` 1. 在Mock数据中添加缺失的 ${differences.missingInMock.length} 个菜单项\n`;
|
||||
report += ` 2. 检查菜单权限配置是否一致\n`;
|
||||
report += ` 3. 验证路由配置完整性\n`;
|
||||
}
|
||||
|
||||
if (differences.totalDifference === 0) {
|
||||
report += `✅ 前后端菜单完全一致,无需修复\n`;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Mock菜单深度调查测试
|
||||
* 专门调查为什么Mock模式下没有菜单显示
|
||||
*/
|
||||
test.describe('Mock菜单深度调查', () => {
|
||||
test('调查Mock模式菜单显示问题', async ({ page }) => {
|
||||
console.log('🔍 开始调查Mock模式菜单问题');
|
||||
|
||||
// 启用所有网络请求日志
|
||||
page.on('response', (response) => {
|
||||
if (response.url().includes('api') || response.url().includes('menu')) {
|
||||
console.log(`📡 API响应: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('api') || request.url().includes('menu')) {
|
||||
console.log(`📤 API请求: ${request.method()} ${request.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听控制台日志
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 控制台错误: ${msg.text()}`);
|
||||
} else if (msg.text().includes('menu') || msg.text().includes('Menu')) {
|
||||
console.log(`📝 菜单相关日志: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 访问应用
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
console.log('📋 检查当前环境配置');
|
||||
|
||||
// 检查环境变量
|
||||
const pageTitle = await page.title();
|
||||
console.log(`📄 页面标题: ${pageTitle}`);
|
||||
|
||||
// 检查是否启用了Mock
|
||||
const mockEnabled = await page.evaluate(() => {
|
||||
return {
|
||||
viteEnableMock:
|
||||
(window as any).__VITE_ENV__?.VITE_ENABLE_MOCK || 'false',
|
||||
viteApiUrl: (window as any).__VITE_ENV__?.VITE_API_URL || 'unknown',
|
||||
viteGlobApiUrl:
|
||||
(window as any).__VITE_ENV__?.VITE_GLOB_API_URL || 'unknown',
|
||||
};
|
||||
});
|
||||
|
||||
console.log('🔧 环境变量检查:');
|
||||
console.log(` VITE_ENABLE_MOCK: ${mockEnabled.viteEnableMock}`);
|
||||
console.log(` VITE_API_URL: ${mockEnabled.viteApiUrl}`);
|
||||
console.log(` VITE_GLOB_API_URL: ${mockEnabled.viteGlobApiUrl}`);
|
||||
|
||||
// 检查当前页面URL
|
||||
console.log(`🌍 当前URL: ${page.url()}`);
|
||||
|
||||
// 尝试登录(如果在登录页面)
|
||||
const isLoginPage = await page
|
||||
.locator('input[placeholder*="用户名"], input[placeholder*="admin"]')
|
||||
.isVisible();
|
||||
|
||||
if (isLoginPage) {
|
||||
console.log('🔐 检测到登录页面');
|
||||
|
||||
// 使用Mock后端的默认用户
|
||||
await page.fill(
|
||||
'input[placeholder*="用户名"], input[placeholder*="admin"]',
|
||||
'admin',
|
||||
);
|
||||
await page.fill(
|
||||
'input[placeholder*="密码"], input[type="password"]',
|
||||
'123456',
|
||||
);
|
||||
|
||||
// 点击登录并等待响应
|
||||
console.log('🚀 尝试登录...');
|
||||
await page.click('button[type="submit"], button:has-text("登录")');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查登录后的URL
|
||||
console.log(`🔄 登录后URL: ${page.url()}`);
|
||||
}
|
||||
|
||||
// 等待页面完全加载
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 详细检查DOM结构
|
||||
console.log('🏗️ 检查DOM结构');
|
||||
|
||||
// 检查是否有侧边栏
|
||||
const sidebarExists = await page
|
||||
.locator('aside, .sidebar, [class*="sidebar"]')
|
||||
.count();
|
||||
console.log(`📐 侧边栏元素数量: ${sidebarExists}`);
|
||||
|
||||
// 检查是否有菜单容器
|
||||
const menuContainers = await page
|
||||
.locator('.ant-menu, [role="menu"], [class*="menu"]')
|
||||
.count();
|
||||
console.log(`📋 菜单容器数量: ${menuContainers}`);
|
||||
|
||||
// 检查具体的菜单项
|
||||
const menuItems = await page
|
||||
.locator('.ant-menu-item, .ant-menu-submenu, [role="menuitem"]')
|
||||
.count();
|
||||
console.log(`📝 菜单项数量: ${menuItems}`);
|
||||
|
||||
// 尝试查找任何包含"仪表板"、"账号"等关键词的元素
|
||||
const dashboardElements = await page
|
||||
.locator('text=仪表板, text=账号, text=群组, text=系统')
|
||||
.count();
|
||||
console.log(`🎯 关键词元素数量: ${dashboardElements}`);
|
||||
|
||||
// 检查localStorage中的用户信息
|
||||
const userInfo = await page.evaluate(() => {
|
||||
return {
|
||||
localStorage: Object.keys(localStorage).map((key) => ({
|
||||
key,
|
||||
value: localStorage.getItem(key),
|
||||
})),
|
||||
sessionStorage: Object.keys(sessionStorage).map((key) => ({
|
||||
key,
|
||||
value: sessionStorage.getItem(key),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
console.log('💾 本地存储检查:');
|
||||
userInfo.localStorage.forEach((item) => {
|
||||
if (
|
||||
item.key.includes('user') ||
|
||||
item.key.includes('token') ||
|
||||
item.key.includes('menu')
|
||||
) {
|
||||
console.log(
|
||||
` localStorage.${item.key}: ${item.value?.substring(0, 100)}...`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试手动调用菜单API
|
||||
console.log('🔧 尝试手动调用菜单API');
|
||||
|
||||
try {
|
||||
const menuApiResponse = await page.evaluate(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/menus', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
return {
|
||||
status: response.status,
|
||||
data: await response.text(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📡 菜单API响应:', menuApiResponse);
|
||||
} catch (error) {
|
||||
console.log('❌ 菜单API调用失败:', error.message);
|
||||
}
|
||||
|
||||
// 截图保存当前状态
|
||||
await page.screenshot({
|
||||
path: 'test-results/screenshots/mock-investigation-full.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 检查是否有错误信息
|
||||
const errorElements = await page
|
||||
.locator('.ant-result-error, .error, [class*="error"]')
|
||||
.count();
|
||||
console.log(`⚠️ 错误元素数量: ${errorElements}`);
|
||||
|
||||
// 检查路由是否正确
|
||||
console.log('🛣️ 路由检查');
|
||||
const currentPath = await page.evaluate(() => window.location.pathname);
|
||||
console.log(` 当前路径: ${currentPath}`);
|
||||
|
||||
// 尝试直接访问仪表板页面
|
||||
console.log('🎯 尝试直接访问仪表板');
|
||||
await page.goto('http://localhost:5174/dashboard');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const dashboardUrl = page.url();
|
||||
console.log(`📊 仪表板访问结果: ${dashboardUrl}`);
|
||||
|
||||
// 最终检查菜单
|
||||
const finalMenuCount = await page
|
||||
.locator('.ant-menu-item, .ant-menu-submenu, [role="menuitem"]')
|
||||
.count();
|
||||
console.log(`📈 最终菜单项数量: ${finalMenuCount}`);
|
||||
|
||||
// 输出总结
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🔍 Mock模式调查总结');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Mock启用状态: ${mockEnabled.viteEnableMock}`);
|
||||
console.log(`侧边栏元素: ${sidebarExists} 个`);
|
||||
console.log(`菜单容器: ${menuContainers} 个`);
|
||||
console.log(`菜单项: ${menuItems} 个`);
|
||||
console.log(`最终菜单项: ${finalMenuCount} 个`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 基本断言
|
||||
expect(sidebarExists).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('测试Mock环境变量设置', async ({ page }) => {
|
||||
console.log('🔧 测试设置Mock环境变量');
|
||||
|
||||
// 设置Mock环境变量
|
||||
await page.addInitScript(() => {
|
||||
// 模拟启用Mock模式
|
||||
Object.defineProperty(window, 'import', {
|
||||
value: {
|
||||
meta: {
|
||||
env: {
|
||||
VITE_ENABLE_MOCK: 'true',
|
||||
VITE_API_URL: 'http://localhost:5320',
|
||||
VITE_GLOB_API_URL: 'http://localhost:5320',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('http://localhost:5174');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
console.log('✅ Mock环境变量设置测试完成');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user