/** * @file helpers.ts * @description Shared E2E test helpers — ฟังก์ชันที่ใช้ร่วมกันในทุกไฟล์ test * รวม: waitAppSettled, login helpers, common locators, constants */ import { type Page, type Locator, expect } from '@playwright/test'; // ========================================== // Constants // ========================================== export const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; export const TEST_EMAIL = 'studentedtest@example.com'; export const TEST_PASSWORD = 'admin123'; /** Timeout configs — ปรับค่าได้ที่เดียว */ export const TIMEOUT: Record = { /** รอหน้าโหลด */ PAGE_LOAD: 15_000, /** รอ login + redirect */ LOGIN: 25_000, /** รอ element แสดงผล */ ELEMENT: 12_000, /** รอ network settle */ SETTLE: 300, }; // ========================================== // Wait Helpers // ========================================== /** * รอให้แอปโหลดเสร็จสมบูรณ์ (DOM + Network + hydration) */ export async function waitAppSettled(page: Page, ms = TIMEOUT.SETTLE) { await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle').catch(() => {}); await page.waitForTimeout(ms); } /** * รอจนกว่า locator ใดก็ได้ใน array จะ visible * @throws เมื่อไม่มี locator ไหน visible ภายใน timeout */ export async function expectAnyVisible( page: Page, locators: Locator[], timeout = TIMEOUT.PAGE_LOAD ) { const start = Date.now(); while (Date.now() - start < timeout) { for (const loc of locators) { try { if (await loc.isVisible()) return; } catch { /* locator detached / stale — ลองใหม่ */ } } await page.waitForTimeout(200); } throw new Error( `None of the expected locators became visible within ${timeout}ms` ); } // ========================================== // Login Locators // ========================================== export function emailLocator(page: Page): Locator { return page .locator('input[type="email"]') .or(page.getByRole('textbox', { name: /อีเมล|email/i })) .first(); } export function passwordLocator(page: Page): Locator { return page .locator('input[type="password"]') .or(page.getByRole('textbox', { name: /รหัสผ่าน|password/i })) .first(); } export function loginButtonLocator(page: Page): Locator { return page .getByRole('button', { name: /เข้าสู่ระบบ|login/i }) .or(page.locator('button[type="submit"]')) .first(); } // ========================================== // Login Flow // ========================================== /** * ล็อกอินด้วย test account — ใช้ใน beforeEach ของ tests ที่ต้อง authenticate * * @param page — Playwright Page * @param opts — ตัวเลือกเสริม * @param opts.assertDashboard — (default: true) ถ้า true จะ assert ว่าเข้า dashboard สำเร็จ * * @throws หาก login ล้มเหลวหรือไม่ถึง dashboard */ export async function setupLogin( page: Page, opts: { assertDashboard?: boolean } = {} ) { const { assertDashboard = true } = opts; await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); // กรอกข้อมูล await emailLocator(page).fill(TEST_EMAIL); await passwordLocator(page).fill(TEST_PASSWORD); await loginButtonLocator(page).click(); // รอ redirect ไป dashboard await page.waitForURL('**/dashboard', { timeout: TIMEOUT.LOGIN }); await waitAppSettled(page); if (assertDashboard) { // ยืนยันว่าเข้า dashboard ได้จริง const evidence = [ page.locator('.q-page-container').first(), page.locator('.q-drawer').first(), page.locator('img[src*="avataaars"]').first(), page.locator('img[alt],[alt="User Avatar"]').first(), ]; await expectAnyVisible(page, evidence, TIMEOUT.PAGE_LOAD); } }