/** * @file auth.spec.ts * @description ทดสอบระบบยืนยันตัวตน (Authentication) — Login, Register, Forgot Password */ import { test, expect, type Page, type Locator } from '@playwright/test'; import { BASE_URL, TEST_EMAIL, TEST_PASSWORD, TIMEOUT, waitAppSettled, expectAnyVisible, emailLocator, passwordLocator, loginButtonLocator, } from './helpers'; // --------------------------- // Helpers: Register // --------------------------- function regHeading(page: Page) { return page.getByRole('heading', { name: 'สร้างบัญชีผู้ใช้งาน' }); } function regUsername(page: Page) { return page.getByRole('textbox', { name: 'username' }).first(); } function regEmail(page: Page) { return page.getByRole('textbox', { name: 'student@example.com' }).first(); } function regPrefix(page: Page) { return page.getByRole('combobox').first(); } function regFirstName(page: Page) { return page.getByText(/^ชื่อ\s*\*$/).locator('..').getByRole('textbox').first(); } function regLastName(page: Page) { return page.getByText(/^นามสกุล\s*\*$/).locator('..').getByRole('textbox').first(); } function regPhone(page: Page) { return page.getByText(/^เบอร์โทรศัพท์\s*\*$/).locator('..').getByRole('textbox').first(); } function regPassword(page: Page) { return page.getByText(/^รหัสผ่าน\s*\*$/).locator('..').getByRole('textbox').first(); } function regConfirmPassword(page: Page) { return page.getByText(/^ยืนยันรหัสผ่าน\s*\*$/).locator('..').getByRole('textbox').first(); } function regSubmit(page: Page) { return page.getByRole('button', { name: 'สร้างบัญชี' }); } function regLoginLink(page: Page) { return page.getByRole('link', { name: 'เข้าสู่ระบบ' }); } function regErrorBox(page: Page) { return page.locator(['.q-field__messages', '.q-field__bottom', '.text-negative', '.q-notification', '.q-banner', '[role="alert"]'].join(', ')); } async function pickPrefix(page: Page, value: 'นาย' | 'นาง' | 'นางสาว' = 'นาย') { const combo = regPrefix(page); await combo.selectOption({ label: value }).catch(async () => { await combo.click(); await page.getByRole('option', { name: value }).click(); }); } function uniqueUser() { const n = Date.now().toString().slice(-6); const rand8 = Math.floor(Math.random() * 1e8).toString().padStart(8, '0'); return { username: `e2e_user_${n}`, email: `e2e_${n}@example.com`, firstName: 'ทดสอบ', lastName: 'ระบบ', phone: `09${rand8}`, password: 'Admin12345!', }; } // --------------------------- // Helpers: Forgot Password // --------------------------- const FORGOT_URL = `${BASE_URL}/auth/forgot-password`; function forgotEmail(page: Page) { return page.locator('input[type="email"]').or(page.getByRole('textbox')).first(); } function forgotSubmit(page: Page) { return page.getByRole('button', { name: /ส่งลิงก์รีเซ็ต/i }).first(); } function forgotBackLink(page: Page) { return page.getByRole('link', { name: /กลับไปหน้าเข้าสู่ระบบ/i }).first(); } // ================== TESTS ================== test.describe('ระบบยืนยันตัวตน (Authentication)', () => { // --- LOGIN --- test.describe('การเข้าสู่ระบบ (Login)', () => { test('Success Login แล้วเข้า /dashboard ได้', async ({ page }) => { 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(); await page.waitForURL('**/dashboard', { timeout: TIMEOUT.LOGIN }); await waitAppSettled(page); const dashboardEvidence = [ 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, dashboardEvidence, TIMEOUT.PAGE_LOAD); }); test('Invalid Email - Thai characters (พิมพ์ภาษาไทย)', async ({ page }) => { await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await emailLocator(page).fill('ทดสอบภาษาไทย'); await passwordLocator(page).fill(TEST_PASSWORD); await expect(page.getByText('ห้ามใส่ภาษาไทย').first()).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); test('Invalid Email Format (อีเมลผิดรูปแบบ)', async ({ page }) => { await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await emailLocator(page).fill('test@domain'); await passwordLocator(page).fill(TEST_PASSWORD); await loginButtonLocator(page).click(); await waitAppSettled(page); await expect( page.getByText('กรุณากรอกอีเมลให้ถูกต้อง (you@example.com)').first() ).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); test('Wrong Password (รหัสผ่านผิด หรืออีเมลไม่ถูกต้องในระบบ)', async ({ page }) => { await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await emailLocator(page).fill(TEST_EMAIL); await passwordLocator(page).fill('wrong-password-123'); await loginButtonLocator(page).click(); await waitAppSettled(page); await expect( page.getByText('กรุณาเช็ค Email หรือ รหัสผ่านใหม่อีกครั้ง').first() ).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); }); // --- REGISTER --- test.describe('การสมัครสมาชิก (Register)', () => { test('หน้า Register ต้องโหลดได้ (Smoke)', async ({ page }) => { await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await expect(regHeading(page)).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD }); await expect(regSubmit(page)).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD }); }); test('ลิงก์ "เข้าสู่ระบบ" ต้องกดแล้วไปหน้า login ได้', async ({ page }) => { await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await regLoginLink(page).click(); await page.waitForURL('**/auth/login', { timeout: TIMEOUT.PAGE_LOAD }); }); test('สมัครสมาชิกสำเร็จ (Happy Path) → redirect ไป /auth/login', async ({ page }) => { const u = uniqueUser(); await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await regUsername(page).fill(u.username); await regEmail(page).fill(u.email); await pickPrefix(page, 'นาย'); await regFirstName(page).fill(u.firstName); await regLastName(page).fill(u.lastName); await regPhone(page).fill(u.phone); await regPassword(page).fill(u.password); await regConfirmPassword(page).fill(u.password); await regSubmit(page).click(); await waitAppSettled(page); // รอ 3 สัญญาณ: redirect ไป login / success toast / error const navToLogin = page.waitForURL('**/auth/login', { timeout: TIMEOUT.LOGIN, waitUntil: 'domcontentloaded' }).then(() => 'login' as const).catch(() => null); const successToast = page.getByText(/สมัครสมาชิกสำเร็จ|success/i, { exact: false }).first().waitFor({ state: 'visible', timeout: TIMEOUT.LOGIN }).then(() => 'success' as const).catch(() => null); const anyError = regErrorBox(page).first().waitFor({ state: 'visible', timeout: TIMEOUT.LOGIN }).then(() => 'error' as const).catch(() => null); const result = await Promise.race([navToLogin, successToast, anyError]); if (result === 'error') { const errs = await regErrorBox(page).allInnerTexts().catch(() => []); throw new Error(`Register failed with errors: ${errs.join(' | ')}`); } // ถ้ามี toast แต่ยัง redirect ไม่ไป ให้ navigate เอง if (!page.url().includes('/auth/login')) { const hasSuccess = await page.getByText(/สมัครสมาชิกสำเร็จ/i, { exact: false }).first().isVisible().catch(() => false); if (hasSuccess) { await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); } } await expect(page).toHaveURL(/\/auth\/login/i, { timeout: TIMEOUT.PAGE_LOAD }); await expect(page.getByRole('heading', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD }); await expect(page.getByRole('button', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD }); }); test('Invalid Email - ใส่ภาษาไทย ต้องขึ้น error', async ({ page }) => { await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await regEmail(page).fill('ทดสอบภาษาไทย'); await regUsername(page).click(); // blur trigger const err = page .getByText(/ห้ามใส่ภาษาไทย|อีเมลไม่ถูกต้อง|รูปแบบอีเมล/i, { exact: false }) .or(regErrorBox(page).filter({ hasText: /ไทย|อีเมล|email|invalid/i })); await expect(err.first()).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); test('Password ไม่ตรงกัน ต้องขึ้น error (ต้องกดสร้างบัญชีก่อน)', async ({ page }) => { const u = uniqueUser(); await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); await regUsername(page).fill(u.username); await regEmail(page).fill(u.email); await pickPrefix(page, 'นาย'); await regFirstName(page).fill(u.firstName); await regLastName(page).fill(u.lastName); await regPhone(page).fill(u.phone); await regPassword(page).fill('Admin12345!'); await regConfirmPassword(page).fill('Admin12345?'); // mismatch await regSubmit(page).click(); await waitAppSettled(page); const mismatchErr = page .getByText(/รหัสผ่านไม่ตรงกัน/i, { exact: false }) .or(regErrorBox(page).filter({ hasText: /รหัสผ่านไม่ตรงกัน/i })); await expect(mismatchErr.first()).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); }); // --- FORGOT PASSWORD --- test.describe('หน้าลืมรหัสผ่าน (Forgot Password)', () => { test.beforeEach(async ({ page }) => { await page.goto(FORGOT_URL, { waitUntil: 'domcontentloaded' }); await waitAppSettled(page); }); test('โหลดหน้าลืมรหัสผ่านได้ครบถ้วน (Smoke Test)', async ({ page }) => { await expect(page.getByRole('heading', { name: /ลืมรหัสผ่าน/i })).toBeVisible(); await expect(forgotEmail(page)).toBeVisible(); await expect(forgotSubmit(page)).toBeVisible(); await expect(forgotBackLink(page)).toBeVisible(); }); test('Validation: ใส่อีเมลภาษาไทยแล้วขึ้น Error', async ({ page }) => { await forgotEmail(page).fill('ฟฟฟฟ'); await page.getByRole('heading', { name: /ลืมรหัสผ่าน/i }).click(); await expect(page.getByText(/ห้ามใส่ภาษาไทย/i).first()).toBeVisible({ timeout: TIMEOUT.ELEMENT }); }); test('กดลิงก์กลับไปหน้า Login ได้', async ({ page }) => { await forgotBackLink(page).click(); await page.waitForURL('**/auth/login', { timeout: TIMEOUT.ELEMENT }); await expect(page).toHaveURL(/\/auth\/login/i); }); test('ทดลองส่งลิงก์รีเซ็ตรหัสผ่าน (API Mock)', async ({ page }) => { await page.route('**/*', async (route) => { const req = route.request(); const url = req.url(); const method = req.method(); const looksLikeForgotApi = method === 'POST' && /forgot|reset/i.test(url) && !/\.(png|jpg|jpeg|webp|svg|css|js|map)$/i.test(url); if (looksLikeForgotApi) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: { message: 'Reset link sent' } }) }); return; } await route.continue(); }); await forgotEmail(page).fill('test@gmail.com'); await forgotSubmit(page).click(); await expect(page.getByText(/ส่งลิงก์เรียบร้อยแล้ว/i, { exact: false })).toBeVisible({ timeout: TIMEOUT.ELEMENT }); await expect(page.getByText(/กรุณาตรวจสอบกล่องจดหมาย/i, { exact: false })).toBeVisible(); }); }); });