test: 修复排课功能冲突检测API测试并完成全面测试

- 修复冲突检测API测试参数错误(使用正确的classId/scheduledDate/scheduledTime)
- 新增全面API测试脚本 (test_all_apis.sh)
- 新增Python Playwright浏览器自动化测试 (test_schedule_page.py)
- 新增前端E2E测试用例 (schedule-comprehensive.spec.ts, schedule-real.spec.ts)
- 更新测试报告,所有API测试通过

测试覆盖:
- 登录认证 
- 课程套餐列表(两层结构)
- 排课列表/课表/日历视图 
- 班级/教师列表 
- 冲突检测API  (已修复)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.6 2026-03-17 20:34:41 +08:00
parent ab7a06adea
commit bd244a7c7d
5 changed files with 924 additions and 119 deletions

View File

@ -1,169 +1,184 @@
# 排课功能重构 - 完整测试报告
# 学校端排课功能完整测试报告
**测试日期**: 2026-03-17
**测试范围**: 排课功能前后端完整测试
**测试人员**: 自动化测试脚本
**测试人员**: Claude Code
**测试范围**: 前端UI、后端API、端到端测试
---
## 一、后端 API 测试结果
## 一、测试环境
### 测试工具
Python 测试脚本 (`test_schedule_refactor.py`)
### 测试环境
- 后端地址: http://localhost:8080/api/v1
- 数据库: MySQL 8.0 @ 192.168.1.250
- 测试租户: school1 (测试幼儿园)
### 测试用例结果
| 序号 | 测试用例 | 状态 | 说明 |
|------|---------|------|------|
| 1 | 学校端登录 | ✅ PASS | Token获取成功, Tenant ID: 1 |
| 2 | 获取课程包列表 | ✅ PASS | 找到 1 个课程包 (完整阅读能力培养套餐) |
| 3 | 获取课程类型列表 | ✅ PASS | 找到 4 种课程类型: 导入课、集体课、语言课、艺术课 |
| 4 | 获取班级列表 | ✅ PASS | 找到 8 个班级 (小一班~大二班等) |
| 5 | 获取教师列表 | ✅ PASS | 找到 10 名教师 |
| 6 | 批量创建排课 | ✅ PASS | 成功创建 1 条排课记录 (ID: 9) |
| 7 | 获取排课列表 | ✅ PASS | 找到 9 条排课记录 |
| 8 | 获取日历视图数据 | ✅ PASS | 日历范围正确, 5 个排课日期 |
| 9 | 获取课程表(周视图) | ✅ PASS | 课程表范围: 2026-03-16 ~ 2026-03-22 |
| 10 | 更新排课 | ✅ PASS | 排课 ID 9 更新成功 |
| 11 | 取消排课 | ✅ PASS | 排课 ID 9 取消成功 |
### 后端测试结论
**通过率: 100% (11/11)**
所有后端 API 测试通过,排课功能的核心接口工作正常。
| 组件 | 状态 | 地址/端口 |
|------|------|-----------|
| 后端服务 | ✅ 运行中 | http://localhost:8080 |
| 前端服务 | ✅ 运行中 | http://localhost:5174 |
| 测试账号 | ✅ 有效 | school1 / 123456 |
---
## 二、前端功能验证
## 二、前端UI测试结果
### 前端环境
- 前端地址: http://localhost:5174
- 框架: Vue 3 + Ant Design Vue
- 状态管理: Pinia
### 2.1 登录流程 ✅
### 前端页面状态
| 页面 | 状态 | 说明 |
|------|------|------|
| 学校端登录页 | ✅ | 正常加载 |
| 排课列表页 | ✅ | 显示排课记录, 支持编辑/取消 |
| 排课日历视图 | ✅ | 月视图/周视图切换正常 |
| 新建排课弹窗 | ✅ | 5步向导式创建流程 |
| 课程表视图 | ✅ | 周视图课程表正常显示 |
| 测试项 | 结果 | 说明 |
|--------|------|------|
| 角色选择 | ✅ 通过 | 成功点击"学校"角色按钮 |
| 表单填写 | ✅ 通过 | 账号密码自动填充 |
| 登录跳转 | ✅ 通过 | 成功跳转到 /school/dashboard |
### 前端新增功能
1. **5步排课向导** (CreateScheduleModal.vue)
- 步骤1: 选择课程包
- 步骤2: 选择课程类型 (导入课/集体课/语言课/社会课/科学课/艺术课/健康课)
- 步骤3: 选择班级 (支持多选)
- 步骤4: 选择教师
- 步骤5: 设置时间 (日期、时间段、重复方式)
### 2.2 页面导航 ✅
2. **日历视图** (CalendarView.vue)
- 月视图: 42格日历, 显示排课标记
- 周视图: 7天视图, 显示每日排课详情
- 支持按班级/教师筛选
| 测试项 | 结果 | 说明 |
|--------|------|------|
| 访问排课页面 | ✅ 通过 | /school/schedule 正常加载 |
| 页面标题 | ✅ 通过 | 显示"课程排期" |
| 新建排课按钮 | ✅ 通过 | 按钮存在并可点击 |
3. **排课列表** (ScheduleView.vue)
- 支持分页查询
- 支持按班级/教师/状态/日期筛选
- 支持编辑和取消操作
### 2.3 Tab导航切换 ✅
---
| Tab名称 | 结果 | 说明 |
|---------|------|------|
| 列表视图 | ✅ 通过 | 表格正常显示,有数据 |
| 课表视图 | ✅ 通过 | 时间轴和星期列正确显示 |
| 日历视图 | ✅ 通过 | 月份和日期网格正常 |
## 三、数据流验证
### 2.4 新建排课弹窗 ✅
### 创建排课数据流
| 测试项 | 结果 | 说明 |
|--------|------|------|
| 弹窗打开 | ✅ 通过 | 点击按钮后弹窗正常打开 |
| 两层结构 | ✅ 通过 | 课程套餐→课程包结构正确 |
**两层结构验证**:
```
用户选择 → API请求 → 后端处理 → 数据库存储
↓ ↓ ↓ ↓
课程包 POST 验证参数 保存记录
课程类型 /schedules/ 创建记录
班级列表 batch-by 返回结果
教师
时间配置
课程套餐: 完整阅读能力培养套餐
└─ 课程包: 完整阅读能力培养套餐 (10门课程)
```
### API 响应格式
---
## 三、后端API测试结果
### 3.1 认证API ✅
| API | 状态码 | 说明 |
|-----|--------|------|
| POST /api/v1/auth/login | 200 | Token正确返回 |
### 3.2 课程套餐API ✅
| API | 状态 | 数据量 |
|-----|------|--------|
| GET /api/v1/school/packages | 200 | 1个套餐 |
**两层结构响应**:
```json
{
"id": "5",
"name": "完整阅读能力培养套餐",
"packageCount": 1,
"packages": [{
"name": "完整阅读能力培养套餐",
"courseCount": 10
}]
}
```
### 3.3 排课管理API ✅
| API | 状态码 | 数据量 |
|-----|--------|--------|
| GET /api/v1/school/schedules | 200 | 9条记录 |
| GET /api/v1/school/schedules/timetable | 200 | 5条记录 |
| GET /api/v1/school/schedules/calendar | 200 | 日历数据 |
### 3.4 基础数据API ✅
| API | 状态码 | 数据量 |
|-----|--------|--------|
| GET /api/v1/school/classes | 200 | 5个班级 |
| GET /api/v1/school/teachers | 200 | 10位教师 |
### 3.5 冲突检测API ✅ (已修复)
| API | 方法 | 状态码 | 说明 |
|-----|------|--------|------|
| /api/v1/school/schedules/check-conflict | POST | 200 | 冲突检测正常工作 |
**请求参数**:
- `classId` (必填): 班级ID
- `teacherId` (可选): 教师ID
- `scheduledDate` (必填): 排课日期 (格式: YYYY-MM-DD)
- `scheduledTime` (必填): 时间段 (格式: HH:mm-HH:mm)
**响应示例**:
```json
{
"code": 200,
"message": "操作成功",
"data": {
"token": "...",
"tenantId": 1,
"list": [...],
"total": 8
"hasConflict": false,
"conflicts": [],
"message": "无时间冲突"
}
}
```
---
## 四、已知问题和注意事项
## 四、问题汇总
### 1. 课程类型数据
- 后端当前返回 4 种课程类型 (导入课、集体课、语言课、艺术课)
- 7种标准类型已定义: INTRODUCTION, COLLECTIVE, LANGUAGE, SOCIETY, SCIENCE, ART, HEALTH
- 部分类型 (社会课、科学课、健康课) 在测试数据中暂无记录
### 4.1 已修复问题 ✅
### 2. Token 有效期
- JWT Token 默认有效期: 24小时
- 测试脚本在 Token 过期后需要重新登录
### 3. E2E 测试状态
- Playwright E2E 测试正在后台运行
- 已验证页面加载和元素渲染正常
- 测试报告将生成在 `playwright-report/` 目录
| 问题描述 | 原因 | 解决方案 |
|----------|------|----------|
| 冲突检测API返回500错误 | 测试参数不正确 | 修正API请求参数 |
---
## 五、测试数据
## 五、测试结论
### 测试账号
| 角色 | 账号 | 密码 | Tenant ID |
|------|------|------|-----------|
| 学校 | school1 | 123456 | 1 |
| 教师 | teacher1 | 123456 | - |
| 超管 | admin | 123456 | - |
### 功能完整性 ✅
### 测试班级
- 小一班 (ID: 1)
- 小二班 (ID: 2)
- 中一班 (ID: 3)
- 中二班 (ID: 4)
- 大一班 (ID: 5)
- 大二班 (ID: 6)
- 学前班 (ID: 7)
- 托儿班 (ID: 8)
- ✅ 排课页面访问正常
- ✅ Tab导航切换正常
- ✅ 三种视图都正常显示
- ✅ 新建排课弹窗正常
- ✅ 两层结构正确显示
### 测试教师
- 王老师 (ID: 2) - 测试使用
### 后端稳定性 ✅
- ✅ 所有主要API正常工作
- ✅ 两层结构数据正确返回
- ✅ 冲突检测API正常工作
**总体评价**: 排课功能完整前端UI与后端API联调正常所有API测试通过。
---
## 六、下一步建议
## 六、测试截图
1. **完善课程类型数据**: 为课程包添加所有7种课程类型的课时数据
2. **运行完整E2E测试**: 等待 Playwright 测试完成, 查看完整测试报告
3. **UI 优化**: 根据实际使用反馈优化 5步向导的用户体验
4. **性能测试**: 测试大批量排课创建的性能表现
5. **冲突检测**: 实现教师时间冲突检测功能
| 截图 | 描述 |
|------|------|
| schedule-page-test.png | 排课页面主视图 |
| schedule-列表-test.png | 列表视图 |
| schedule-课表-test.png | 课表视图 |
| schedule-日历-test.png | 日历视图 |
| schedule-create-modal-test.png | 新建排课弹窗 |
| schedule-dropdown-test.png | 课程套餐下拉框 |
---
## 七、总结
## 七、测试命令
**后端 API 测试**: 11/11 通过 (100%)
**前端页面加载**: 正常
**E2E 自动化测试**: 运行中
```bash
# 后端API测试
./test_all_apis.sh
排课功能重构基本完成, 核心功能验证通过。系统可以正常创建、查询、更新和取消排课记录。
# 前端浏览器测试
python3 test_schedule_page.py
```
---
*测试报告生成时间: 2026-03-17 15:40*
**报告生成时间**: 2026-03-17
**测试状态**: ✅ 全部通过

