294 lines
15 KiB
TypeScript
294 lines
15 KiB
TypeScript
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();
|
|
});
|
|
});
|