import { test, expect } from '@playwright/test'; import { TEST_URLS } from '../fixtures/test-data'; import { faker } from '@faker-js/faker'; /** * Instructor Create Course & Structure Tests * ใช้ cookies จาก instructor-setup project (ไม่ต้อง login ซ้ำ) * ใช้ faker สุ่มข้อมูลหลักสูตรจริงๆ */ // ── Dynamic Course Data Generator (ไม่ hardcode) ──────────────── const TOPICS = ['JavaScript', 'Python', 'React', 'Vue.js', 'Node.js', 'TypeScript', 'Docker', 'Kubernetes', 'GraphQL', 'Next.js', 'Flutter', 'Swift', 'Rust', 'Go', 'Machine Learning']; const LEVELS = ['เบื้องต้น', 'พื้นฐาน', 'ขั้นสูง', 'สำหรับมือใหม่', 'เชิงปฏิบัติ']; const LEVELS_EN = ['Fundamentals', 'Basics', 'Advanced', 'for Beginners', 'Hands-on']; const ACTIONS_TH = ['เรียนรู้', 'ทำความเข้าใจ', 'ฝึกปฏิบัติ', 'ประยุกต์ใช้', 'สำรวจ']; const ACTIONS_EN = ['Learn', 'Understand', 'Practice', 'Apply', 'Explore']; const CHAPTER_THEMES_TH = ['แนะนำ', 'พื้นฐาน', 'การใช้งาน', 'เทคนิค', 'โปรเจกต์']; const CHAPTER_THEMES_EN = ['Introduction to', 'Basics of', 'Working with', 'Techniques in', 'Projects with']; const LESSON_TYPES: ('วิดีโอ' | 'แบบทดสอบ')[] = ['วิดีโอ', 'วิดีโอ', 'วิดีโอ', 'แบบทดสอบ']; // 75% video, 25% quiz const timestamp = Date.now(); const topic = faker.helpers.arrayElement(TOPICS); const levelIndex = faker.number.int({ min: 0, max: LEVELS.length - 1 }); /** สร้างข้อมูล lesson สุ่ม */ function generateLesson(topic: string, chapterIndex: number, lessonIndex: number) { const subtopic = faker.helpers.arrayElement([ `${faker.helpers.arrayElement(ACTIONS_TH)} ${topic}`, `${topic} ${faker.helpers.arrayElement(['ตอนที่', 'ส่วนที่', 'หัวข้อที่'])} ${chapterIndex + 1}.${lessonIndex + 1}`, `${faker.helpers.arrayElement(['การตั้งค่า', 'การใช้งาน', 'แนวคิด', 'ตัวอย่าง', 'การทดลอง'])} ${topic} บทที่ ${chapterIndex + 1}`, ]); const subtopicEn = faker.helpers.arrayElement([ `${faker.helpers.arrayElement(ACTIONS_EN)} ${topic}`, `${topic} Part ${chapterIndex + 1}.${lessonIndex + 1}`, `${faker.helpers.arrayElement(['Setting up', 'Using', 'Concepts of', 'Examples of', 'Experimenting with'])} ${topic} Ch.${chapterIndex + 1}`, ]); const type = faker.helpers.arrayElement(LESSON_TYPES); return { title: { th: type === 'แบบทดสอบ' ? `แบบทดสอบ ${chapterIndex + 1}.${lessonIndex + 1}: ${subtopic}` : subtopic, en: type === 'แบบทดสอบ' ? `Quiz ${chapterIndex + 1}.${lessonIndex + 1}: ${subtopicEn}` : subtopicEn, }, type, content: { th: `ในบทเรียนนี้คุณจะได้${faker.helpers.arrayElement(ACTIONS_TH)} ${topic} ${faker.helpers.arrayElement(['แบบเจาะลึก', 'อย่างละเอียด', 'ผ่านตัวอย่างจริง', 'พร้อมแบบฝึกหัด'])}`, en: `In this lesson you will ${faker.helpers.arrayElement(ACTIONS_EN).toLowerCase()} ${topic} ${faker.helpers.arrayElement(['in depth', 'in detail', 'through real examples', 'with exercises'])}`, }, }; } /** สร้างข้อมูล chapter สุ่ม */ function generateChapter(topic: string, chapterIndex: number, themeIndex: number) { const lessonCount = faker.number.int({ min: 2, max: 3 }); const lessons = Array.from({ length: lessonCount }, (_, i) => generateLesson(topic, chapterIndex, i)); // ทำให้ lesson สุดท้ายเป็นแบบทดสอบเสมอ lessons[lessons.length - 1] = { ...lessons[lessons.length - 1], type: 'แบบทดสอบ', title: { th: `แบบทดสอบ: ${CHAPTER_THEMES_TH[themeIndex]} ${topic}`, en: `Quiz: ${CHAPTER_THEMES_EN[themeIndex]} ${topic}`, }, }; return { title: { th: `${CHAPTER_THEMES_TH[themeIndex]} ${topic}`, en: `${CHAPTER_THEMES_EN[themeIndex]} ${topic}`, }, description: { th: `${faker.helpers.arrayElement(ACTIONS_TH)} ${CHAPTER_THEMES_TH[themeIndex].toLowerCase()} ${topic} ${faker.helpers.arrayElement(['อย่างเป็นระบบ', 'อย่างมีประสิทธิภาพ', 'แบบ step-by-step', 'ผ่านตัวอย่างจริง'])}`, en: `${faker.helpers.arrayElement(ACTIONS_EN)} ${CHAPTER_THEMES_EN[themeIndex].toLowerCase()} ${topic} ${faker.helpers.arrayElement(['systematically', 'effectively', 'step by step', 'through real examples'])}`, }, lessons, }; } // ── สร้างข้อมูล course ────────────────────────────────────────── const COURSE_DATA = { title: { th: `${topic} ${LEVELS[levelIndex]} ${timestamp}`, en: `${topic} ${LEVELS_EN[levelIndex]} ${timestamp}`, }, slug: `${topic.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${timestamp}`, description: { th: `เรียนรู้ ${topic} ${LEVELS[levelIndex]} ครอบคลุมเนื้อหาสำคัญทั้งหมดที่จำเป็น พร้อมตัวอย่างและแบบฝึกหัดที่จะช่วยให้คุณเข้าใจอย่างลึกซึ้ง`, en: `Learn ${topic} ${LEVELS_EN[levelIndex]} covering all essential topics with examples and exercises to help you gain deep understanding.`, }, }; const chapterCount = faker.number.int({ min: 2, max: 3 }); // สุ่ม theme ที่ไม่ซ้ำกันสำหรับแต่ละบท const shuffledThemeIndexes = faker.helpers.shuffle([0, 1, 2, 3, 4]).slice(0, chapterCount); const CHAPTERS = shuffledThemeIndexes.map((themeIdx, i) => generateChapter(topic, i, themeIdx)); // ── Tests (serial — ต้องทำงานต่อเนื่องกัน) ───────────────────── test.describe.serial('Create Course & Structure', () => { let courseUrl: string; // ── Step 1: สร้างหลักสูตรใหม่ ── test('create a new course with faker data', async ({ page }) => { await page.goto(TEST_URLS.instructorCreateCourse); await page.waitForLoadState('networkidle'); // กรอกชื่อหลักสูตร await page.locator('input').filter({ hasText: '' }).nth(0) .or(page.getByLabel('ชื่อหลักสูตร (ภาษาไทย) *')) .fill(COURSE_DATA.title.th); await page.getByLabel('ชื่อหลักสูตร (English) *').fill(COURSE_DATA.title.en); // กรอก Slug await page.getByLabel('Slug (URL) *').clear(); await page.getByLabel('Slug (URL) *').fill(COURSE_DATA.slug); // เลือกหมวดหมู่ (เลือกตัวแรกจาก dropdown) await page.locator('.q-select').click(); await page.waitForTimeout(300); await page.getByRole('listbox').locator('.q-item').first().click(); // กรอกคำอธิบาย await page.getByLabel('คำอธิบาย (ภาษาไทย) *').fill(COURSE_DATA.description.th); await page.getByLabel('คำอธิบาย (English) *').fill(COURSE_DATA.description.en); // ตั้งค่า — หลักสูตรฟรี (toggle อยู่แล้วเป็น default) // ตรวจสอบว่า "หลักสูตรฟรี" toggle เปิดอยู่ const freeToggle = page.getByText('หลักสูตรฟรี'); await expect(freeToggle).toBeVisible(); // กดสร้าง await page.getByRole('button', { name: 'สร้างหลักสูตร' }).click(); // รอ redirect ไปหน้า course detail await page.waitForURL('**/instructor/courses/*', { timeout: 15_000 }); await expect(page).toHaveURL(/\/instructor\/courses\/\d+/); // เก็บ URL สำหรับ test ถัดไป courseUrl = page.url(); // ตรวจสอบว่าชื่อ course แสดงบนหน้า detail await page.waitForLoadState('networkidle'); await expect(page.getByText(COURSE_DATA.title.th)).toBeVisible({ timeout: 10_000 }); }); // ── Step 2: อัปโหลดรูปหลักสูตร ── test('upload course thumbnail', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // สร้างไฟล์รูปทดสอบ (1x1 PNG ขนาดเล็ก) const pngBuffer = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', 'base64' ); // กดที่ thumbnail area เพื่อเปิด file input const fileInput = page.locator('input[type="file"][accept="image/*"]'); await fileInput.setInputFiles({ name: `test-thumbnail-${Date.now()}.png`, mimeType: 'image/png', buffer: pngBuffer, }); // รอ upload เสร็จ await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); // ตรวจสอบว่ารูปแสดง (img tag ปรากฏ) await expect(page.locator('img[alt]').first()).toBeVisible({ timeout: 10_000 }); }); // ── Step 3: ไปหน้าจัดการโครงสร้าง ── test('navigate to structure page', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กดปุ่ม "จัดการโครงสร้าง" ใน Structure tab await page.getByRole('button', { name: 'จัดการโครงสร้าง' }).click(); await page.waitForURL('**/structure**', { timeout: 10_000 }); await expect(page).toHaveURL(/\/structure/); // ตรวจสอบ header await expect(page.getByText('จัดการโครงสร้างหลักสูตร')).toBeVisible(); // ตรวจสอบ empty state await expect(page.getByText('ยังไม่มีบทเรียน')).toBeVisible(); }); // ── Step 4: เพิ่มบทที่ 1 ── test('add chapter 1 with faker data', async ({ page }) => { // Navigate to structure page const structureUrl = courseUrl + '/structure'; await page.goto(structureUrl); await page.waitForLoadState('networkidle'); // กดเพิ่มบทแรก await page.getByRole('button', { name: 'เพิ่มบทแรก' }).click(); await page.waitForTimeout(300); // กรอกข้อมูล chapter const chapter = CHAPTERS[0]; await page.getByLabel('ชื่อบท (ภาษาไทย) *').fill(chapter.title.th); await page.getByLabel('ชื่อบท (English) *').fill(chapter.title.en); await page.getByLabel('คำอธิบาย (ภาษาไทย) *').fill(chapter.description.th); await page.getByLabel('คำอธิบาย (English) *').fill(chapter.description.en); // บันทึก await page.getByRole('button', { name: 'บันทึก' }).click(); await page.waitForLoadState('networkidle'); // ตรวจสอบ chapter แสดงผล await expect(page.getByText(chapter.title.th)).toBeVisible({ timeout: 10_000 }); }); // ── Step 5: เพิ่ม lessons ในบทที่ 1 ── test('add lessons to chapter 1', async ({ page }) => { const structureUrl = courseUrl + '/structure'; await page.goto(structureUrl); await page.waitForLoadState('networkidle'); const chapter = CHAPTERS[0]; for (const lesson of chapter.lessons) { // กดปุ่มเพิ่มบทเรียน (ปุ่ม + ที่อยู่ข้าง chapter header) const chapterCard = page.locator('.q-card').filter({ hasText: chapter.title.th }); await chapterCard.locator('button').filter({ has: page.locator('.q-icon:has-text("add")') }).click(); await page.waitForTimeout(300); // กรอกชื่อบทเรียน await page.getByLabel('ชื่อบทเรียน (ภาษาไทย) *').fill(lesson.title.th); await page.getByLabel('ชื่อบทเรียน (English) *').fill(lesson.title.en); // เลือกประเภท await page.locator('.q-dialog .q-select').click(); await page.waitForTimeout(200); await page.getByRole('listbox').getByText(lesson.type).click(); // กรอกเนื้อหา await page.getByLabel('เนื้อหา (ภาษาไทย) *').fill(lesson.content.th); await page.getByLabel('เนื้อหา (English) *').fill(lesson.content.en); // บันทึก await page.locator('.q-dialog').getByRole('button', { name: 'บันทึก' }).click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(500); // ตรวจสอบว่า lesson แสดง await expect(page.getByText(lesson.title.th)).toBeVisible({ timeout: 10_000 }); } // ตรวจสอบจำนวนบทเรียน await expect(page.getByText(`${chapter.lessons.length} บทเรียน`)).toBeVisible(); }); // ── Step 6: เพิ่มบทที่ 2 ── test('add chapter 2 with faker data', async ({ page }) => { const structureUrl = courseUrl + '/structure'; await page.goto(structureUrl); await page.waitForLoadState('networkidle'); // กดเพิ่มบท (ปุ่ม header) await page.getByRole('button', { name: 'เพิ่มบท' }).click(); await page.waitForTimeout(300); // กรอกข้อมูล chapter 2 const chapter = CHAPTERS[1]; await page.getByLabel('ชื่อบท (ภาษาไทย) *').fill(chapter.title.th); await page.getByLabel('ชื่อบท (English) *').fill(chapter.title.en); await page.getByLabel('คำอธิบาย (ภาษาไทย) *').fill(chapter.description.th); await page.getByLabel('คำอธิบาย (English) *').fill(chapter.description.en); // บันทึก await page.getByRole('button', { name: 'บันทึก' }).click(); await page.waitForLoadState('networkidle'); // ตรวจสอบ chapter 2 แสดงผล await expect(page.getByText(chapter.title.th)).toBeVisible({ timeout: 10_000 }); }); // ── Step 7: เพิ่ม lessons ในบทที่ 2 ── test('add lessons to chapter 2', async ({ page }) => { const structureUrl = courseUrl + '/structure'; await page.goto(structureUrl); await page.waitForLoadState('networkidle'); const chapter = CHAPTERS[1]; for (const lesson of chapter.lessons) { // กดปุ่มเพิ่มบทเรียนของบทที่ 2 (ปุ่มที่ 2) // หาบทที่ 2 ก่อน แล้วกดปุ่ม add ของ chapter นั้น const chapterCard = page.locator('.q-card').filter({ hasText: chapter.title.th }); await chapterCard.locator('button').filter({ has: page.locator('.q-icon:text("add")') }).click(); await page.waitForTimeout(300); // กรอกชื่อบทเรียน await page.getByLabel('ชื่อบทเรียน (ภาษาไทย) *').fill(lesson.title.th); await page.getByLabel('ชื่อบทเรียน (English) *').fill(lesson.title.en); // เลือกประเภท await page.locator('.q-dialog .q-select').click(); await page.waitForTimeout(200); await page.getByRole('listbox').getByText(lesson.type).click(); // กรอกเนื้อหา await page.getByLabel('เนื้อหา (ภาษาไทย) *').fill(lesson.content.th); await page.getByLabel('เนื้อหา (English) *').fill(lesson.content.en); // บันทึก await page.locator('.q-dialog').getByRole('button', { name: 'บันทึก' }).click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(500); // ตรวจสอบว่า lesson แสดง await expect(page.getByText(lesson.title.th)).toBeVisible({ timeout: 10_000 }); } }); // ── Step 8: เพิ่มข้อสอบในแบบทดสอบ ── test('add quiz questions to a quiz lesson', async ({ page }) => { const structureUrl = courseUrl + '/structure'; await page.goto(structureUrl); await page.waitForLoadState('networkidle'); // หา quiz lesson แรกจากบทแรก → กดปุ่ม edit (icon "edit") เพื่อเข้าหน้า quiz const firstQuizLesson = CHAPTERS[0].lessons.find(l => l.type === 'แบบทดสอบ'); if (!firstQuizLesson) throw new Error('No quiz lesson found in chapter 1'); // Locate the quiz lesson row and click its edit button const quizRow = page.locator('.q-item').filter({ hasText: firstQuizLesson.title.th }); await quizRow.locator('button').filter({ has: page.locator('.q-icon:has-text("edit")') }).click(); // รอเข้าหน้า quiz await page.waitForURL('**/quiz', { timeout: 10_000 }); await page.waitForLoadState('networkidle'); await expect(page.getByText('แก้ไขบทเรียน (แบบทดสอบ)')).toBeVisible(); // สุ่มจำนวนข้อสอบ 2-4 ข้อ const questionCount = faker.number.int({ min: 2, max: 4 }); for (let q = 0; q < questionCount; q++) { const questionTh = `${faker.helpers.arrayElement(ACTIONS_TH)} ${topic} คำถามที่ ${q + 1}: ${faker.helpers.arrayElement([ 'ข้อใดถูกต้อง?', 'ข้อใดเป็นจริง?', 'ข้อใดคือคำตอบที่ดีที่สุด?', 'ข้อใดต่อไปนี้ถูกต้อง?', ])}`; const questionEn = `${faker.helpers.arrayElement(ACTIONS_EN)} ${topic} Question ${q + 1}: ${faker.helpers.arrayElement([ 'Which is correct?', 'Which is true?', 'Which is the best answer?', 'Which of the following is correct?', ])}`; // กดปุ่ม "เพิ่มคำถาม" await page.getByRole('button', { name: 'เพิ่มคำถาม' }).click(); await page.waitForTimeout(300); // กรอกคำถาม const dialog = page.locator('.q-dialog'); await dialog.getByLabel('คำถาม (ภาษาไทย) *').fill(questionTh); await dialog.getByLabel('คำถาม (English)').fill(questionEn); // เพิ่มตัวเลือก (default มี 2 ตัว → เพิ่มอีก 2 ให้ครบ 4) await dialog.getByRole('button', { name: 'เพิ่มตัวเลือก' }).click(); await dialog.getByRole('button', { name: 'เพิ่มตัวเลือก' }).click(); // กรอก 4 ตัวเลือก const choices = [ { th: `${topic} คำตอบที่ถูกต้อง ${q + 1}`, en: `${topic} correct answer ${q + 1}` }, { th: `${topic} ตัวเลือกที่ 2 ${q + 1}`, en: `${topic} option 2 ${q + 1}` }, { th: `${topic} ตัวเลือกที่ 3 ${q + 1}`, en: `${topic} option 3 ${q + 1}` }, { th: `${topic} ตัวเลือกที่ 4 ${q + 1}`, en: `${topic} option 4 ${q + 1}` }, ]; for (let c = 0; c < 4; c++) { await dialog.getByLabel(`ตัวเลือก ${c + 1} (TH)`).fill(choices[c].th); await dialog.getByLabel(`ตัวเลือก ${c + 1} (EN)`).fill(choices[c].en); } // ตัวเลือกแรก (index 0) เป็นคำตอบที่ถูก (default เลือกไว้แล้ว) // บันทึก await dialog.getByRole('button', { name: 'บันทึก' }).click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(500); // ตรวจสอบว่าคำถามแสดงในรายการ await expect(page.getByText(`คำถามที่ ${q + 1}`).first()).toBeVisible({ timeout: 10_000 }); } // ตรวจสอบจำนวนข้อสอบทั้งหมด await expect(page.getByText(`คำถาม (${questionCount} ข้อ)`)).toBeVisible(); }); // ── Step 9: ตรวจสอบโครงสร้างทั้งหมดจากหน้า course detail ── test('verify full structure on course detail page', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // ตรวจสอบว่า tab โครงสร้างแสดง chapters ทั้งหมด for (const chapter of CHAPTERS) { // ตรวจสอบ chapter title (scope ไปที่ header element เพื่อกัน text ซ้ำ) await expect(page.locator('.font-semibold').getByText(chapter.title.th)).toBeVisible({ timeout: 10_000 }); // ตรวจสอบ lessons ใน chapter for (const lesson of chapter.lessons) { await expect(page.locator('.q-item__label').getByText(lesson.title.th)).toBeVisible(); } } // ตรวจสอบจำนวนบทเรียนรวม const totalLessons = CHAPTERS.reduce((sum, ch) => sum + ch.lessons.length, 0); await expect(page.getByText(`${totalLessons} บทเรียน`)).toBeVisible(); }); });