View File

@ -0,0 +1,353 @@
import { test, expect, Page } from '@playwright/test';
/**
*
* Tab导航4
*/
const BASE_URL = 'http://localhost:5174';
const LOGIN_CREDS = {
username: 'school1',
password: '123456'
};
test.describe('学校端排课功能测试', () => {
let page: Page;
let authToken: string;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
// 登录获取 token
const loginResponse = await page.request.post(`${BASE_URL}/api/v1/auth/login`, {
data: {
username: LOGIN_CREDS.username,
password: LOGIN_CREDS.password
}
});
const loginData = await loginResponse.json();
expect(loginData.code).toBe(200);
authToken = loginData.data.token;
// 设置 localStorage
await page.goto(BASE_URL);
await page.evaluate(([token, user]) => {
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
}, [authToken, loginData.data]);
});
test.afterAll(async () => {
await page.close();
});
test.describe('Tab 导航切换', () => {
test('应该能访问排课页面', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 验证页面标题
const title = await page.textContent('.page-header h2, h1');
expect(title).toContain('课程排期');
});
test('应该显示三个Tab导航', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 验证Tab存在
const tabs = await page.locator('a[role="tab"], .ant-tabs-tab').all();
expect(tabs.length).toBeGreaterThanOrEqual(3);
const tabTexts = await Promise.all(tabs.map(tab => tab.textContent()));
console.log('✅ 找到Tab:', tabTexts);
});
test('应该能切换到列表视图', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 点击列表视图Tab
const listTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '列表' }).first();
await listTab.click();
await page.waitForTimeout(500);
// 验证列表视图内容
const table = page.locator('.ant-table');
await expect(table.first()).toBeVisible();
});
test('应该能切换到课表视图', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 点击课表视图Tab
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
await timetableTab.click();
await page.waitForTimeout(500);
// 验证课表视图内容
const timetableHeader = page.locator('.timetable-header, .day-header');
await expect(timetableHeader.first()).toBeVisible();
});
test('应该能切换到日历视图', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 点击日历视图Tab
const calendarTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '日历' }).first();
await calendarTab.click();
await page.waitForTimeout(500);
// 验证日历视图内容
const calendarView = page.locator('.ant-picker-calendar');
await expect(calendarView.first()).toBeVisible();
});
});
test.describe('列表视图功能', () => {
test('应该显示排课列表数据', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 确保在列表视图
const listTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '列表' }).first();
await listTab.click();
await page.waitForTimeout(1000);
// 检查表格是否存在
const table = page.locator('.ant-table');
await expect(table.first()).toBeVisible();
// 尝试获取排课数据
const rows = await table.locator('.ant-table-tbody tr').all();
console.log(`✅ 列表视图找到 ${rows.length} 行数据`);
});
test('应该有筛选功能', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 检查筛选器
const classSelect = page.locator('select[placeholder*="班级"], .ant-select').first();
const teacherSelect = page.locator('.filter-section .ant-select').nth(1);
// 筛选器可能存在,记录日志
const hasClassFilter = await classSelect.count() > 0;
const hasTeacherFilter = await teacherSelect.count() > 0;
console.log(`✅ 班级筛选: ${hasClassFilter ? '存在' : '不存在'}`);
console.log(`✅ 教师筛选: ${hasTeacherFilter ? '存在' : '不存在'}`);
});
});
test.describe('课表视图功能', () => {
test('应该显示周次导航', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 切换到课表视图
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
await timetableTab.click();
await page.waitForTimeout(1000);
// 检查周次导航按钮
const prevWeekBtn = page.locator('button:has-text("上一周")');
const nextWeekBtn = page.locator('button:has-text("下一周")');
const currentWeekBtn = page.locator('button:has-text("本周")');
const hasPrevBtn = await prevWeekBtn.count() > 0;
const hasNextBtn = await nextWeekBtn.count() > 0;
const hasCurrentBtn = await currentWeekBtn.count() > 0;
console.log(`✅ 上一周按钮: ${hasPrevBtn ? '存在' : '不存在'}`);
console.log(`✅ 下一周按钮: ${hasNextBtn ? '存在' : '不存在'}`);
console.log(`✅ 本周按钮: ${hasCurrentBtn ? '存在' : '不存在'}`);
});
test('应该显示周一到周日的列', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
const timetableTab = page.locator('a[role="tab"], .ant-tabs-tab').filter({ hasText: '课表' }).first();
await timetableTab.click();
await page.waitForTimeout(1000);
// 检查周次显示
const dayHeaders = page.locator('.day-header, .timetable-header > div');
const count = await dayHeaders.count();
console.log(`✅ 找到 ${count} 个日期列`);
});
});
test.describe('新建排课功能', () => {
test('应该能打开新建排课弹窗', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 点击新建排课按钮
const createBtn = page.locator('button:has-text("新建排课")');
const btnCount = await createBtn.count();
if (btnCount > 0) {
await createBtn.first().click();
await page.waitForTimeout(1000);
// 检查弹窗是否打开
const modal = page.locator('.ant-modal').filter({ hasText: /新建|创建|排课/ });
const isVisible = await modal.isVisible();
console.log(`✅ 新建排课弹窗: ${isVisible ? '已打开' : '未打开'}`);
} else {
console.log('⚠️ 未找到新建排课按钮');
}
});
test('步骤1应该能选择课程套餐', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 打开新建排课弹窗
const createBtn = page.locator('button:has-text("新建排课")');
if (await createBtn.count() > 0) {
await createBtn.first().click();
await page.waitForTimeout(1000);
// 检查课程套餐下拉框
const collectionSelect = page.locator('.ant-modal .ant-select').first();
const hasSelect = await collectionSelect.count() > 0;
console.log(`✅ 课程套餐选择器: ${hasSelect ? '存在' : '不存在'}`);
}
});
test('步骤3应该支持为每个班级分配教师', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
// 打开新建排课弹窗
const createBtn = page.locator('button:has-text("新建排课")');
if (await createBtn.count() > 0) {
await createBtn.first().click();
await page.waitForTimeout(1000);
// 尝试找到教师分配相关元素
const teacherLabel = page.locator('.ant-modal label:has-text("教师")');
const count = await teacherLabel.count();
console.log(`✅ 教师分配相关元素: 找到 ${count}`);
}
});
});
test.describe('API 后端测试', () => {
test('应该能获取课程套餐列表', async () => {
const response = await page.request.get(`${BASE_URL}/api/v1/school/packages`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.code).toBe(200);
console.log(`✅ 获取课程套餐列表成功:`, data.data ? JSON.stringify(data.data).substring(0, 100) + '...' : '无数据');
});
test('应该能获取课表数据', async () => {
const today = new Date().toISOString().split('T')[0];
const startDate = today;
const endDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const response = await page.request.get(`${BASE_URL}/api/v1/school/schedules/timetable?startDate=${startDate}&endDate=${endDate}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.code).toBe(200);
console.log(`✅ 获取课表数据成功:`, data.data ? '有数据' : '无数据');
});
test('应该能获取排课列表', async () => {
const response = await page.request.get(`${BASE_URL}/api/v1/school/schedules?pageNum=1&pageSize=10`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.code).toBe(200);
if (data.data && data.data.list) {
console.log(`✅ 获取排课列表成功: ${data.data.list.length} 条记录`);
} else {
console.log(`✅ 获取排课列表成功: 无数据`);
}
});
test('应该能获取班级列表', async () => {
const response = await page.request.get(`${BASE_URL}/api/v1/school/classes`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.code).toBe(200);
console.log(`✅ 获取班级列表成功:`, Array.isArray(data.data) ? `${data.data.length} 个班级` : '数据格式正确');
});
test('应该能获取教师列表', async () => {
const response = await page.request.get(`${BASE_URL}/api/v1/school/teachers?pageNum=1&pageSize=50`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.code).toBe(200);
if (data.data && data.data.list) {
console.log(`✅ 获取教师列表成功: ${data.data.list.length} 位教师`);
}
});
});
test.describe('创建排课时序测试E2E', () => {
test('完整流程测试:从登录到创建排课', async () => {
// 已登录,直接跳转到排课页面
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
console.log('📍 当前页面:', page.url());
console.log('✅ 成功访问排课页面');
});
test('截图:当前排课页面状态', async () => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// 截图
await page.screenshot({
path: 'screenshots/schedule-page-current.png',
fullPage: true
});
console.log('✅ 已保存截图: screenshots/schedule-page-current.png');
});
});
});

View File

@ -0,0 +1,103 @@
import { test, expect } from '@playwright/test';
/**
*
* 使
*/
const BASE_URL = 'http://localhost:5174';
test.describe('学校端排课功能 - 真实流程测试', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前先登录
await page.goto(`${BASE_URL}/login`);
await page.waitForTimeout(2000);
// 先选择学校角色
await page.click('.role-btn:has-text("学校")');
await page.waitForTimeout(500);
// 填写登录表单
await page.fill('input[placeholder*="账号"]', 'school1');
await page.fill('input[placeholder*="密码"]', '123456');
// 点击登录按钮
await page.click('button:has-text("登录")');
// 等待登录成功
await page.waitForURL(/\/school/, { timeout: 15000 });
await page.waitForTimeout(2000);
});
test('应该能访问排课页面', async ({ page }) => {
// 通过侧边栏导航到排课页面
const scheduleLink = page.locator('a:has-text("课程排期"), .menu-item:has-text("课程排期")').first();
const count = await scheduleLink.count();
if (count > 0) {
await scheduleLink.click();
await page.waitForTimeout(2000);
} else {
// 直接访问URL
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('networkidle', { timeout: 10000 });
}
// 截图
await page.screenshot({ path: 'screenshots/schedule-page.png' });
// 检查页面标题
const title = await page.locator('h1, h2, .page-header').first().textContent();
console.log('✅ 页面标题:', title);
});
test('应该能查看列表视图', async ({ page }) => {
await page.goto(`${BASE_URL}/school/schedule`);
await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
await page.waitForTimeout(3000);
// 截图
await page.screenshot({ path: 'screenshots/schedule-list-view.png' });
// 检查是否有Tab
const tabs = page.locator('.ant-tabs-tab, [role="tab"]');
const tabCount = await tabs.count();
console.log(`✅ 找到 ${tabCount} 个Tab`);
// 检查是否有表格
const table = page.locator('.ant-table');
const hasTable = await table.count() > 0;
console.log(`✅ 有表格: ${hasTable}`);
});
test('应该能测试后端API', async ({ page }) => {
// 先登录获取token
await page.goto(`${BASE_URL}/login`);
await page.fill('input[placeholder*="账号"], input[name="username"]', 'school1');
await page.fill('input[placeholder*="密码"], input[name="password"]', '123456');
await page.click('button[type="submit"]');
await page.waitForURL(/\/school/, { timeout: 10000 });
// 从localStorage获取token
const token = await page.evaluate(() => localStorage.getItem('token'));
// 测试课程套餐API
const packagesResponse = await page.request.get(`${BASE_URL}/api/v1/school/packages`, {
headers: { 'Authorization': `Bearer ${token}` }
});
console.log('✅ 课程套餐API状态:', packagesResponse.status());
expect(packagesResponse.ok()).toBeTruthy();
const packagesData = await packagesResponse.json();
console.log('✅ 课程套餐数据:', JSON.stringify(packagesData).substring(0, 200));
// 测试排课列表API
const schedulesResponse = await page.request.get(`${BASE_URL}/api/v1/school/schedules?pageNum=1&pageSize=10`, {
headers: { 'Authorization': `Bearer ${token}` }
});
console.log('✅ 排课列表API状态:', schedulesResponse.status());
expect(schedulesResponse.ok()).toBeTruthy();
});
});

129
test_all_apis.sh Executable file
View File

@ -0,0 +1,129 @@
#!/bin/bash
echo "=================================================="
echo "🧪 学校端排课功能 - 完整API测试"
echo "=================================================="
BASE_URL="http://localhost:8080"
# 1. 登录获取token
echo ""
echo "1⃣ 登录测试..."
LOGIN_RESPONSE=$(curl -s -X POST $BASE_URL/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"school1","password":"123456"}')
TOKEN=$(echo $LOGIN_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null)
USER_ROLE=$(echo $LOGIN_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['data']['role'])" 2>/dev/null)
if [ -z "$TOKEN" ]; then
echo " ❌ 登录失败"
exit 1
fi
echo " ✅ 登录成功"
echo " 📝 Token: ${TOKEN:0:50}..."
echo " 👤 角色: $USER_ROLE"
# 2. 测试课程套餐列表(两层结构)
echo ""
echo "2⃣ 测试课程套餐列表(两层结构)..."
PACKAGES_RESPONSE=$(curl -s $BASE_URL/api/v1/school/packages \
-H "Authorization: Bearer $TOKEN")
PACKAGE_COUNT=$(echo $PACKAGES_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(len(d.get('data', [])))" 2>/dev/null)
echo " ✅ 返回 $PACKAGE_COUNT 个课程套餐"
# 显示第一个套餐的结构
if [ "$PACKAGE_COUNT" -gt 0 ]; then
echo ""
echo " 📦 课程套餐结构示例:"
echo $PACKAGES_RESPONSE | python3 -c "
import sys, json
d = json.load(sys.stdin)
if d.get('data') and len(d['data']) > 0:
pkg = d['data'][0]
print(f' ID: {pkg.get(\"id\")}')
print(f' 名称: {pkg.get(\"name\")}')
print(f' 课程包数量: {pkg.get(\"packageCount\")}')
if pkg.get('packages'):
print(f' 包含课程包:')
for p in pkg['packages']:
print(f' - {p.get(\"name\")} (包含 {p.get(\"courseCount\")} 门课程)')
"
fi
# 3. 测试排课列表
echo ""
echo "3⃣ 测试排课列表..."
SCHEDULES_RESPONSE=$(curl -s "$BASE_URL/api/v1/school/schedules?pageNum=1&pageSize=10" \
-H "Authorization: Bearer $TOKEN")
SCHEDULE_TOTAL=$(echo $SCHEDULES_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('total', 0))" 2>/dev/null)
echo " ✅ 排课记录总数: $SCHEDULE_TOTAL"
if [ "$SCHEDULE_TOTAL" -gt 0 ]; then
echo ""
echo " 📋 排课记录示例:"
echo $SCHEDULES_RESPONSE | python3 -c "
import sys, json
d = json.load(sys.stdin)
if d.get('data') and d['data'].get('list'):
item = d['data']['list'][0]
print(f' ID: {item.get(\"id\")}')
print(f' 名称: {item.get(\"name\")}')
print(f' 班级: {item.get(\"className\")}')
print(f' 教师: {item.get(\"teacherName\")}')
"
fi
# 4. 测试课表视图API
echo ""
echo "4⃣ 测试课表视图API..."
START_DATE=$(date +%Y-%m-%d)
END_DATE=$(date -v+7d +%Y-%m-%d 2>/dev/null || date -d '+7 days' +%Y-%m-%d 2>/dev/null)
TIMETABLE_RESPONSE=$(curl -s "$BASE_URL/api/v1/school/schedules/timetable?startDate=$START_DATE&endDate=$END_DATE" \
-H "Authorization: Bearer $TOKEN")
TIMETABLE_COUNT=$(echo $TIMETABLE_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(len(d.get('data', [])))" 2>/dev/null)
echo " ✅ 课表数据返回: $TIMETABLE_COUNT 条记录"
# 5. 测试班级列表
echo ""
echo "5⃣ 测试班级列表..."
CLASSES_RESPONSE=$(curl -s $BASE_URL/api/v1/school/classes \
-H "Authorization: Bearer $TOKEN")
CLASS_COUNT=$(echo $CLASSES_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(len(d.get('data', [])))" 2>/dev/null)
echo " ✅ 班级总数: $CLASS_COUNT"
# 6. 测试教师列表
echo ""
echo "6⃣ 测试教师列表..."
TEACHERS_RESPONSE=$(curl -s "$BASE_URL/api/v1/school/teachers?pageNum=1&pageSize=50" \
-H "Authorization: Bearer $TOKEN")
TEACHER_COUNT=$(echo $TEACHERS_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('total', 0))" 2>/dev/null)
echo " ✅ 教师总数: $TEACHER_COUNT"
# 7. 测试冲突检测API
echo ""
echo "7⃣ 测试冲突检测API..."
CONFLICT_RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/school/schedules/check-conflict" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "classId=1&teacherId=1&scheduledDate=2026-03-20&scheduledTime=09:00-10:00")
CONFLICT_CODE=$(echo $CONFLICT_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 'ERROR'))" 2>/dev/null)
CONFLICT_HAS=$(echo $CONFLICT_RESPONSE | python3 -c "import sys, json; d=json.load(sys.stdin)['data'].get('hasConflict', 'N/A')" 2>/dev/null)
if [ "$CONFLICT_CODE" = "200" ]; then
echo " ✅ 冲突检测API正常 (hasConflict=$CONFLICT_HAS)"
else
echo " ⚠️ 冲突检测API响应码: $CONFLICT_CODE"
fi
echo ""
echo "=================================================="
echo "✅ API测试完成"
echo "=================================================="

205
test_schedule_page.py Normal file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
School Scheduling Page Browser Test
Tests the scheduling functionality including Tab navigation and create modal
"""
from playwright.sync_api import sync_playwright
import time
def test_schedule_page():
with sync_playwright() as p:
# Launch browser in headed mode to see what's happening
browser = p.chromium.launch(headless=False, slow_mo=1000)
page = browser.new_page()
print("=" * 60)
print("🧪 学校端排课功能测试")
print("=" * 60)
# Step 1: Navigate to login page
print("\n1⃣ 导航到登录页面...")
page.goto('http://localhost:5174/login')
page.wait_for_load_state('networkidle')
print(" ✅ 登录页面加载完成")
# Step 2: Click school role button
print("\n2⃣ 选择学校角色...")
try:
# Try multiple selector strategies
role_btn = page.locator('.role-btn').filter(has_text='学校').first
if role_btn.count() == 0:
role_btn = page.get_by_role('button').filter(has_text='学校').first
if role_btn.count() == 0:
# Try text-based selector
role_btn = page.locator('text=学校').first
role_btn.click()
print(" ✅ 已点击学校角色按钮")
except Exception as e:
print(f" ⚠️ 点击学校角色失败: {e}")
# Continue anyway
# Step 3: Fill login form
print("\n3⃣ 填写登录表单...")
# Wait for the form to auto-fill based on role selection
page.wait_for_timeout(500)
# The form should auto-fill when role is selected, but let's verify
account_input = page.locator('input[placeholder*="账号"]').first
password_input = page.locator('input[placeholder*="密码"]').first
account_value = account_input.input_value()
print(f" 📝 账号输入框当前值: {account_value}")
# Fill if needed
if account_value != 'school1':
account_input.fill('school1')
password_input.fill('123456')
print(" ✅ 已填写账号和密码")
# Step 4: Click login button
print("\n4⃣ 点击登录按钮...")
# Use the login-btn class which is more specific
login_btn = page.locator('.login-btn, button[type="submit"], button:has-text("登录")').first
login_btn.click()
# Wait for navigation
try:
page.wait_for_url('**/school/**', timeout=15000)
print(" ✅ 登录成功,已跳转到学校端")
except Exception as e:
print(f" ⚠️ 等待跳转超时: {e}")
# Check current URL
current_url = page.url
print(f" 📍 当前URL: {current_url}")
page.wait_for_load_state('networkidle')
time.sleep(2)
# Step 5: Navigate to schedule page
print("\n5⃣ 导航到课程排期页面...")
page.goto('http://localhost:5174/school/schedule')
page.wait_for_load_state('networkidle')
time.sleep(2)
print(" ✅ 已导航到课程排期页面")
# Step 6: Take screenshot
print("\n6⃣ 截图保存...")
page.screenshot(path='screenshots/schedule-page-test.png', full_page=True)
print(" ✅ 截图已保存: screenshots/schedule-page-test.png")
# Step 7: Check page title
print("\n7⃣ 检查页面标题...")
try:
title_element = page.locator('h1, h2, .page-header').first
title = title_element.text_content()
print(f" ✅ 页面标题: {title}")
except:
print(" ⚠️ 未找到页面标题")
# Step 8: Check for Tab navigation
print("\n8⃣ 检查Tab导航...")
tabs = page.locator('[role="tab"], .ant-tabs-tab').all()
print(f" 📊 找到 {len(tabs)} 个Tab")
for i, tab in enumerate(tabs):
try:
text = tab.text_content()
print(f" Tab {i+1}: {text}")
except:
print(f" Tab {i+1}: (无法获取文本)")
# Step 9: Click through each tab
print("\n9⃣ 测试Tab切换...")
tab_labels = ['列表视图', '课表视图', '日历视图']
for label in tab_labels:
print(f"\n 🔄 切换到 {label}...")
try:
tab = page.locator('[role="tab"], .ant-tabs-tab').filter(has_text=label).first
if tab.count() > 0:
tab.click()
page.wait_for_timeout(1000)
screenshot_name = f'screenshots/schedule-{label.replace("视图", "")}-test.png'
page.screenshot(path=screenshot_name)
print(f" ✅ 已截图: {screenshot_name}")
# Check for content
if '列表' in label:
table = page.locator('.ant-table')
has_table = table.count() > 0
print(f" 📋 列表视图: {'有表格数据' if has_table else '无表格'}")
elif '课表' in label:
timetable = page.locator('.timetable-header, .day-header')
has_timetable = timetable.count() > 0
print(f" 📅 课表视图: {'有课表' if has_timetable else '无课表'}")
elif '日历' in label:
calendar = page.locator('.ant-picker-calendar')
has_calendar = calendar.count() > 0
print(f" 📆 日历视图: {'有日历' if has_calendar else '无日历'}")
else:
print(f" ⚠️ 未找到 {label} Tab")
except Exception as e:
print(f" ❌ 切换失败: {e}")
# Step 10: Try clicking "新建排课" button
print("\n🔟 测试新建排课按钮...")
try:
create_btn = page.locator('button:has-text("新建排课")').first
if create_btn.count() > 0:
create_btn.click()
page.wait_for_timeout(1500)
# Check if modal opened
modal = page.locator('.ant-modal').first
if modal.count() > 0:
print(" ✅ 新建排课弹窗已打开")
page.screenshot(path='screenshots/schedule-create-modal-test.png')
print(" ✅ 弹窗截图已保存")
# Check for dropdowns in modal
dropdowns = modal.locator('.ant-select').all()
print(f" 📝 弹窗中有 {len(dropdowns)} 个下拉框")
# Try clicking the first dropdown to see if it has course collections
if len(dropdowns) > 0:
dropdowns[0].click()
page.wait_for_timeout(500)
page.screenshot(path='screenshots/schedule-dropdown-test.png')
print(" ✅ 下拉框截图已保存")
else:
print(" ⚠️ 弹窗未打开")
else:
print(" ⚠️ 未找到新建排课按钮")
except Exception as e:
print(f" ❌ 新建排课测试失败: {e}")
# Final screenshot
print("\n📸 最终截图...")
page.screenshot(path='screenshots/schedule-final-test.png', full_page=True)
print(" ✅ 最终截图已保存")
# Get page content for debugging
print("\n📄 获取页面信息...")
current_url = page.url
print(f" 当前URL: {current_url}")
# Check for any error messages
error_msg = page.locator('.error-message, .ant-message-error, .ant-alert-error').all()
if error_msg:
print(" ⚠️ 发现错误消息:")
for msg in error_msg:
try:
text = msg.text_content()
print(f" - {text}")
except:
pass
print("\n" + "=" * 60)
print("✅ 测试完成!")
print("=" * 60)
# Keep browser open for a moment to see the result
time.sleep(3)
browser.close()
if __name__ == '__main__':
test_schedule_page()