import { test, expect } from '@playwright/test'; import { TEST_URLS } from '../fixtures/test-data'; import { faker, fakerTH } from '@faker-js/faker'; /** * Instructor Course Detail Tabs Tests * ทดสอบการเข้าดูแต่ละ tab ในหน้ารายละเอียดหลักสูตร * ใช้ cookies จาก instructor-setup project (ไม่ต้อง login ซ้ำ) */ test.describe.serial('Course Detail Tabs', () => { let courseUrl: string; // ── Step 1: ค้นหาและเข้าหลักสูตร "พื้นฐาน Python สำหรับผู้เริ่มต้น" ── test('navigate to Python course detail page', async ({ page }) => { await page.goto(TEST_URLS.instructorCourses); await page.waitForLoadState('networkidle'); // ค้นหาหลักสูตร (debounce 600ms) await page.getByPlaceholder('ค้นหาหลักสูตร...').fill('พื้นฐาน Python'); await page.waitForTimeout(1000); // หา course card ที่มีชื่อตรง แล้วกดปุ่ม visibility (ดูรายละเอียด) const courseCard = page.locator('.bg-white.rounded-xl').filter({ hasText: 'พื้นฐาน Python สำหรับผู้เริ่มต้น' }).first(); await expect(courseCard).toBeVisible({ timeout: 10_000 }); await courseCard.locator('button').filter({ has: page.locator('.q-icon:has-text("visibility")') }).click(); // รอเข้าหน้ารายละเอียด await page.waitForURL('**/instructor/courses/*', { timeout: 10_000 }); await page.waitForLoadState('networkidle'); // เก็บ URL courseUrl = page.url(); // ตรวจสอบ header elements await expect(page.locator('h1')).toBeVisible(); // ตรวจสอบว่ามี tabs ครบ await expect(page.getByRole('tab', { name: 'โครงสร้าง' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'ผู้เรียน' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'ผู้สอน' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'ผลการทดสอบ' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'ประวัติการขออนุมัติ' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'ประกาศ' })).toBeVisible(); }); // ── Step 2: tab โครงสร้าง (default) ── test('display structure tab and preview a lesson', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // tab โครงสร้างเป็น default → ควรแสดงอยู่แล้ว const structureTab = page.getByRole('tab', { name: 'โครงสร้าง' }); await expect(structureTab).toBeVisible(); // ตรวจสอบว่ามี chapters หรือ empty state await expect(page.getByText('โครงสร้างบทเรียน')).toBeVisible(); const hasEmptyState = await page.getByText('ยังไม่มีบทเรียน').isVisible().catch(() => false); const hasChapters = !hasEmptyState && (await page.locator('.q-list.border-t').first().isVisible().catch(() => false)); expect(hasChapters || hasEmptyState).toBeTruthy(); // ถ้ามี chapters → กดเข้าดู lesson แรก if (hasChapters) { // หา lesson แรกใน structure tab แล้วกด const firstLesson = page.locator('.q-item').filter({ hasText: /Lesson \d/ }).first(); await expect(firstLesson).toBeVisible(); await firstLesson.click(); // ตรวจสอบว่า LessonPreviewDialog เปิด const dialog = page.locator('.q-dialog'); await expect(dialog).toBeVisible({ timeout: 5_000 }); // ตรวจสอบว่าแสดง title ของ lesson await expect(dialog.locator('.text-h6')).toBeVisible(); // ปิด dialog await dialog.locator('button').filter({ has: page.locator('.q-icon:has-text("close")') }).click(); await expect(dialog).toBeHidden({ timeout: 3_000 }); } }); // ── Step 3: tab ผู้เรียน ── test('display students tab and view student detail', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กด tab ผู้เรียน await page.getByRole('tab', { name: 'ผู้เรียน' }).click(); await page.waitForTimeout(500); // ตรวจสอบ: stat cards หรือ empty state const hasStats = await page.getByText('ผู้เรียนทั้งหมด').isVisible().catch(() => false); const hasEmptyState = await page.getByText('ยังไม่มีผู้เรียนในหลักสูตรนี้').isVisible().catch(() => false); expect(hasStats || hasEmptyState).toBeTruthy(); // ถ้ามีผู้เรียน → กดเข้าดูรายละเอียดคนแรก if (hasStats) { await expect(page.getByPlaceholder('ค้นหาผู้เรียน...')).toBeVisible(); // กดที่นักเรียนคนแรกในรายการ const firstStudent = page.locator('.q-item.cursor-pointer').first(); await expect(firstStudent).toBeVisible(); await firstStudent.click(); // ตรวจสอบว่า detail modal เปิด (maximized) const dialog = page.locator('.q-dialog'); await expect(dialog).toBeVisible({ timeout: 10_000 }); // ตรวจสอบ progress info await expect(dialog.getByText('ความคืบหน้าทั้งหมด')).toBeVisible({ timeout: 10_000 }); // ปิด modal await dialog.locator('button').filter({ has: page.locator('.q-icon:has-text("close")') }).click(); await expect(dialog).toBeHidden({ timeout: 3_000 }); } }); // ── Step 4: tab ผู้สอน ── test('display instructors tab and search to add instructor', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กด tab ผู้สอน await page.getByRole('tab', { name: 'ผู้สอน' }).click(); await page.waitForTimeout(500); // ตรวจสอบ header await expect(page.getByText('ผู้สอนในรายวิชา')).toBeVisible(); // ตรวจสอบ: มีการ์ดผู้สอนอย่างน้อย 1 คน หรือ empty state const hasInstructors = await page.getByText('หัวหน้าผู้สอน').isVisible().catch(() => false); const hasEmptyState = await page.getByText('ยังไม่มีข้อมูลผู้สอน').isVisible().catch(() => false); expect(hasInstructors || hasEmptyState).toBeTruthy(); // ถ้ามีปุ่มเพิ่มผู้สอน (แสดงเฉพาะ primary instructor) const addBtn = page.getByRole('button', { name: 'เพิ่มผู้สอน' }); const hasAddBtn = await addBtn.isVisible().catch(() => false); if (hasAddBtn) { // กดปุ่มเพิ่มผู้สอน await addBtn.click(); // ตรวจสอบว่า dialog เปิด const dialog = page.locator('.q-dialog'); await expect(dialog).toBeVisible({ timeout: 5_000 }); await expect(dialog.getByText('เพิ่มผู้สอน')).toBeVisible(); // ค้นหา "lertp" ใน q-select (use-input) const selectInput = dialog.locator('.q-select input[type="search"]'); await selectInput.fill('lertp'); await page.waitForTimeout(1500); // ตรวจสอบว่ามีผลลัพธ์ (dropdown options) หรือ "ไม่พบผู้ใช้" const hasResults = await page.locator('.q-menu .q-item').first().isVisible().catch(() => false); expect(hasResults).toBeTruthy(); } }); // ── Step 5: tab ผลการทดสอบ ── test('display quiz results and view student detail', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กด tab ผลการทดสอบ await page.getByRole('tab', { name: 'ผลการทดสอบ' }).click(); await page.waitForTimeout(1000); // ตรวจสอบ: มี quiz selector หรือ empty state const hasQuizSelector = await page.getByText('เลือกแบบทดสอบ').isVisible().catch(() => false); const hasEmptyState = await page.getByText('หลักสูตรนี้ยังไม่มีแบบทดสอบ').isVisible().catch(() => false); expect(hasQuizSelector || hasEmptyState).toBeTruthy(); // ถ้ามีแบบทดสอบ → ตรวจสอบตาราง + กดดูรายละเอียดนักเรียน if (hasQuizSelector) { // รอตารางโหลด (auto-select quiz แรก) await page.waitForTimeout(1500); // ตรวจสอบว่ามี stats cards (คะแนนเฉลี่ย) const hasStats = await page.getByText('คะแนนเฉลี่ย').isVisible().catch(() => false); // ตรวจสอบว่ามี row ในตาราง const firstRow = page.locator('.q-tr.cursor-pointer').first(); const hasStudents = await firstRow.isVisible().catch(() => false); if (hasStudents) { // กดนักเรียนคนแรก await firstRow.click(); // ตรวจสอบ detail dialog const dialog = page.locator('.q-dialog'); await expect(dialog).toBeVisible({ timeout: 10_000 }); // ตรวจสอบข้อมูลคะแนน await expect(dialog.getByText('คะแนนที่ได้')).toBeVisible({ timeout: 10_000 }); // ปิด dialog await dialog.locator('button').filter({ has: page.locator('.q-icon:has-text("close")') }).click(); await expect(dialog).toBeHidden({ timeout: 3_000 }); } } }); // ── Step 6: tab ประวัติการขออนุมัติ ── test('should display approval history tab content', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กด tab ประวัติการขออนุมัติ await page.getByRole('tab', { name: 'ประวัติการขออนุมัติ' }).click(); await page.waitForTimeout(500); // ตรวจสอบ: มี timeline หรือ empty state const hasTimeline = await page.locator('.q-timeline').isVisible().catch(() => false); const hasEmptyState = await page.getByText('ไม่พบประวัติการขออนุมัติ').isVisible().catch(() => false); expect(hasTimeline || hasEmptyState).toBeTruthy(); }); // ── Step 7: tab ประกาศ ── test('display announcements tab and create a new announcement', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); // กด tab ประกาศ await page.getByRole('tab', { name: 'ประกาศ' }).click(); await page.waitForTimeout(500); // ตรวจสอบ header await expect(page.getByText('ประกาศ').first()).toBeVisible(); // สร้างข้อมูลประกาศ const announcementTitle = fakerTH.lorem.sentence({ min: 3, max: 6 }); const announcementTitleEn = faker.lorem.sentence({ min: 3, max: 6 }); const announcementContent = fakerTH.lorem.paragraphs(1); const announcementContentEn = faker.lorem.paragraphs(1); // กดปุ่มสร้างประกาศ const createBtn = page.getByRole('button', { name: 'สร้างประกาศ' }); await expect(createBtn).toBeVisible(); await createBtn.click(); // ตรวจสอบว่า dialog เปิด const dialog = page.locator('.q-dialog'); await expect(dialog).toBeVisible({ timeout: 5_000 }); await expect(dialog.getByText('สร้างประกาศใหม่')).toBeVisible(); // กรอกหัวข้อ (ภาษาไทย) await dialog.getByLabel('หัวข้อ (ภาษาไทย) *').fill(announcementTitle); // กรอกหัวข้อ (English) await dialog.getByLabel('หัวข้อ (English)').fill(announcementTitleEn); // กรอกเนื้อหา (ภาษาไทย) await dialog.getByLabel('เนื้อหา (ภาษาไทย) *').fill(announcementContent); // กรอกเนื้อหา (English) await dialog.getByLabel('เนื้อหา (English)').fill(announcementContentEn); // กดสร้าง await dialog.getByRole('button', { name: 'สร้าง' }).click(); // รอ dialog ปิด + ตรวจสอบ success await expect(dialog).toBeHidden({ timeout: 10_000 }); // ตรวจสอบว่าประกาศที่สร้างแสดงในรายการ await page.waitForTimeout(500); await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 5_000 }); }); // ── Step 8: ทดสอบสลับ tabs อย่างรวดเร็ว ── test('should switch between all tabs without errors', async ({ page }) => { await page.goto(courseUrl); await page.waitForLoadState('networkidle'); const tabs = ['โครงสร้าง', 'ผู้เรียน', 'ผู้สอน', 'ผลการทดสอบ', 'ประวัติการขออนุมัติ', 'ประกาศ']; for (const tabName of tabs) { await page.getByRole('tab', { name: tabName }).click(); await page.waitForTimeout(300); // ตรวจสอบว่า tab active (ไม่ crash) const tabPanel = page.locator('.q-tab-panel:visible'); await expect(tabPanel).toBeVisible({ timeout: 5_000 }); } // สลับกลับไปที่ tab แรก await page.getByRole('tab', { name: 'โครงสร้าง' }).click(); await page.waitForTimeout(300); await expect(page.locator('.q-tab-panel:visible')).toBeVisible(); }); });