kindergarten_java/reading-platform-frontend/tests/e2e/broadcast-flow/broadcast.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

508 lines
16 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('展播模式', () => {
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 }) => {
test.slow();
// 1. 进入上课模式
await page.click('text=上课记录');
await page.waitForTimeout(2000);
const recordItems = page.locator('[class*="record"], [class*="lesson"]');
if (await recordItems.count() > 0) {
await recordItems.first().click();
await page.waitForTimeout(2000);
} else {
await page.click('text=课程中心');
await page.waitForTimeout(1000);
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
await firstCourseCard.click();
await page.waitForTimeout(2000);
const selectLessonsButton = page.locator('button:has-text("选择课程上课")');
if (await selectLessonsButton.count() > 0) {
await selectLessonsButton.click();
await page.waitForTimeout(2000);
const confirmButton = page.locator('button:has-text("确定")');
if (await confirmButton.count() > 0) {
await confirmButton.click();
await page.waitForTimeout(3000);
}
}
}
await page.waitForTimeout(2000);
// 2. 查找展播模式按钮
const broadcastButton = page.locator('button:has-text("展播")').or(page.locator('button:has-text("全屏")'));
const buttonCount = await broadcastButton.count();
if (buttonCount > 0) {
// 3. 点击展播模式(新标签页)
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.first().click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 4. 验证新标签页URL
const broadcastUrl = newPage.url();
test.info().annotations.push({
type: 'success',
description: `展播模式已打开: ${broadcastUrl}`,
});
// 5. 验证展播模式全屏状态
const isFullscreen = await newPage.locator('[class*="fullscreen"], [class*="broadcast"]').count() > 0;
test.info().annotations.push({
type: 'info',
description: isFullscreen ? '进入全屏展播模式' : '展播页面已打开',
});
await newPage.screenshot({ path: 'test-results/broadcast-open.png', fullPage: true });
await newPage.close();
} else {
test.info().annotations.push({
type: 'warning',
description: '未找到展播模式按钮',
});
}
});
test('测试2: 展播模式布局验证', async ({ page }) => {
test.slow();
// 1. 直接访问展播模式URL如果有上课记录ID
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
// 2. 查找展播入口
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 3. 验证展播页面结构
const mainContainer = newPage.locator('main, [class*="container"], [class*="broadcast"]');
expect(await mainContainer.count()).toBeGreaterThan(0);
// 4. 验证内容显示区
const contentArea = newPage.locator('[class*="content"], [class*="display"]');
expect(await contentArea.count()).toBeGreaterThan(0);
// 5. 验证控制按钮区域
const controls = newPage.locator('[class*="control"], [class*="button"]');
const controlCount = await controls.count();
test.info().annotations.push({
type: 'info',
description: `控制按钮数量: ${controlCount}`,
});
await newPage.screenshot({ path: 'test-results/broadcast-layout.png', fullPage: true });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试3: 展播模式Kids Mode展示', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 验证Kids Mode组件显示
const kidsMode = newPage.locator('[class*="kids"], [class*="child"], [class*="display"]');
const hasKidsMode = await kidsMode.count() > 0;
test.info().annotations.push({
type: 'info',
description: hasKidsMode ? 'Kids Mode组件已显示' : '未找到Kids Mode',
});
// 2. 验证内容元素
if (hasKidsMode) {
const images = kidsMode.first().locator('img');
const imageCount = await images.count();
const textContent = await kidsMode.first().allTextContents();
const textCount = textContent.filter(t => t.trim()).length;
test.info().annotations.push({
type: 'info',
description: `图片数量: ${imageCount}, 文本段落数: ${textCount}`,
});
}
await newPage.screenshot({ path: 'test-results/broadcast-kids-mode.png', fullPage: true });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试4: 展播模式全屏功能', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 检查是否自动全屏
const isFullscreen = await newPage.evaluate(() => {
return document.fullscreenElement != null ||
document.webkitFullscreenElement != null ||
document.mozFullScreenElement != null;
});
test.info().annotations.push({
type: 'info',
description: isFullscreen ? '已自动进入全屏模式' : '未自动全屏',
});
// 2. 手动触发全屏(如果未自动全屏)
if (!isFullscreen) {
const fullscreenButton = newPage.locator('button:has-text("全屏")').or(page.locator('[class*="fullscreen"]'));
if (await fullscreenButton.count() > 0) {
await fullscreenButton.first().click();
await newPage.waitForTimeout(1000);
}
}
await newPage.screenshot({ path: 'test-results/broadcast-fullscreen.png', fullPage: true });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试5: 展播模式环节切换', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 查找环节导航
const stepNav = newPage.locator('[class*="step"], [class*="nav"], .ant-steps');
const stepCount = await stepNav.count();
// 2. 尝试下一个环节
const nextButton = newPage.locator('button:has-text("下一")').or(page.locator('button:has-text("下一步")'));
const nextCount = await nextButton.count();
if (nextCount > 0) {
await nextButton.first().click();
await newPage.waitForTimeout(1500);
test.info().annotations.push({
type: 'success',
description: '切换到下一个环节',
});
}
// 3. 尝试上一个环节
const prevButton = newPage.locator('button:has-text("上一")').or(page.locator('button:has-text("上一步")'));
const prevCount = await prevButton.count();
if (prevCount > 0) {
await prevButton.first().click();
await newPage.waitForTimeout(1500);
}
test.info().annotations.push({
type: 'info',
description: `导航区域: ${stepCount}, 下一按钮: ${nextCount}, 上一按钮: ${prevCount}`,
});
await newPage.screenshot({ path: 'test-results/broadcast-switch-step.png' });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试6: 展播模式键盘控制', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 测试空格键
await newPage.keyboard.press('Space');
await newPage.waitForTimeout(1000);
// 2. 测试左右箭头
await newPage.keyboard.press('ArrowRight');
await newPage.waitForTimeout(1000);
await newPage.keyboard.press('ArrowLeft');
await newPage.waitForTimeout(1000);
// 3. 测试ESC退出
await newPage.keyboard.press('Escape');
await newPage.waitForTimeout(1000);
test.info().annotations.push({
type: 'success',
description: '键盘控制测试完成',
});
await newPage.screenshot({ path: 'test-results/broadcast-keyboard.png' });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试7: 展播模式资源展示', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 查找图片资源
const images = newPage.locator('img');
const imageCount = await images.count();
// 2. 查找视频资源
const videos = newPage.locator('video');
const videoCount = await videos.count();
// 3. 查找文字内容
const textElements = await newPage.locator('body').allTextContents();
const textLength = textElements.join('').length;
test.info().annotations.push({
type: 'info',
description: `图片: ${imageCount}, 视频: ${videoCount}, 文字长度: ${textLength}`,
});
await newPage.screenshot({ path: 'test-results/broadcast-resources.png', fullPage: true });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试8: 展播模式颜色主题', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 检查背景色(深色主题适合投影)
const backgroundColor = await newPage.evaluate(() => {
return window.getComputedStyle(document.body).backgroundColor;
});
// 2. 检查文字颜色(高对比度)
const textColor = await newPage.evaluate(() => {
const mainElement = document.querySelector('main, [class*="content"]');
return mainElement ? window.getComputedStyle(mainElement).color : 'rgb(0, 0, 0)';
});
test.info().annotations.push({
type: 'info',
description: `背景色: ${backgroundColor}, 文字色: ${textColor}`,
});
await newPage.screenshot({ path: 'test-results/broadcast-theme.png', fullPage: true });
await newPage.close();
} else {
test.skip('无法找到展播入口');
}
});
test('测试9: 展播模式关闭和返回', async ({ page }) => {
test.slow();
// 进入展播模式
await page.goto('http://localhost:5173/teacher/lessons');
await page.waitForTimeout(2000);
const broadcastButton = page.locator('button:has-text("展播")');
if (await broadcastButton.count() > 0) {
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
broadcastButton.click()
]);
await newPage.waitForLoadState('networkidle');
await newPage.waitForTimeout(2000);
// 1. 查找关闭按钮
const closeButton = newPage.locator('button:has-text("关闭")').or(page.locator('.ant-modal-close'));
const closeCount = await closeButton.count();
// 2. 查找返回按钮
const backButton = newPage.locator('button:has-text("返回")').or(page.locator('[class*="back"]'));
const backCount = await backButton.count();
test.info().annotations.push({
type: 'info',
description: `关闭按钮: ${closeCount}, 返回按钮: ${backCount}`,
});
// 3. 测试ESC关闭
await newPage.keyboard.press('Escape');
await newPage.waitForTimeout(1000);
// 4. 检查页面是否还在
const isStillOpen = await newPage.evaluate(() => document.readyState !== 'unloaded');
test.info().annotations.push({
type: 'info',
description: isStillOpen ? '页面仍然打开' : '页面已关闭',
});
if (isStillOpen) {
await newPage.close();
}
} else {
test.skip('无法找到展播入口');
}
});
test('测试10: 展播模式URL参数支持', async ({ page }) => {
test.slow();
// 1. 测试带step参数的URL
const testUrls = [
'http://localhost:5173/teacher/broadcast/1',
'http://localhost:5173/teacher/lessons/1/broadcast',
];
for (const testUrl of testUrls) {
try {
await page.goto(testUrl);
await page.waitForTimeout(2000);
// 2. 检查页面是否正常加载
const content = page.locator('main, [class*="content"]');
const hasContent = await content.count() > 0;
test.info().annotations.push({
type: 'info',
description: `${testUrl}: ${hasContent ? '加载成功' : '加载失败'}`,
});
if (hasContent) {
await page.screenshot({ path: `test-results/broadcast-url-${testUrls.indexOf(testUrl)}.png` });
}
} catch (e) {
test.info().annotations.push({
type: 'warning',
description: `${testUrl}: 访问失败`,
});
}
}
});
});