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

508 lines
16 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('展播模式', () => {
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}: 访问失败`,
});
}
}
});
});