kindergarten_java/reading-platform-frontend/tests/e2e/phase6-school-course/school-course.spec.ts
Claude Opus 4.6 e87e2dde00 refactor: 完成代码重构规范化 - 2026-03-12
后端重构:
- 添加统一响应格式 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>
2026-03-12 17:27:13 +08:00

322 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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' });
});
});