kindergarten_java/lesingle-edu-reading-platform-frontend/tests/e2e/phase6-school-course/school-course.spec.ts

322 lines
11 KiB
TypeScript
Raw Normal View History

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