feat: Implement E2E tests for authentication, student account, discovery, and classroom features, alongside new browse pages and a useAuth composable.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 48s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 4s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 48s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 4s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
0205aab461
commit
b0b665f588
35 changed files with 546 additions and 862 deletions
|
|
@ -1,95 +1,175 @@
|
|||
/**
|
||||
* @file classroom.spec.ts
|
||||
* @description ทดสอบระบบห้องเรียนออนไลน์ และระบบแบบทดสอบ
|
||||
* (Classroom, Learning & Quiz System)
|
||||
*
|
||||
* รวม 2 module:
|
||||
* - Classroom & Learning (Layout, Access Control, Video/Quiz area)
|
||||
* - Quiz System (Start Screen, Pagination, Submit & Navigation)
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { BASE_URL, TIMEOUT, waitAppSettled, setupLogin } from './helpers';
|
||||
|
||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
// ==========================================
|
||||
// Mock: ข้อมูล Quiz สำหรับ test
|
||||
// ==========================================
|
||||
async function mockQuizData(page: any) {
|
||||
await page.route('**/lessons/*', async (route: any) => {
|
||||
const mockQuestions = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
question: { th: `คำถามข้อที่ ${i + 1}?`, en: `Question ${i + 1}?` },
|
||||
text: { th: `คำถามข้อที่ ${i + 1}?`, en: `Question ${i + 1}?` },
|
||||
choices: [
|
||||
{ id: i * 10 + 1, text: { th: 'ก', en: 'A' } },
|
||||
{ id: i * 10 + 2, text: { th: 'ข', en: 'B' } },
|
||||
{ id: i * 10 + 3, text: { th: 'ค', en: 'C' } }
|
||||
]
|
||||
}));
|
||||
|
||||
async function waitAppSettled(page: any) {
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
await page.waitForTimeout(200);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
data: {
|
||||
id: 17,
|
||||
type: 'QUIZ',
|
||||
quiz: {
|
||||
id: 99,
|
||||
title: { th: 'แบบทดสอบปลายภาค (Mock)', en: 'Final Exam (Mock)' },
|
||||
time_limit: 30,
|
||||
questions: mockQuestions
|
||||
}
|
||||
},
|
||||
progress: {}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ฟังก์ชันจำลองล็อกอิน
|
||||
async function setupLogin(page: any) {
|
||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
await waitAppSettled(page);
|
||||
|
||||
await page.locator('input[type="email"]').or(page.getByRole('textbox', { name: /อีเมล|email/i })).first().fill('studentedtest@example.com');
|
||||
await page.locator('input[type="password"]').or(page.getByRole('textbox', { name: /รหัสผ่าน|password/i })).first().fill('admin123');
|
||||
await page.getByRole('button', { name: /เข้าสู่ระบบ|login/i }).or(page.locator('button[type="submit"]')).first().click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 15_000 }).catch(() => {});
|
||||
await waitAppSettled(page);
|
||||
}
|
||||
|
||||
test.describe('ระบบห้องเรียนออนไลน์ (Classroom & Learning)', () => {
|
||||
// ==========================================
|
||||
// Tests
|
||||
// ==========================================
|
||||
test.describe('ระบบห้องเรียนออนไลน์และแบบทดสอบ (Classroom & Quiz)', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupLogin(page);
|
||||
});
|
||||
|
||||
test('6.1 เข้าห้องเรียนหลัก (Classroom Basic Layout)', async ({ page }) => {
|
||||
// สมมติว่ามี Course ID: 1 ทดสอบแบบเปิดหน้าตรงๆ
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||
|
||||
// 1. โครงร่างของหน้า (Top Bar) ควรมีปุ่มกลับ กับไอคอนแผงด้านข้าง
|
||||
const backBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'arrow_back' }) }).first();
|
||||
await expect(backBtn).toBeVisible({ timeout: 15_000 });
|
||||
// --------------------------------------------------
|
||||
// Section 1: ห้องเรียน (Classroom & Learning)
|
||||
// --------------------------------------------------
|
||||
test.describe('ห้องเรียน (Classroom Layout & Access)', () => {
|
||||
|
||||
const menuCurriculumBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'menu_open' }) }).first();
|
||||
await expect(menuCurriculumBtn).toBeVisible({ timeout: 15_000 });
|
||||
test('6.1 เข้าห้องเรียนหลัก (Classroom Basic Layout)', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||
|
||||
// 2. เช็คว่ามีพื้นที่ Sidebar หลักสูตร (CurriculumSidebar Component) โผล่ขึ้นมาหรือมีอยู่ใน DOM
|
||||
const sidebar = page.locator('.q-drawer').first();
|
||||
if (!await sidebar.isVisible()) {
|
||||
await menuCurriculumBtn.click();
|
||||
}
|
||||
await expect(sidebar).toBeVisible();
|
||||
});
|
||||
// 1. โครงร่างของหน้า — ปุ่มกลับ + ไอคอนแผงด้านข้าง
|
||||
const backBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'arrow_back' }) }).first();
|
||||
await expect(backBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
test('6.2 เช็คสถานะการเข้าถึงเนื้อหา (Access Control)', async ({ page }) => {
|
||||
// ลองสุ่ม Course ID สูงๆ ที่อาจจะไม่อนุญาตให้เรียน (ไม่มีสิทธิ์) ควรรองรับกล่องแจ้งเตือนด้วย Alert ของระบบ
|
||||
// ใน learning.vue จะมีการสั่ง `alert(msg)` แต่อาจจะต้องพึ่งกลไก Intercepter
|
||||
|
||||
page.on('dialog', async dialog => {
|
||||
// หน้าต่าง Alert ถ้ามีสิทธิ์ไม่อนุญาตมันจะเด้งอันนี้
|
||||
expect(dialog.message()).toBeTruthy();
|
||||
await dialog.accept();
|
||||
const menuCurriculumBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'menu_open' }) }).first();
|
||||
await expect(menuCurriculumBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// 2. Sidebar หลักสูตร
|
||||
const sidebar = page.locator('.q-drawer').first();
|
||||
if (!await sidebar.isVisible()) {
|
||||
await menuCurriculumBtn.click();
|
||||
}
|
||||
await expect(sidebar).toBeVisible();
|
||||
});
|
||||
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=99999`);
|
||||
|
||||
// รอดู Loading หายไป
|
||||
const loadingMask = page.locator('.animate-pulse, .q-spinner');
|
||||
await loadingMask.first().waitFor({ state: 'hidden', timeout: 20_000 }).catch(() => {});
|
||||
test('6.2 เช็คสถานะการเข้าถึงเนื้อหา (Access Control)', async ({ page }) => {
|
||||
page.on('dialog', async dialog => {
|
||||
expect(dialog.message()).toBeTruthy();
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=99999`);
|
||||
|
||||
const loadingMask = page.locator('.animate-pulse, .q-spinner');
|
||||
await loadingMask.first().waitFor({ state: 'hidden', timeout: TIMEOUT.PAGE_LOAD }).catch(() => {});
|
||||
});
|
||||
|
||||
test('6.3 การแสดงผลช่องวิดีโอ หรือ พื้นที่ทำข้อสอบ (Video / Quiz)', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||
|
||||
const videoLocator = page.locator('video').first();
|
||||
const quizLocator = page.getByText(/เริ่มทำแบบทดสอบ|แบบทดสอบ/i).first();
|
||||
const errorLocator = page.getByText(/ไม่สามารถเข้าถึง/i).first();
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
videoLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD }),
|
||||
quizLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD }),
|
||||
errorLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD })
|
||||
]);
|
||||
|
||||
const isOkay = (await videoLocator.isVisible()) || (await quizLocator.isVisible()) || (await errorLocator.isVisible());
|
||||
expect(isOkay).toBeTruthy();
|
||||
} catch {
|
||||
await page.screenshot({ path: 'tests/e2e/screenshots/classroom-blank-state.png', fullPage: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('6.3 การแสดงผลช่องวิดีโอ (Video Player) หรือ พื้นที่ทำข้อสอบ (Quiz)', async ({ page }) => {
|
||||
// เข้าหน้าห้องเรียน Course id: 1
|
||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||
|
||||
// กรณีที่ 1: อาจแสดง Video ถ้าเป็นบทเรียนวิดีโอ
|
||||
const videoLocator = page.locator('video').first();
|
||||
|
||||
// กรณีที่ 2: ถ้าบทแรกเป็น Quiz จะแสดงไอคอนแบบทดสอบ
|
||||
const quizLocator = page.getByText(/เริ่มทำแบบทดสอบ|แบบทดสอบ/i).first();
|
||||
|
||||
// กรณีที่ 3: ไม่มีบทเรียนเนื้อหาใดๆ เลยให้แสดง
|
||||
const errorLocator = page.getByText(/ไม่สามารถเข้าถึง/i).first();
|
||||
// --------------------------------------------------
|
||||
// Section 2: แบบทดสอบ (Quiz System)
|
||||
// --------------------------------------------------
|
||||
test.describe('แบบทดสอบ (Quiz System)', () => {
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
videoLocator.waitFor({ state: 'visible', timeout: 20_000 }),
|
||||
quizLocator.waitFor({ state: 'visible', timeout: 20_000 }),
|
||||
errorLocator.waitFor({ state: 'visible', timeout: 20_000 })
|
||||
]);
|
||||
|
||||
const isOkay = (await videoLocator.isVisible()) || (await quizLocator.isVisible()) || (await errorLocator.isVisible());
|
||||
expect(isOkay).toBeTruthy();
|
||||
} catch {
|
||||
// ถ้าไม่มีเลยใน 20 วิ ถือว่าหน้าอาจจะล้มเหลว หรือเป็น Content เปล่า
|
||||
// ให้ลอง Capture เพื่อเก็บข้อมูลไปใช้งาน
|
||||
await page.screenshot({ path: 'tests/e2e/screenshots/classroom-blank-state.png', fullPage: true });
|
||||
}
|
||||
test('7.1 โหลดหน้า Quiz และเริ่มทำข้อสอบได้ (Start Screen)', async ({ page }) => {
|
||||
await mockQuizData(page);
|
||||
await page.goto(`${BASE_URL}/classroom/quiz?course_id=2&lesson_id=17`);
|
||||
|
||||
const startBtn = page.getByRole('button', { name: /เริ่มทำแบบทดสอบ|Start/i }).first();
|
||||
await expect(startBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// กดเริ่มทำ
|
||||
await startBtn.click();
|
||||
|
||||
// เช็คว่าหน้า Taking (คำถามข้อที่ 1) โผล่มา
|
||||
const questionText = page.locator('h3').first();
|
||||
await expect(questionText).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||
});
|
||||
|
||||
test('7.2 แถบข้อสอบแบ่งหน้า (Pagination — เลื่อนซ้าย/ขวา)', async ({ page }) => {
|
||||
await mockQuizData(page);
|
||||
await page.goto(`${BASE_URL}/classroom/quiz?course_id=2&lesson_id=17`);
|
||||
|
||||
const startBtn = page.getByRole('button', { name: /เริ่มทำแบบทดสอบ|Start/i }).first();
|
||||
await expect(startBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
await startBtn.click();
|
||||
|
||||
// ลูกศรเลื่อนหน้าข้อสอบ
|
||||
const nextPaginationPageBtn = page.locator('button').filter({ has: page.locator('i.q-icon:has-text("chevron_right")') }).first();
|
||||
|
||||
if (await nextPaginationPageBtn.isVisible()) {
|
||||
await expect(nextPaginationPageBtn).toBeEnabled();
|
||||
await nextPaginationPageBtn.click();
|
||||
|
||||
// ข้อที่ 11 ต้องแสดง
|
||||
const question11Btn = page.locator('button').filter({ hasText: /^11$/ }).first();
|
||||
await expect(question11Btn).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('7.3 การแสดงผลปุ่มถัดไป/ส่งคำตอบ (Submit & Navigation UI)', async ({ page }) => {
|
||||
await mockQuizData(page);
|
||||
await page.goto(`${BASE_URL}/classroom/quiz?course_id=2&lesson_id=17`);
|
||||
|
||||
const startBtn = page.getByRole('button', { name: /เริ่มทำแบบทดสอบ|Start/i }).first();
|
||||
await expect(startBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
await startBtn.click();
|
||||
|
||||
// รอคำถามโหลดเสร็จ
|
||||
await expect(page.locator('h3').first()).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||
|
||||
const submitBtn = page.locator('button').filter({ hasText: /(ส่งคำตอบ|Submit)/i }).first();
|
||||
const nextBtn = page.locator('button').filter({ hasText: /(ถัดไป|Next)/i }).first();
|
||||
|
||||
// ข้อแรกต้องมีปุ่มถัดไปหรือปุ่มส่ง
|
||||
await expect(submitBtn.or(nextBtn)).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue