后端重构:
- 添加统一响应格式 ResultDto<T> 和 PageResultDto<T>
- 添加分页查询 DTO 基类 PageQueryDto
- 添加响应转换拦截器 TransformInterceptor
- 添加公共工具函数(JSON 解析、分页计算)
- 配置 Swagger/OpenAPI 文档(访问路径:/api-docs)
- Tenant 模块 DTO 规范化示例(添加 @ApiProperty 装饰器)
- CourseLesson 控制器重构 - 移除类级路径参数,修复 Orval 验证错误
- 后端 DTO 规范化 - 为 Course、Lesson、TeacherCourse、SchoolCourse 控制器添加完整的 Swagger 装饰器
前端重构:
- 配置 Orval 从后端 OpenAPI 自动生成 API 客户端
- 生成 API 客户端代码(带完整参数定义)
- 创建 API 客户端统一入口 (src/api/client.ts)
- 创建 API 适配层 (src/api/teacher.adapter.ts)
- 配置文件路由 (unplugin-vue-router)
- 课程模块迁移到新 API 客户端
- 修复 PrepareModeView.vue API 调用错误
- 教师模块迁移到新 API 客户端
- 修复 school-course.ts 类型错误
- 清理 teacher.adapter.ts 未使用导入
- 修复 client.ts API 客户端结构
- 创建文件路由目录结构
Bug 修复:
- 修复路由配置问题 - 移除 top-level await,改用手动路由配置
- 修复响应拦截器 - 正确解包 { code, message, data } 格式的响应
- 清理 teacher.adapter.ts 未使用导入
- 修复 client.ts API 客户端结构
- 创建文件路由目录结构
系统测试:
- 后端 API 测试通过 (7/7)
- 前端路由测试通过 (4/4)
- 数据库完整性验证通过
- Orval API 客户端验证通过
- 超管端功能测试通过 (97.8% 通过率)
新增文件:
- reading-platform-backend/src/common/dto/result.dto.ts
- reading-platform-backend/src/common/dto/page-query.dto.ts
- reading-platform-backend/src/common/interceptors/transform.interceptor.ts
- reading-platform-backend/src/common/utils/json.util.ts
- reading-platform-backend/src/common/utils/pagination.util.ts
- reading-platform-frontend/orval.config.ts
- reading-platform-frontend/src/api/generated/mutator.ts
- reading-platform-frontend/src/api/client.ts
- reading-platform-frontend/src/api/teacher.adapter.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('Phase 6: 校本课程包功能测试', () => {
|
||
let baseURL = 'http://localhost:5173';
|
||
|
||
// 测试前准备
|
||
test.beforeEach(async ({ page }) => {
|
||
// 导航到登录页面
|
||
await page.goto(baseURL);
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// 选择教师角色
|
||
await page.click('text=教师');
|
||
await page.waitForTimeout(500);
|
||
|
||
// 登录(表单会自动填充测试账号)
|
||
const accountInput = page.locator('input[placeholder*="账号"]').or(page.locator('input[name="account"]'));
|
||
await accountInput.fill('teacher1');
|
||
|
||
const passwordInput = page.locator('input[placeholder*="密码"]').or(page.locator('input[type="password"]'));
|
||
await passwordInput.fill('123456');
|
||
|
||
// 点击登录按钮
|
||
const loginButton = page.locator('button[type="submit"]').or(page.locator('.login-btn')).or(page.locator('button:has-text("登录")'));
|
||
await loginButton.click();
|
||
|
||
// 等待登录成功 - 跳转到 dashboard 或 courses
|
||
await page.waitForURL('**/dashboard', { timeout: 10000 }).catch(() => {
|
||
// 如果没有跳转到 dashboard,可能已经跳转到其他页面,检查是否成功
|
||
return page.waitForURL('**/courses', { timeout: 5000 });
|
||
});
|
||
await page.waitForTimeout(1000);
|
||
});
|
||
|
||
test('测试1: 创建校本课程包流程', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 1. 进入课程中心
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 2. 点击第一个课程卡片(整个卡片可点击)
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
const cardCount = await firstCourseCard.count();
|
||
|
||
if (cardCount > 0) {
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 3. 检查是否有"创建校本版本"按钮
|
||
const createSchoolVersionButton = page.locator('button:has-text("创建校本版本")');
|
||
const buttonExists = await createSchoolVersionButton.count();
|
||
|
||
if (buttonExists > 0) {
|
||
// 记录当前URL
|
||
const beforeUrl = page.url();
|
||
|
||
// 4. 点击"创建校本版本"按钮
|
||
await createSchoolVersionButton.click();
|
||
|
||
// 5. 等待自动创建并跳转到编辑页面(系统自动创建并跳转)
|
||
await page.waitForURL('**/school-courses/**/edit', { timeout: 15000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 6. 验证页面标题显示正确
|
||
await expect(page.locator('text=创建校本课程包').or(page.locator('text=编辑校本课程包'))).toBeVisible();
|
||
|
||
// 7. 验证URL包含school-courses编辑页路径
|
||
const currentUrl = page.url();
|
||
expect(currentUrl).toMatch(/\/school-courses\/\d+\/edit/);
|
||
|
||
test.info().annotations.push({
|
||
type: 'result',
|
||
description: `创建成功,从 ${beforeUrl} 跳转到 ${currentUrl}`,
|
||
});
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到"创建校本版本"按钮',
|
||
});
|
||
}
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到课程卡片',
|
||
});
|
||
}
|
||
});
|
||
|
||
test('测试2: 个人课程中心列表', async ({ page }) => {
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 验证页面标题
|
||
await expect(page.locator('text=我的校本课程包').or(page.locator('text=校本课程包')).first()).toBeVisible({ timeout: 5000 });
|
||
|
||
// 3. 检查保存位置筛选器
|
||
const filterTabs = page.locator('text=全部').or(page.locator('text=个人')).or(page.locator('text=校本'));
|
||
const filterCount = await filterTabs.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `筛选器数量: ${filterCount}`,
|
||
});
|
||
|
||
// 4. 检查课程列表
|
||
const courseCards = page.locator('.ant-card, [class*="course"], [class*="Course"]');
|
||
const courseCount = await courseCards.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `课程数量: ${courseCount}`,
|
||
});
|
||
|
||
// 截图
|
||
await page.screenshot({ path: 'test-results/school-course-list.png' });
|
||
});
|
||
|
||
test('测试3: 编辑校本课程包', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 查找编辑按钮
|
||
const editButton = page.locator('button:has-text("编辑")').first();
|
||
|
||
if (await editButton.count() > 0) {
|
||
await editButton.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 3. 验证进入编辑页面
|
||
const url = page.url();
|
||
expect(url).toContain('/edit');
|
||
|
||
// 4. 检查步骤导航
|
||
const steps = page.locator('.ant-steps-item, [class*="step"]');
|
||
const stepCount = await steps.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `编辑步骤数量: ${stepCount}`,
|
||
});
|
||
|
||
// 5. 尝试切换步骤
|
||
const step2 = page.locator('text=课程介绍').or(page.locator('text=步骤2'));
|
||
if (await step2.count() > 0) {
|
||
await step2.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 6. 保存修改
|
||
const saveButton = page.locator('button:has-text("保存")').or(page.locator('button:has-text("保存到个人")'));
|
||
if (await saveButton.count() > 0) {
|
||
await saveButton.first().click();
|
||
await page.waitForTimeout(2000);
|
||
}
|
||
|
||
// 截图
|
||
await page.screenshot({ path: 'test-results/school-course-edit.png' });
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到可编辑的课程',
|
||
});
|
||
}
|
||
});
|
||
|
||
test('测试4: 查看校本课程详情', async ({ page }) => {
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 查找查看按钮
|
||
const viewButton = page.locator('button:has-text("查看")').first();
|
||
|
||
if (await viewButton.count() > 0) {
|
||
await viewButton.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 3. 验证详情页面
|
||
const url = page.url();
|
||
expect(url).toContain('/school-courses/');
|
||
|
||
// 4. 检查详情页内容
|
||
const detailElements = page.locator('text=开始备课').or(page.locator('text=课程介绍'));
|
||
await expect(detailElements.first()).toBeVisible({ timeout: 5000 });
|
||
|
||
// 截图
|
||
await page.screenshot({ path: 'test-results/school-course-detail.png' });
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到可查看的课程',
|
||
});
|
||
}
|
||
});
|
||
|
||
test('测试5: 备课模式', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 查找"开始备课"按钮
|
||
const prepareButton = page.locator('button:has-text("开始备课")').first();
|
||
|
||
if (await prepareButton.count() > 0) {
|
||
await prepareButton.click();
|
||
await page.waitForTimeout(3000);
|
||
|
||
// 3. 验证进入备课模式
|
||
const url = page.url();
|
||
expect(url).toContain('/prepare');
|
||
|
||
// 4. 检查备课模式布局
|
||
const navigation = page.locator('aside, [class*="navigation"], [class*="sidebar"]');
|
||
await expect(navigation.first()).toBeVisible({ timeout: 5000 });
|
||
|
||
// 5. 检查课程列表
|
||
const lessonItems = page.locator('[class*="lesson"], [class*="course"]');
|
||
const lessonCount = await lessonItems.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `备课模式课程数量: ${lessonCount}`,
|
||
});
|
||
|
||
// 截图
|
||
await page.screenshot({ path: 'test-results/prepare-mode.png' });
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到"开始备课"按钮',
|
||
});
|
||
}
|
||
});
|
||
|
||
test('测试6: 删除校本课程包', async ({ page }) => {
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 记录删除前的课程数量
|
||
const courseCards = page.locator('.ant-card, [class*="course"]');
|
||
const beforeCount = await courseCards.count();
|
||
|
||
// 3. 查找删除按钮
|
||
const deleteButton = page.locator('button:has-text("删除")').first();
|
||
|
||
if (await deleteButton.count() > 0) {
|
||
// 4. 点击删除
|
||
await deleteButton.click();
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 5. 确认删除
|
||
const confirmButton = page.locator('button:has-text("确定")').or(page.locator('button:has-text("确认")'));
|
||
if (await confirmButton.count() > 0) {
|
||
await confirmButton.first().click();
|
||
await page.waitForTimeout(2000);
|
||
}
|
||
|
||
// 6. 验证删除成功
|
||
const afterCount = await courseCards.count();
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `删除前: ${beforeCount}, 删除后: ${afterCount}`,
|
||
});
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到删除按钮',
|
||
});
|
||
}
|
||
});
|
||
|
||
test('测试7: 筛选功能', async ({ page }) => {
|
||
// 1. 进入校本课程包页面
|
||
await page.click('text=校本课程包');
|
||
await page.waitForURL('**/school-courses', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 查找筛选器
|
||
const personalFilter = page.locator('text=个人').or(page.locator('[role="tab"]:has-text("个人")'));
|
||
const schoolFilter = page.locator('text=校本').or(page.locator('[role="tab"]:has-text("校本")'));
|
||
|
||
if (await personalFilter.count() > 0) {
|
||
// 3. 点击"个人"筛选
|
||
await personalFilter.first().click();
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 4. 点击"校本"筛选
|
||
if (await schoolFilter.count() > 0) {
|
||
await schoolFilter.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
test.info().annotations.push({
|
||
type: 'success',
|
||
description: '筛选功能测试完成',
|
||
});
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到筛选器',
|
||
});
|
||
}
|
||
|
||
// 截图
|
||
await page.screenshot({ path: 'test-results/school-course-filter.png' });
|
||
});
|
||
});
|