/** * 课程包创建 E2E 测试 - 基于"折耳兔奇奇测试课程 01"模板 * * 测试流程: * 1. 超管登录 * 2. 进入课程管理页面 * 3. 新建课程包 * 4. 完成 7 个步骤(基本信息、课程介绍、排课参考、导入课、集体课、领域课、环创建设) * 5. 验证创建成功 * * 数据来源:基于提供的 JSON 模板数据简化 */ import { test, expect } from '@playwright/test'; import { loginAsAdmin, waitForTable, waitForSuccess } from './helpers'; // 测试数据 - 基于 JSON 模板 const TEST_COURSE = { // 基本信息 name: '折耳兔奇奇测试课程 01', theme: '社会认知', // 使用实际存在的主题 grades: ['小班'] as string[], domainTags: ['社会适应'] as string[], pictureBookName: '折耳兔奇奇', coreContent: '通过折耳兔奇奇的故事,帮助孩子理解友谊和分享的重要性', duration: 25, // 课程介绍 introSummary: '本课程以绘本《折耳兔奇奇》为核心载体,面向幼儿园小班幼儿,紧扣"接受认同自己"的核心主题,帮助幼儿建立自信、乐观的心态。', introHighlights: '1. 主题鲜明,情感真挚\n2. 领域融合,全面发展\n3. 形式多样,趣味十足\n4. 家园协同,全程联动\n5. 环创赋能,延伸教育', introGoals: '1. 认知目标:理解《折耳兔奇奇》绘本核心情节\n2. 能力目标:提升幼儿语言表达、社会交往能力\n3. 情感目标:感受奇奇从自卑到自信的情绪变化', introSchedule: '周一:导入课认识奇奇;周二:语言领域绘本解读;周三:社会领域夸夸我和你;周四:健康领域保护耳朵;周五:科学领域兔子的小秘密', introKeyPoints: '1. 掌握绘本核心内容\n2. 落实五大领域目标\n3. 推动家校协同', introMethods: '1. 绘本阅读法\n2. 课件演示法\n3. 互动游戏法\n4. 情境教学法', introEvaluation: '1. 过程性评价\n2. 作品评价\n3. 家园评价\n4. 总结性评价', introNotes: '1. 尊重幼儿个体差异\n2. 严格控制各环节课时时长\n3. 注重安全保障', // 排课参考 totalLessons: 5, lessonDuration: 25, scheduleAdvice: '建议每周 1-2 课时,按导入课→集体课→五大领域主题课顺序推进', // 导入课 introLessonName: '导入课——认识奇奇,发现我的特别之处', introLessonDuration: 10, introLessonObjectives: '1. 认知目标:认识绘本主角折耳兔奇奇,能准确说出奇奇最明显的外形特点(折耳朵)\n2. 能力目标:能大胆、主动地说出自己的一个特别之处\n3. 情感目标:乐于参与集体互动,愿意分享自己的特点', introLessonPreparation: '1. 物质准备:折耳兔奇奇毛绒玩具 1 个、绘本封面放大图 1 张、轻音乐\n2. 经验准备:幼儿有观察自己和同伴外貌、爱好的经验', // 集体课 collectiveLessonName: '集体课——折耳兔奇奇绘本阅读', collectiveLessonDuration: 25, collectiveLessonObjectives: '1. 认知:完整观看绘本动画,熟悉绘本主要情节和角色\n2. 能力:能跟随教师朗读简单的绘本对话\n3. 情感:感受绘本的趣味性,初步体会奇奇的烦恼', // 领域课 - 健康领域 healthLessonName: '健康领域——保护我的"小耳朵"', healthLessonDuration: 25, healthLessonObjectives: '1. 认知:知道耳朵是我们的重要器官,了解耳朵的作用\n2. 能力:学习保护耳朵的正确方法\n3. 情感:愿意主动保护自己的身体器官', // 领域课 - 科学领域 scienceLessonName: '科学领域——兔子的小秘密', scienceLessonDuration: 25, scienceLessonObjectives: '1. 认知:了解兔子的基本特征和生活习性\n2. 能力:能通过观察、对比发现不同兔子的外形区别\n3. 情感:对兔子产生探究兴趣,爱护小动物', // 环创建设 environmentConstruction: '1. 夸夸卡展示区:展示幼儿制作的夸夸卡\n2. "我的特别之处"展示墙:张贴幼儿分享的特别之处作品\n3. 兔子探秘墙:张贴兔子图片和观察记录\n4. 音乐角环创:张贴儿歌歌词图谱、兔子头饰\n5. 健康小卫士展示区:张贴保护耳朵方法海报', }; test.describe('课程包创建 - 折耳兔奇奇测试课程 01', () => { test('完整流程:创建课程包并验证', async ({ page }) => { test.setTimeout(300000); // 5 分钟超时 // ==================== 步骤 1: 登录 ==================== console.log('\n========== 开始登录 =========='); await loginAsAdmin(page); await page.waitForURL('**/admin/**', { timeout: 15000 }).catch(() => { console.log('警告:URL 等待超时,但继续执行'); }); await page.waitForTimeout(2000); // 增加等待时间确保页面完全加载 console.log('✅ 登录成功'); // ==================== 步骤 2: 进入课程管理页面 ==================== console.log('\n========== 进入课程管理页面 =========='); // 直接导航到课程列表页 await page.goto('/admin/courses', { timeout: 30000, waitUntil: 'networkidle' }); await page.waitForTimeout(3000); // 等待数据加载 // 验证页面元素(不强制要求表格存在) const pageContainer = page.locator('.package-list-page, .course-list-page, [class*="course"], [class*="package"]').first(); if (await pageContainer.isVisible({ timeout: 5000 })) { console.log('✅ 课程管理页面加载完成'); } else { console.log('⚠️ 未找到预期的页面容器,但继续执行'); } // ==================== 步骤 3: 点击新建课程包按钮 ==================== console.log('\n========== 点击新建课程包 =========='); // 使用 CSS 选择器定位 Ant Design Vue 按钮 const createButton = page.locator('button:has-text("新建课程包")').first(); await createButton.waitFor({ state: 'visible', timeout: 15000 }); await createButton.click(); await page.waitForTimeout(1000); console.log('✅ 点击新建课程包'); // 验证进入创建页面 await expect(page.getByText('创建课程包')).toBeVisible({ timeout: 5000 }); await expect(page.getByText('基本信息')).toBeVisible(); console.log('✅ 进入课程创建页面'); // ==================== 步骤 1: 基本信息 ==================== console.log('\n========== 步骤 1: 基本信息 =========='); // 填写课程包名称 const nameInput = page.getByPlaceholder('请输入课程包名称'); await expect(nameInput).toBeVisible({ timeout: 3000 }); await nameInput.fill(TEST_COURSE.name); console.log(` - 填写课程名称:${TEST_COURSE.name}`); // 选择关联主题 await page.locator('.ant-select-selector').first().click(); await page.waitForTimeout(500); await page.getByText(TEST_COURSE.theme, { exact: true }).first().click(); await page.waitForTimeout(300); console.log(` - 选择主题:${TEST_COURSE.theme}`); // 选择适用年级 await page.getByRole('checkbox', { name: TEST_COURSE.grades[0] }).click(); await page.waitForTimeout(300); console.log(` - 选择年级:${TEST_COURSE.grades[0]}`); // 填写核心内容 const coreContentInput = page.getByPlaceholder('请输入课程包核心内容'); await expect(coreContentInput).toBeVisible({ timeout: 3000 }); await coreContentInput.fill(TEST_COURSE.coreContent); console.log(` - 填写核心内容:${TEST_COURSE.coreContent}`); // 填写绘本名称 const bookNameInput = page.getByPlaceholder('请输入关联绘本名称(可选)'); await expect(bookNameInput).toBeVisible({ timeout: 3000 }); await bookNameInput.fill(TEST_COURSE.pictureBookName); console.log(` - 填写绘本名称:${TEST_COURSE.pictureBookName}`); // 选择领域标签 const domainSelector = page.locator('.ant-select-selector').last(); await domainSelector.click(); await page.waitForTimeout(800); await page.getByText(TEST_COURSE.domainTags[0], { exact: true }).first().click({ force: true }); await page.waitForTimeout(300); await page.keyboard.press('Escape'); console.log(` - 选择领域标签:${TEST_COURSE.domainTags[0]}`); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 1 完成 - 基本信息'); // ==================== 步骤 2: 课程介绍 ==================== console.log('\n========== 步骤 2: 课程介绍 =========='); await expect(page.locator('.intro-header .title:has-text("课程介绍")')).toBeVisible({ timeout: 3000 }); // 填写课程简介 - 使用更精确的定位 await page.getByRole('tab', { name: '课程简介' }).click(); await page.waitForTimeout(1000); // 使用 visible 选择器找到可见的 textarea const summaryTextarea = page.locator('.tab-content textarea:visible').first(); await summaryTextarea.waitFor({ state: 'visible', timeout: 10000 }); await summaryTextarea.fill(TEST_COURSE.introSummary); console.log(' - 填写课程简介'); // 填写课程亮点 await page.getByRole('tab', { name: '课程亮点' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introHighlights); console.log(' - 填写课程亮点'); // 填写课程总目标 await page.getByRole('tab', { name: '课程总目标' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introGoals); console.log(' - 填写课程目标'); // 填写内容安排 await page.getByRole('tab', { name: '内容安排' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introSchedule); console.log(' - 填写内容安排'); // 填写重难点 await page.getByRole('tab', { name: '重难点' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introKeyPoints); console.log(' - 填写重难点'); // 填写教学方法 await page.getByRole('tab', { name: '教学方法' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introMethods); console.log(' - 填写教学方法'); // 填写评价方式 await page.getByRole('tab', { name: '评价方式' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introEvaluation); console.log(' - 填写评价方式'); // 填写注意事项 await page.getByRole('tab', { name: '注意事项' }).click(); await page.waitForTimeout(500); await page.locator('.tab-content textarea:visible').first().fill(TEST_COURSE.introNotes); console.log(' - 填写注意事项'); // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 2 完成 - 课程介绍'); // ==================== 步骤 3: 排课参考 ==================== console.log('\n========== 步骤 3: 排课参考 =========='); await expect(page.locator('.step3-schedule-ref .title:has-text("排课计划参考")')).toBeVisible({ timeout: 3000 }); // 使用快速填充模板(可选步骤,跳过详细填写) const fillWeekButton = page.getByText('填充一周模板'); if (await fillWeekButton.isVisible({ timeout: 5000 })) { await fillWeekButton.click(); await page.waitForTimeout(500); console.log(' - 使用快速填充模板'); } // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 3 完成 - 排课参考'); // ==================== 步骤 4: 导入课 ==================== console.log('\n========== 步骤 4: 导入课 =========='); await expect(page.locator('.step4-intro-lesson .title:has-text("导入课")')).toBeVisible({ timeout: 3000 }); // 点击创建导入课按钮(如果需要) const createIntroButton = page.getByText('创建导入课'); if (await createIntroButton.isVisible({ timeout: 3000 })) { await createIntroButton.click(); await page.waitForTimeout(1000); console.log(' - 点击创建导入课'); } // 填写导入课名称 const introLessonNameInput = page.locator('input[placeholder*="课程名称"]').first(); if (await introLessonNameInput.isVisible({ timeout: 3000 })) { await introLessonNameInput.fill(TEST_COURSE.introLessonName); console.log(` - 填写导入课名称:${TEST_COURSE.introLessonName}`); } // 填写课时时长 const introLessonDurationInput = page.locator('input[placeholder*="时长"]').first(); if (await introLessonDurationInput.isVisible({ timeout: 3000 })) { await introLessonDurationInput.fill(String(TEST_COURSE.introLessonDuration)); console.log(` - 填写导入课时长:${TEST_COURSE.introLessonDuration}分钟`); } // 填写教学目标 const introLessonObjectivesTextarea = page.locator('textarea[placeholder*="教学目标"]').first(); if (await introLessonObjectivesTextarea.isVisible({ timeout: 3000 })) { await introLessonObjectivesTextarea.fill(TEST_COURSE.introLessonObjectives); console.log(' - 填写导入课教学目标'); } // 填写教学准备 const introLessonPreparationTextarea = page.locator('textarea[placeholder*="教学准备"]').first(); if (await introLessonPreparationTextarea.isVisible({ timeout: 3000 })) { await introLessonPreparationTextarea.fill(TEST_COURSE.introLessonPreparation); console.log(' - 填写导入课教学准备'); } // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 4 完成 - 导入课'); // ==================== 步骤 5: 集体课 ==================== console.log('\n========== 步骤 5: 集体课 =========='); await expect(page.locator('.step5-collective-lesson .title:has-text("集体课")')).toBeVisible({ timeout: 3000 }); // 点击创建集体课按钮(如果需要) const createCollectiveButton = page.getByText('创建集体课'); if (await createCollectiveButton.isVisible({ timeout: 3000 })) { await createCollectiveButton.click(); await page.waitForTimeout(1000); console.log(' - 点击创建集体课'); } // 填写集体课名称 const collectiveLessonNameInput = page.locator('input[placeholder*="课程名称"]').first(); if (await collectiveLessonNameInput.isVisible({ timeout: 3000 })) { await collectiveLessonNameInput.fill(TEST_COURSE.collectiveLessonName); console.log(` - 填写集体课名称:${TEST_COURSE.collectiveLessonName}`); } // 填写课时时长 const collectiveLessonDurationInput = page.locator('input[placeholder*="时长"]').first(); if (await collectiveLessonDurationInput.isVisible({ timeout: 3000 })) { await collectiveLessonDurationInput.fill(String(TEST_COURSE.collectiveLessonDuration)); console.log(` - 填写集体课时长:${TEST_COURSE.collectiveLessonDuration}分钟`); } // 填写教学目标 const collectiveLessonObjectivesTextarea = page.locator('textarea[placeholder*="教学目标"]').first(); if (await collectiveLessonObjectivesTextarea.isVisible({ timeout: 3000 })) { await collectiveLessonObjectivesTextarea.fill(TEST_COURSE.collectiveLessonObjectives); console.log(' - 填写集体课教学目标'); } // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 5 完成 - 集体课'); // ==================== 步骤 6: 领域课 ==================== console.log('\n========== 步骤 6: 领域课 =========='); await expect(page.locator('.step6-domain-lessons .title:has-text("五大领域课")')).toBeVisible({ timeout: 3000 }); // 填写健康领域课程 console.log(' - 填写健康领域课程'); // 展开健康领域卡片 const healthDomainCard = page.locator('.domain-card').filter({ hasText: /健康/ }).first(); const healthSwitch = healthDomainCard.locator('.ant-switch').first(); if (await healthSwitch.isVisible({ timeout: 3000 })) { await healthSwitch.click(); await page.waitForTimeout(500); } // 点击展开按钮 const healthExpandButton = healthDomainCard.locator('button:has-text("展开")').first(); if (await healthExpandButton.isVisible({ timeout: 3000 })) { await healthExpandButton.click(); await page.waitForTimeout(1000); } // 填写健康领域课程名称 const healthLessonNameInput = healthDomainCard.locator('input[placeholder*="课程名称"]').first(); if (await healthLessonNameInput.isVisible({ timeout: 3000 })) { await healthLessonNameInput.fill(TEST_COURSE.healthLessonName); console.log(` 课程名称:${TEST_COURSE.healthLessonName}`); } // 填写课时时长 const healthLessonDurationInput = healthDomainCard.locator('input[placeholder*="时长"]').first(); if (await healthLessonDurationInput.isVisible({ timeout: 3000 })) { await healthLessonDurationInput.fill(String(TEST_COURSE.healthLessonDuration)); console.log(` 课时时长:${TEST_COURSE.healthLessonDuration}分钟`); } // 填写教学目标 const healthLessonObjectivesTextarea = healthDomainCard.locator('textarea[placeholder*="教学目标"]').first(); if (await healthLessonObjectivesTextarea.isVisible({ timeout: 3000 })) { await healthLessonObjectivesTextarea.fill(TEST_COURSE.healthLessonObjectives); console.log(' 教学目标:已填写'); } // 填写科学领域课程 console.log(' - 填写科学领域课程'); // 展开科学领域卡片 const scienceDomainCard = page.locator('.domain-card').filter({ hasText: /科学/ }).first(); const scienceSwitch = scienceDomainCard.locator('.ant-switch').first(); if (await scienceSwitch.isVisible({ timeout: 3000 })) { await scienceSwitch.click(); await page.waitForTimeout(500); } // 点击展开按钮 const scienceExpandButton = scienceDomainCard.locator('button:has-text("展开")').first(); if (await scienceExpandButton.isVisible({ timeout: 3000 })) { await scienceExpandButton.click(); await page.waitForTimeout(1000); } // 填写科学领域课程名称 const scienceLessonNameInput = scienceDomainCard.locator('input[placeholder*="课程名称"]').first(); if (await scienceLessonNameInput.isVisible({ timeout: 3000 })) { await scienceLessonNameInput.fill(TEST_COURSE.scienceLessonName); console.log(` 课程名称:${TEST_COURSE.scienceLessonName}`); } // 填写课时时长 const scienceLessonDurationInput = scienceDomainCard.locator('input[placeholder*="时长"]').first(); if (await scienceLessonDurationInput.isVisible({ timeout: 3000 })) { await scienceLessonDurationInput.fill(String(TEST_COURSE.scienceLessonDuration)); console.log(` 课时时长:${TEST_COURSE.scienceLessonDuration}分钟`); } // 填写教学目标 const scienceLessonObjectivesTextarea = scienceDomainCard.locator('textarea[placeholder*="教学目标"]').first(); if (await scienceLessonObjectivesTextarea.isVisible({ timeout: 3000 })) { await scienceLessonObjectivesTextarea.fill(TEST_COURSE.scienceLessonObjectives); console.log(' 教学目标:已填写'); } // 点击下一步 await page.getByRole('button', { name: '下一步' }).click(); await page.waitForTimeout(1000); console.log('✅ 步骤 6 完成 - 领域课'); // ==================== 步骤 7: 环创建设 ==================== console.log('\n========== 步骤 7: 环创建设 =========='); await expect(page.locator('.step7-environment .title:has-text("环创建设")')).toBeVisible({ timeout: 3000 }); // 填写环创建设内容 const envTextarea = page.locator('.step7-environment textarea').first(); await expect(envTextarea).toBeVisible({ timeout: 3000 }); await envTextarea.fill(TEST_COURSE.environmentConstruction); console.log(' - 填写环创建设内容'); // 点击提交/创建按钮 - 使用 CSS 选择器定位 const submitButton = page.locator('.step-actions button.ant-btn-primary').last(); await submitButton.waitFor({ state: 'visible', timeout: 10000 }); await submitButton.click(); console.log(' - 点击创建按钮'); console.log('✅ 步骤 7 完成 - 环创建设'); // 等待一段时间让保存操作完成 await page.waitForTimeout(5000); // ==================== 验证创建成功 ==================== console.log('\n========== 验证创建结果 =========='); // 等待成功提示 try { await page.waitForSelector('.ant-message-success', { timeout: 10000 }); console.log('✅ 成功提示显示'); } catch (e) { console.log('⚠️ 未找到成功提示,检查是否有错误提示'); const errorMsg = await page.locator('.ant-message-error').textContent().catch(() => ''); if (errorMsg) { console.error('❌ 错误信息:', errorMsg); } } // 给页面一些时间执行跳转逻辑 await page.waitForTimeout(2000); // 等待跳转到列表页 - 使用精确匹配(排除 edit 页面) console.log('⏳ 等待页面跳转到课程列表...'); await page.waitForURL((url) => { const urlStr = url.toString(); if (urlStr.includes('/edit')) { console.log(' - 仍在编辑页面,URL:', urlStr); return false; } if (urlStr.includes('/admin/courses')) { console.log(' - 已跳转到课程列表页,URL:', urlStr); return true; } return false; }, { timeout: 15000 }); console.log('✅ 页面跳转到课程列表'); // 等待表格加载完成 await page.waitForTimeout(3000); // 检查当前 URL const currentUrl = page.url(); console.log('📍 当前 URL:', currentUrl); // 使用更宽松的等待策略 const table = page.locator('.ant-table').first(); await table.waitFor({ state: 'attached', timeout: 10000 }); await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); await page.waitForTimeout(2000); // 检查表格是否为空 const isEmpty = await table.locator('.ant-empty').isVisible().catch(() => false); console.log('📊 表格是否为空:', isEmpty); // 获取表格行数 const rowCount = await table.locator('.ant-table-tbody tr').count(); console.log('📊 表格行数:', rowCount); // 获取所有课程包名称 const courseNames = await page.locator('.ant-table-tbody tr td:first-child').allTextContents(); console.log('📊 当前课程包列表:', courseNames); // 查找新课程包 const courseRow = table.getByText(TEST_COURSE.name); try { await expect(courseRow).toBeVisible({ timeout: 10000 }); console.log(`✅ 新课程包 "${TEST_COURSE.name}" 在列表中显示`); } catch { // 如果找不到,刷新页面后再试一次 console.log(' - 第一次尝试未找到,刷新页面...'); await page.reload({ waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 再次检查课程包列表 const courseNamesAfterRefresh = await page.locator('.ant-table-tbody tr td:first-child').allTextContents(); console.log('📊 刷新后课程包列表:', courseNamesAfterRefresh); await expect(table.getByText(TEST_COURSE.name)).toBeVisible({ timeout: 10000 }); console.log(`✅ 新课程包 "${TEST_COURSE.name}" 在列表中显示(刷新后)`); } console.log('\n🎉 课程包创建测试全部通过!'); }); });