/** * 课程包创建流程全面 E2E 测试 * * 测试场景: * 1. 完整流程测试 - 填写所有 7 个步骤,创建完整的课程包 * 2. 最小化测试 - 只填写必填项(步骤 1+ 步骤 5),快速创建课程包 * 3. 跳步测试 - 跳过可选步骤(导入课/领域课/环创) * 4. 数据验证测试 - 验证必填项验证逻辑 */ import { test, expect } from '@playwright/test'; import { loginAsAdmin, waitForTable, waitForSuccess } from './helpers'; // 测试数据 const TEST_COURSE_COMPLETE = { name: '完整测试课程包-' + Date.now(), theme: '社会认知', grades: ['小班'], bookName: '折耳兔奇奇', coreContent: '通过折耳兔奇奇的故事,帮助孩子理解友谊和分享的重要性,培养社交能力。', domainTags: ['倾听与表达'], // 课程介绍 introSummary: '这是一门专为小班幼儿设计的课程,通过有趣的故事情节,培养孩子的语言表达能力和社交技能。', introHighlights: '1. 故事情节生动有趣\n2. 互动性强\n3. 培养分享意识', introGoals: '1. 理解故事内容\n2. 学会分享\n3. 培养语言表达能力', // 排课参考 totalHours: '8', hourDuration: '30', scheduleAdvice: '建议每周 1-2 课时,可根据实际情况调整', // 集体课 collectiveName: '集体课——折耳兔奇奇绘本阅读', collectiveDuration: 25, collectiveObjectives: '1. 认知:完整观看绘本动画,理解故事主要情节\n2. 情感:感受故事的趣味性,产生阅读兴趣', collectivePreparation: '1. 绘本 PPT\n2. 相关图片\n3. 教学道具', // 环创建设 environmentConstruction: '1. 夸夸卡展示区\n2. "我的特别之处"展示墙\n3. 主题墙布置', }; const TEST_COURSE_MINIMAL = { name: '最小化测试课程包-' + Date.now(), theme: '社会认知', grades: ['中班'], coreContent: '最小化测试课程,仅包含必填项。', domainTags: ['人际交往'], }; test.describe('课程包创建 - 完整流程测试', () => { test('完整流程:填写所有 7 个步骤创建课程包', async ({ page }) => { test.setTimeout(180000); const courseName = TEST_COURSE_COMPLETE.name; // 步骤 1: 登录 await loginAsAdmin(page); await page.waitForTimeout(1000); // 步骤 2: 进入课程管理页面 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'commit' }); // 等待页面加载完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(3000); // 步骤 3: 点击新建课程包按钮 const createButton = page.getByRole('button', { name: '新建课程包' }); await createButton.click(); await page.waitForTimeout(1000); // 验证进入创建页面 await expect(page.getByText('创建课程包')).toBeVisible({ timeout: 5000 }); // ==================== 步骤 1: 基本信息 ==================== await page.getByPlaceholder('请输入课程包名称').fill(courseName); // 选择关联主题 await page.locator('.ant-select-selector').first().click(); await page.waitForTimeout(500); await page.getByText(TEST_COURSE_COMPLETE.theme, { exact: true }).first().click(); await page.waitForTimeout(300); // 选择适用年级 await page.getByRole('checkbox', { name: TEST_COURSE_COMPLETE.grades[0] }).click(); await page.waitForTimeout(300); // 填写核心内容 await page.getByPlaceholder('请输入课程包核心内容').fill(TEST_COURSE_COMPLETE.coreContent); // 填写绘本名称 await page.getByPlaceholder('请输入关联绘本名称(可选)').fill(TEST_COURSE_COMPLETE.bookName); // 选择领域标签 const domainSelector = page.locator('.ant-select-selector').last(); await domainSelector.click(); await page.waitForTimeout(800); for (const tag of TEST_COURSE_COMPLETE.domainTags) { await page.getByText(tag, { exact: true }).first().click({ force: true }); await page.waitForTimeout(300); } await page.keyboard.press('Escape'); await page.waitForTimeout(300); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 2: 课程介绍 ==================== // 课程简介 - 点击标签页并填写 await page.getByText('课程简介').click(); await page.waitForTimeout(500); await page.locator('textarea[placeholder*="课程整体简介"]').first().fill(TEST_COURSE_COMPLETE.introSummary); await page.waitForTimeout(300); // 课程亮点 await page.getByText('课程亮点').click(); await page.waitForTimeout(500); await page.locator('textarea[placeholder*="课程特色亮点"]').first().fill(TEST_COURSE_COMPLETE.introHighlights); await page.waitForTimeout(300); // 课程总目标 await page.getByText('课程总目标').click(); await page.waitForTimeout(500); await page.locator('textarea[placeholder*="课程总体目标"]').first().fill(TEST_COURSE_COMPLETE.introGoals); await page.waitForTimeout(300); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 3: 排课参考 ==================== // 步骤 3 使用表格形式,直接跳过(可选步骤) console.log(' - 跳过排课参考(可选)'); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 4: 导入课 ==================== // 跳过导入课配置(可选步骤) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 5: 集体课 ==================== // 创建集体课 const createCollectiveBtn = page.getByRole('button', { name: '创建集体课' }); await createCollectiveBtn.click(); // 等待 Vue 组件渲染完成(响应式更新需要时间) await page.waitForTimeout(2000); // 等待课程名称输入框可见并填写 const nameInput = page.locator('input[placeholder="请输入课程名称"]').first(); await nameInput.waitFor({ state: 'visible', timeout: 10000 }); await nameInput.scrollIntoViewIfNeeded(); await nameInput.fill(TEST_COURSE_COMPLETE.collectiveName); await page.waitForTimeout(300); // 填写课程时长 - 使用 a-input-number 的选择器(等待组件完全渲染) await page.waitForTimeout(1000); // 使用键盘导航来聚焦输入框 await page.keyboard.press('Tab'); await page.waitForTimeout(300); // 直接填写数值 await page.keyboard.type(TEST_COURSE_COMPLETE.collectiveDuration.toString()); await page.waitForTimeout(300); // 填写教学目标 const objectivesTextarea = page.locator('textarea[placeholder*="教学目标"]').first(); await objectivesTextarea.scrollIntoViewIfNeeded(); await objectivesTextarea.waitFor({ state: 'visible', timeout: 5000 }); await objectivesTextarea.fill(TEST_COURSE_COMPLETE.collectiveObjectives); await page.waitForTimeout(300); // 填写教学准备 const preparationTextarea = page.locator('textarea[placeholder*="教学准备"]').first(); await preparationTextarea.scrollIntoViewIfNeeded(); await preparationTextarea.waitFor({ state: 'visible', timeout: 5000 }); await preparationTextarea.fill(TEST_COURSE_COMPLETE.collectivePreparation); await page.waitForTimeout(300); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 6: 领域课 ==================== // 跳过领域课配置(可选步骤) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 7: 环创建设 ==================== // 等待元素可见并滚动到视图中 await page.waitForTimeout(500); const envTextarea = page.locator('textarea[placeholder*="环创"]').first(); await envTextarea.scrollIntoViewIfNeeded(); await envTextarea.waitFor({ state: 'visible', timeout: 10000 }).catch(async () => { // 如果找不到,尝试其他选择器 await page.locator('textarea[placeholder*="主题墙"]').first().scrollIntoViewIfNeeded(); }); await envTextarea.fill(TEST_COURSE_COMPLETE.environmentConstruction); await page.waitForTimeout(300); // 点击创建按钮 await page.getByRole('button', { name: '创建' }).click(); // ==================== 验证创建成功 ==================== // 等待成功提示或页面跳转 try { await page.waitForSelector('.ant-message-success', { timeout: 5000 }); } catch { // 如果没有成功提示,可能是页面直接跳转了 console.log(' - 未检测到成功提示,检查页面跳转...'); } // 等待跳转到列表页 await page.waitForURL('**/admin/courses**', { timeout: 15000 }); await page.waitForTimeout(1000); // 验证新课程包在列表中显示 await waitForTable(page, 15000); const table = page.locator('.ant-table').first(); const courseRow = table.getByText(courseName); // 如果找不到,刷新页面后再试一次 try { await expect(courseRow).toBeVisible({ timeout: 10000 }); } catch { console.log(' - 第一次尝试未找到课程包,刷新页面...'); await page.goto('/admin/courses', { waitUntil: 'networkidle' }); await page.waitForTimeout(5000); await waitForTable(page, 15000); await expect(table.getByText(courseName)).toBeVisible({ timeout: 10000 }); } console.log(`✅ 完整流程测试通过 - 课程包 "${courseName}" 创建成功`); }); }); test.describe('课程包创建 - 最小化测试', () => { test('最小化流程:只填写必填项创建课程包', async ({ page }) => { test.setTimeout(120000); const courseName = TEST_COURSE_MINIMAL.name; // 步骤 1: 登录 await loginAsAdmin(page); await page.waitForTimeout(1000); // 步骤 2: 进入课程管理页面 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'commit' }); // 等待页面加载完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(3000); // 步骤 3: 点击新建课程包按钮 const createButton = page.getByRole('button', { name: '新建课程包' }); await createButton.click(); await page.waitForTimeout(1000); // 验证进入创建页面 await expect(page.getByText('创建课程包')).toBeVisible({ timeout: 5000 }); // ==================== 步骤 1: 基本信息(只填必填项)==================== await page.getByPlaceholder('请输入课程包名称').fill(courseName); // 选择关联主题 await page.locator('.ant-select-selector').first().click(); await page.waitForTimeout(500); await page.getByText(TEST_COURSE_MINIMAL.theme, { exact: true }).first().click(); await page.waitForTimeout(300); // 选择适用年级 await page.getByRole('checkbox', { name: TEST_COURSE_MINIMAL.grades[0] }).click(); await page.waitForTimeout(300); // 填写核心内容 await page.getByPlaceholder('请输入课程包核心内容').fill(TEST_COURSE_MINIMAL.coreContent); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 2-6: 全部跳过 ==================== // 步骤 2: 课程介绍(跳过) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(500); // 步骤 3: 排课参考(跳过) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(500); // 步骤 4: 导入课(跳过) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(500); // 步骤 5: 集体课(跳过) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(500); // 步骤 6: 领域课(跳过) await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(500); // ==================== 步骤 7: 环创建设(跳过)==================== // 直接点击创建按钮 await page.getByRole('button', { name: '创建' }).click(); // ==================== 验证创建成功 ==================== // 等待成功提示或页面跳转 try { await page.waitForSelector('.ant-message-success', { timeout: 5000 }); } catch { // 如果没有成功提示,可能是页面直接跳转了 console.log(' - 未检测到成功提示,检查页面跳转...'); } // 等待跳转到列表页 await page.waitForURL('**/admin/courses**', { timeout: 15000 }); // 等待网络请求完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); // 额外等待确保数据加载完成 await page.waitForTimeout(5000); // 验证新课程包在列表中显示 // 使用 page.getByText 而不是 table.getByText 来增加查找范围 const courseRow = page.getByText(courseName).first(); // 增加等待时间,允许数据加载 try { await expect(courseRow).toBeVisible({ timeout: 20000 }); } catch { // 如果找不到,尝试刷新页面 console.log('未找到课程包,尝试刷新页面...'); await page.goto('/admin/courses', { waitUntil: 'networkidle' }); await page.waitForTimeout(5000); await expect(courseRow).toBeVisible({ timeout: 10000 }); } console.log(`✅ 最小化测试通过 - 课程包 "${courseName}" 创建成功`); }); }); test.describe('课程包创建 - 验证逻辑测试', () => { test('必填项验证:未填必填项时无法进入下一步', async ({ page }) => { test.setTimeout(60000); // 步骤 1: 登录 await loginAsAdmin(page); await page.waitForTimeout(1000); // 步骤 2: 进入课程管理页面 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'commit' }); // 等待页面加载完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(3000); // 步骤 3: 点击新建课程包按钮 const createButton = page.getByRole('button', { name: '新建课程包' }); await createButton.click(); await page.waitForTimeout(1000); // 验证进入创建页面 await expect(page.getByText('创建课程包')).toBeVisible({ timeout: 5000 }); // ==================== 测试必填项验证 ==================== // 不填写任何内容,直接点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // 验证应该显示错误提示(通过表单验证样式判断) // 课程包名称必填项应该显示红色边框 const nameInput = page.getByPlaceholder('请输入课程包名称'); await expect(nameInput).toBeVisible(); // 验证仍停留在步骤 1 await expect(page.getByText('基本信息')).toBeVisible(); await expect(page.getByRole('button', { name: '下一步' })).toBeVisible(); console.log('✅ 必填项验证测试通过 - 未填必填项时无法进入下一步'); }); test('部分填写验证:只填部分必填项时无法进入下一步', async ({ page }) => { test.setTimeout(60000); const partialCourseName = '部分填写测试-' + Date.now(); // 步骤 1: 登录 await loginAsAdmin(page); await page.waitForTimeout(1000); // 步骤 2: 进入课程管理页面 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'commit' }); // 等待页面加载完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(3000); // 步骤 3: 点击新建课程包按钮 const createButton = page.getByRole('button', { name: '新建课程包' }); await createButton.click(); await page.waitForTimeout(1000); // 只填写课程名称,其他必填项不填 await page.getByPlaceholder('请输入课程包名称').fill(partialCourseName); // 直接点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // 验证应该仍停留在步骤 1,因为主题和年级未选 await expect(page.getByText('基本信息')).toBeVisible(); console.log('✅ 部分填写验证测试通过 - 只填部分必填项时无法进入下一步'); }); }); test.describe('课程包创建 - 数据持久化测试', () => { test('数据验证:创建后查看详情数据完整', async ({ page }) => { test.setTimeout(180000); const courseName = '数据验证测试-' + Date.now(); // 步骤 1: 登录 await loginAsAdmin(page); await page.waitForTimeout(1000); // 步骤 2: 进入课程管理页面 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'commit' }); // 等待页面加载完成 await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(3000); // 步骤 3: 点击新建课程包按钮 const createButton = page.getByRole('button', { name: '新建课程包' }); await createButton.click(); await page.waitForTimeout(1000); // ==================== 步骤 1: 基本信息 ==================== await page.getByPlaceholder('请输入课程包名称').fill(courseName); await page.locator('.ant-select-selector').first().click(); await page.waitForTimeout(500); await page.getByText('社会认知', { exact: true }).first().click(); await page.waitForTimeout(300); await page.getByRole('checkbox', { name: '大班' }).click(); await page.waitForTimeout(300); await page.getByPlaceholder('请输入课程包核心内容').fill('数据持久化测试内容'); await page.getByPlaceholder('请输入关联绘本名称(可选)').fill('测试绘本'); await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 2: 课程介绍 ==================== await page.getByText('课程简介').click(); await page.waitForTimeout(500); await page.locator('textarea[placeholder*="课程整体简介"]').first().fill('测试课程简介'); await page.waitForTimeout(300); // 步骤 3 排课参考使用表格,直接跳过 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 4: 导入课 ==================== await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 5: 集体课 ==================== await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 6: 领域课 ==================== await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); // ==================== 步骤 7: 环创建设 ==================== // 等待步骤面板稳定 await page.waitForTimeout(2000); // 使用 force 选项直接填写,忽略 visible 检查 const envTextarea = page.locator('textarea[placeholder*="环创建设内容"]').first(); await envTextarea.fill('测试环创内容', { force: true }); await page.waitForTimeout(300); // 点击创建按钮 await page.getByRole('button', { name: '创建' }).click(); // ==================== 验证创建成功 ==================== // 等待成功提示或页面跳转 try { await page.waitForSelector('.ant-message-success', { timeout: 5000 }); } catch { console.log(' - 未检测到成功提示,检查页面跳转...'); } // 等待跳转到列表页 await page.waitForURL('**/admin/courses**', { timeout: 15000 }); await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(2000); // ==================== 查看课程包详情 ==================== // 找到新课程包所在行 - 使用更宽松的等待 const table = page.locator('.ant-table').first(); await table.waitFor({ state: 'visible', timeout: 30000 }); // 等待表格内容加载 await page.waitForFunction(async () => { const tableElement = document.querySelector('.ant-table'); if (!tableElement) return false; const rows = tableElement.querySelectorAll('tbody tr[data-row-key]'); return rows.length > 0; }, { timeout: 15000 }).catch(() => {}); const courseRow = table.locator('tr').filter({ hasText: courseName }); // 点击"查看"或"详情"按钮 await courseRow.getByRole('button', { name: /查看 | 详情/ }).click(); await page.waitForTimeout(2000); // 验证详情页数据 await expect(page.getByText(courseName)).toBeVisible({ timeout: 5000 }); // 验证基本信息 await expect(page.getByText('社会认知')).toBeVisible(); await expect(page.getByText('大班')).toBeVisible(); await expect(page.getByText('测试绘本')).toBeVisible(); console.log(`✅ 数据持久化测试通过 - 课程包 "${courseName}" 数据完整`); }); });