kindergarten_java/reading-platform-frontend/tests/e2e/course-view-flow/course-view.spec.ts

387 lines
13 KiB
TypeScript
Raw Permalink 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('标准课程包查看流程', () => {
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();
// 等待登录成功
await page.waitForURL('**/dashboard', { timeout: 10000 }).catch(() => {
return page.waitForURL('**/courses', { timeout: 5000 });
});
await page.waitForTimeout(1000);
});
test('测试1: 课程中心列表加载', async ({ page }) => {
// 1. 进入课程中心
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(2000);
// 2. 验证页面标题
await expect(page.locator('text=课程中心').or(page.locator('.page-title'))).toBeVisible();
// 3. 验证筛选栏存在
const filterBar = page.locator('.filter-bar').or(page.locator('[class*="filter"]'));
await expect(filterBar.first()).toBeVisible();
// 4. 验证课程卡片加载
const courseCards = page.locator('.course-card').or(page.locator('[class*="course-card"]'));
const cardCount = await courseCards.count();
test.info().annotations.push({
type: 'info',
description: `课程卡片数量: ${cardCount}`,
});
// 5. 验证至少有一个课程
expect(cardCount).toBeGreaterThan(0);
// 6. 验证课程卡片元素
const firstCard = courseCards.first();
await expect(firstCard.locator('.course-title').or(page.locator('h3'))).toBeVisible();
// 截图
await page.screenshot({ path: 'test-results/course-list.png' });
});
test('测试2: 筛选功能', async ({ page }) => {
test.slow();
// 1. 进入课程中心
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(2000);
// 2. 测试年级筛选
const gradeFilter = page.locator('.filter-item').filter({ hasText: '年级' }).locator('.ant-select');
if (await gradeFilter.count() > 0) {
await gradeFilter.first().click();
await page.waitForTimeout(500);
await page.click('text=小班');
await page.waitForTimeout(1500);
}
// 3. 测试领域筛选
const domainFilter = page.locator('.filter-item').filter({ hasText: '领域' }).locator('.ant-select');
if (await domainFilter.count() > 0) {
await domainFilter.first().click();
await page.waitForTimeout(500);
await page.click('text=语言');
await page.waitForTimeout(1500);
}
// 4. 测试搜索功能
const searchInput = page.locator('input[placeholder*="搜索"]');
if (await searchInput.count() > 0) {
await searchInput.first().fill('测试');
await page.waitForTimeout(2000);
}
// 截图
await page.screenshot({ path: 'test-results/course-filter.png' });
test.info().annotations.push({
type: 'success',
description: '筛选功能测试完成',
});
});
test('测试3: 课程详情页完整信息', 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();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 3. 验证课程详情页URL
const url = page.url();
expect(url).toContain('/courses/');
// 4. 验证课程基本信息
await expect(page.locator('.course-title, h1').first()).toBeVisible();
await expect(page.locator('text=开始备课')).toBeVisible();
await expect(page.locator('text=选择课程上课')).toBeVisible();
// 5. 验证Tab导航存在
const tabs = page.locator('.ant-tabs-tab');
const tabCount = await tabs.count();
test.info().annotations.push({
type: 'info',
description: `Tab数量: ${tabCount}`,
});
// 6. 验证核心内容Tab
const introTab = page.locator('text=课程介绍').or(page.locator('[class*="intro"]'));
if (await introTab.count() > 0) {
await introTab.first().click();
await page.waitForTimeout(1000);
}
// 7. 验证课程配置Tab
const lessonsTab = page.locator('text=课程配置').or(page.locator('[class*="lesson"]'));
if (await lessonsTab.count() > 0) {
await lessonsTab.first().click();
await page.waitForTimeout(1000);
}
// 8. 验证数字资源Tab
const resourcesTab = page.locator('text=数字资源').or(page.locator('[class*="resource"]'));
if (await resourcesTab.count() > 0) {
await resourcesTab.first().click();
await page.waitForTimeout(1000);
}
// 截图
await page.screenshot({ path: 'test-results/course-detail.png', fullPage: true });
});
test('测试4: 课程介绍内容验证', async ({ page }) => {
test.slow();
// 1. 进入课程中心并选择课程
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 2. 点击课程介绍Tab
const introTab = page.locator('text=课程介绍');
if (await introTab.count() > 0) {
await introTab.first().click();
await page.waitForTimeout(1000);
}
// 3. 验证核心内容
const coreContent = page.locator('text=核心内容').or(page.locator('[class*="core"]'));
if (await coreContent.count() > 0) {
await expect(coreContent.first()).toBeVisible();
}
// 4. 验证8个富文本字段
const introFields = [
'课程简介',
'课程亮点',
'课程总目标',
'课程内容安排',
'教学重难点',
'教学方法',
'课程评价',
'注意事项'
];
let visibleFieldCount = 0;
for (const field of introFields) {
const fieldLocator = page.locator(`text=${field}`);
if (await fieldLocator.count() > 0) {
visibleFieldCount++;
}
}
test.info().annotations.push({
type: 'info',
description: `可见介绍字段: ${visibleFieldCount}/${introFields.length}`,
});
// 截图
await page.screenshot({ path: 'test-results/course-intro.png', fullPage: true });
});
test('测试5: 课程配置验证', async ({ page }) => {
test.slow();
// 1. 进入课程中心并选择课程
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 2. 点击课程配置Tab
const lessonsTab = page.locator('text=课程配置');
if (await lessonsTab.count() > 0) {
await lessonsTab.first().click();
await page.waitForTimeout(1000);
}
// 3. 验证导入课存在
const introLesson = page.locator('text=导入课').or(page.locator('[class*="introduction"]'));
const introCount = await introLesson.count();
// 4. 验证集体课存在
const collectiveLesson = page.locator('text=集体课').or(page.locator('[class*="collective"]'));
const collectiveCount = await collectiveLesson.count();
// 5. 验证五大领域课
const domainLessons = ['健康', '语言', '社会', '科学', '艺术'];
let visibleDomainCount = 0;
for (const domain of domainLessons) {
const domainLocator = page.locator(`text=${domain}领域`);
if (await domainLocator.count() > 0) {
visibleDomainCount++;
}
}
test.info().annotations.push({
type: 'info',
description: `导入课: ${introCount > 0 ? '✓' : '✗'}, 集体课: ${collectiveCount > 0 ? '✓' : '✗'}, 领域课: ${visibleDomainCount}/5`,
});
// 6. 验证教学环节展开
const stepContent = page.locator('[class*="step"]').or(page.locator('[class*="process"]'));
if (await stepContent.count() > 0) {
await expect(stepContent.first()).toBeVisible();
}
// 截图
await page.screenshot({ path: 'test-results/course-lessons.png', fullPage: true });
});
test('测试6: 数字资源验证', async ({ page }) => {
test.slow();
// 1. 进入课程中心并选择课程
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 2. 点击数字资源Tab
const resourcesTab = page.locator('text=数字资源');
if (await resourcesTab.count() > 0) {
await resourcesTab.first().click();
await page.waitForTimeout(1000);
}
// 3. 验证资源分类
const resourceTypes = ['图片', '视频', '音频', 'PPT', 'PDF'];
const resourceResults: Record<string, boolean> = {};
for (const type of resourceTypes) {
const typeLocator = page.locator(`text=${type}`);
resourceResults[type] = await typeLocator.count() > 0;
}
test.info().annotations.push({
type: 'info',
description: `资源类型: ${JSON.stringify(resourceResults)}`,
});
// 4. 验证资源列表或空状态
const resourceList = page.locator('[class*="resource"]').or(page.locator('[class*="file"]'));
const emptyState = page.locator('text=暂无资源').or(page.locator('[class*="empty"]'));
const hasResources = await resourceList.count() > 0;
const hasEmptyState = await emptyState.count() > 0;
test.info().annotations.push({
type: 'info',
description: hasResources ? '有资源文件' : (hasEmptyState ? '显示空状态' : '未找到资源区域'),
});
// 截图
await page.screenshot({ path: 'test-results/course-resources.png', fullPage: true });
});
test('测试7: 收藏功能', async ({ page }) => {
// 1. 进入课程中心并选择课程
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 2. 查找收藏按钮
const favoriteButton = page.locator('text=收藏').or(page.locator('[class*="favorite"]'));
const favoriteExists = await favoriteButton.count() > 0;
if (favoriteExists) {
// 3. 点击收藏
await favoriteButton.first().click();
await page.waitForTimeout(1000);
// 4. 验证收藏状态变化
test.info().annotations.push({
type: 'success',
description: '收藏功能测试完成',
});
} else {
test.info().annotations.push({
type: 'warning',
description: '未找到收藏按钮',
});
}
});
test('测试8: 从课程详情进入备课', async ({ page }) => {
// 1. 进入课程中心并选择课程
await page.click('text=课程中心');
await page.waitForURL('**/courses', { timeout: 10000 });
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
// 2. 点击"开始备课"按钮
const prepareButton = page.locator('button:has-text("开始备课")');
await expect(prepareButton).toBeVisible();
await prepareButton.click();
// 3. 验证跳转到备课模式
await page.waitForURL('**/prepare', { timeout: 10000 });
await page.waitForTimeout(2000);
// 4. 验证备课模式布局
await expect(page.locator('aside, [class*="navigation"], [class*="sidebar"]').first()).toBeVisible();
// 5. 验证课程列表
const lessonItems = page.locator('[class*="lesson"], [class*="course"]');
const lessonCount = await lessonItems.count();
expect(lessonCount).toBeGreaterThan(0);
test.info().annotations.push({
type: 'success',
description: `成功进入备课模式,课程数量: ${lessonCount}`,
});
// 截图
await page.screenshot({ path: 'test-results/enter-prepare-mode.png' });
});
});