elearning/Frontend-Learner/tests/e2e/auth.spec.ts
supalerk-ar66 b6c1aebe30
Some checks failed
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Failing after 25s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Has been skipped
Build and Deploy Frontend Learner / Notify Deployment Status (push) Failing after 1s
feat: Implement Playwright E2E tests for authentication, quiz, student account, and discovery, and add a new quiz page.
2026-03-04 15:07:02 +07:00

266 lines
14 KiB
TypeScript

import { test, expect, type Page, type Locator } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
async function waitAppSettled(page: Page) {
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(250);
}
// ---------------------------
// Helpers: Login
// ---------------------------
const LOGIN_EMAIL = 'studentedtest@example.com';
const LOGIN_PASSWORD = 'admin123';
function loginEmailLocator(page: Page): Locator {
return page.locator('input[type="email"]').or(page.getByRole('textbox', { name: /อีเมล|email/i })).first();
}
function loginPasswordLocator(page: Page): Locator {
return page.locator('input[type="password"]').or(page.getByRole('textbox', { name: /รหัสผ่าน|password/i })).first();
}
function loginButtonLocator(page: Page): Locator {
return page.getByRole('button', { name: /เข้าสู่ระบบ|login/i }).or(page.locator('button[type="submit"]')).first();
}
async function expectAnyVisible(page: Page, locators: Locator[], timeout = 20_000) {
const start = Date.now();
while (Date.now() - start < timeout) {
for (const loc of locators) {
try {
if (await loc.isVisible()) return;
} catch {}
}
await page.waitForTimeout(200);
}
throw new Error('None of the expected locators became visible.');
}
// ---------------------------
// 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 loginEmailLocator(page).fill(LOGIN_EMAIL);
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
await loginButtonLocator(page).click();
await page.waitForURL('**/dashboard', { timeout: 25_000 });
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, 20_000);
});
test('Invalid Email - Thai characters (พิมพ์ภาษาไทย)', async ({ page }) => {
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
await waitAppSettled(page);
await loginEmailLocator(page).fill('ทดสอบภาษาไทย');
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
const errorHint = page.getByText('ห้ามใส่ภาษาไทย');
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
});
test('Invalid Email Format (อีเมลผิดรูปแบบ)', async ({ page }) => {
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
await waitAppSettled(page);
await loginEmailLocator(page).fill('test@domain');
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
await loginButtonLocator(page).click();
await waitAppSettled(page);
const errorHint = page.getByText('กรุณากรอกอีเมลให้ถูกต้อง (you@example.com)');
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
});
test('Wrong Password (รหัสผ่านผิด หรืออีเมลไม่ถูกต้องในระบบ)', async ({ page }) => {
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
await waitAppSettled(page);
await loginEmailLocator(page).fill(LOGIN_EMAIL);
await loginPasswordLocator(page).fill('wrong-password-123');
await loginButtonLocator(page).click();
await waitAppSettled(page);
const errorHint = page.getByText('กรุณาเช็ค Email หรือ รหัสผ่านใหม่อีกครั้ง');
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
});
});
// --- 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: 15_000 });
await expect(regSubmit(page)).toBeVisible({ timeout: 15_000 });
});
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: 15_000 });
});
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);
const navToLogin = page.waitForURL('**/auth/login', { timeout: 25_000, waitUntil: 'domcontentloaded' }).then(() => 'login' as const).catch(() => null);
const successToast = page.getByText(/สมัครสมาชิกสำเร็จ|success/i, { exact: false }).first().waitFor({ state: 'visible', timeout: 25_000 }).then(() => 'success' as const).catch(() => null);
const anyError = regErrorBox(page).first().waitFor({ state: 'visible', timeout: 25_000 }).then(() => 'error' as const).catch(() => null);
const result = await Promise.race([navToLogin, successToast, anyError]);
if (result === 'error') {
throw new Error('Register errors visible');
}
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: 15_000 });
await expect(page.getByRole('heading', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
await expect(page.getByRole('button', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
});
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();
const err = page.getByText(/ห้ามใส่ภาษาไทย|อีเมลไม่ถูกต้อง|รูปแบบอีเมล/i, { exact: false }).or(regErrorBox(page).filter({ hasText: /ไทย|อีเมล|email|invalid/i }));
await expect(err.first()).toBeVisible({ timeout: 12_000 });
});
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?');
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: 12_000 });
});
});
// --- 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();
const err = page.getByText(/ห้ามใส่ภาษาไทย/i).first();
await expect(err).toBeVisible({ timeout: 10_000 });
});
test('กดลิงก์กลับไปหน้า Login ได้', async ({ page }) => {
await forgotBackLink(page).click();
await page.waitForURL('**/auth/login', { timeout: 10_000 });
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: 10_000 });
await expect(page.getByText(/กรุณาตรวจสอบกล่องจดหมาย/i, { exact: false })).toBeVisible();
});
});
});