241 lines
No EOL
9.8 KiB
TypeScript
241 lines
No EOL
9.8 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);
|
|
}
|
|
|
|
// ===== Anchors / Scope =====
|
|
function headingRegister(page: Page) {
|
|
return page.getByRole('heading', { name: 'สร้างบัญชีผู้ใช้งาน' });
|
|
}
|
|
|
|
// ===== Inputs (ตาม snapshot ที่คุณส่งมา) =====
|
|
function usernameInput(page: Page): Locator {
|
|
// snapshot: textbox "username"
|
|
return page.getByRole('textbox', { name: 'username' }).first();
|
|
}
|
|
|
|
function emailInput(page: Page): Locator {
|
|
// snapshot: textbox "student@example.com"
|
|
return page.getByRole('textbox', { name: 'student@example.com' }).first();
|
|
}
|
|
|
|
function prefixCombobox(page: Page): Locator {
|
|
// snapshot: combobox มี option นาย/นาง/นางสาว
|
|
return page.getByRole('combobox').first();
|
|
}
|
|
|
|
function firstNameInput(page: Page): Locator {
|
|
// snapshot: label "ชื่อ *" + textbox
|
|
return page.getByText(/^ชื่อ\s*\*$/).locator('..').getByRole('textbox').first();
|
|
}
|
|
|
|
function lastNameInput(page: Page): Locator {
|
|
return page.getByText(/^นามสกุล\s*\*$/).locator('..').getByRole('textbox').first();
|
|
}
|
|
|
|
function phoneInput(page: Page): Locator {
|
|
return page.getByText(/^เบอร์โทรศัพท์\s*\*$/).locator('..').getByRole('textbox').first();
|
|
}
|
|
|
|
function passwordInput(page: Page): Locator {
|
|
// snapshot: label "รหัสผ่าน *" + textbox (มีปุ่ม visibility อยู่ข้างๆ)
|
|
return page.getByText(/^รหัสผ่าน\s*\*$/).locator('..').getByRole('textbox').first();
|
|
}
|
|
|
|
function confirmPasswordInput(page: Page): Locator {
|
|
return page.getByText(/^ยืนยันรหัสผ่าน\s*\*$/).locator('..').getByRole('textbox').first();
|
|
}
|
|
|
|
function submitButton(page: Page): Locator {
|
|
return page.getByRole('button', { name: 'สร้างบัญชี' });
|
|
}
|
|
|
|
function loginLink(page: Page): Locator {
|
|
return page.getByRole('link', { name: 'เข้าสู่ระบบ' });
|
|
}
|
|
|
|
function errorBox(page: Page): Locator {
|
|
// ทั้ง field message และ notification/toast/alert
|
|
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 = prefixCombobox(page);
|
|
|
|
// ถ้าเป็น <select> จริง selectOption จะเวิร์คทันที
|
|
await combo.selectOption({ label: value }).catch(async () => {
|
|
// fallback: คลิกแล้วเลือก option
|
|
await combo.click();
|
|
await page.getByRole('option', { name: value }).click();
|
|
});
|
|
}
|
|
|
|
// ===== Test data =====
|
|
function uniqueUser() {
|
|
const n = Date.now().toString().slice(-6);
|
|
|
|
// ✅ แก้ปัญหา "Phone number already exists" ด้วยเบอร์สุ่มไม่ซ้ำ
|
|
// รูปแบบ 09xxxxxxxx (10 หลัก)
|
|
const rand8 = Math.floor(Math.random() * 1e8).toString().padStart(8, '0');
|
|
const phone = `09${rand8}`;
|
|
|
|
return {
|
|
username: `e2e_user_${n}`,
|
|
email: `e2e_${n}@example.com`,
|
|
firstName: 'ทดสอบ',
|
|
lastName: 'ระบบ',
|
|
phone,
|
|
password: 'Admin12345!',
|
|
};
|
|
}
|
|
|
|
// ================== TESTS ==================
|
|
test.describe('Register Page (auth/register)', () => {
|
|
test('หน้า Register ต้องโหลดได้ (Smoke)', async ({ page }) => {
|
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
|
await waitAppSettled(page);
|
|
|
|
await expect(headingRegister(page)).toBeVisible({ timeout: 15_000 });
|
|
await expect(submitButton(page)).toBeVisible({ timeout: 15_000 });
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-page.png', fullPage: true });
|
|
});
|
|
|
|
test('ลิงก์ "เข้าสู่ระบบ" ต้องกดแล้วไปหน้า login ได้', async ({ page }) => {
|
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
|
await waitAppSettled(page);
|
|
|
|
await loginLink(page).click();
|
|
await page.waitForURL('**/auth/login', { timeout: 15_000 });
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-go-login.png', fullPage: true });
|
|
});
|
|
|
|
test('สมัครสมาชิกสำเร็จ (Happy Path) → redirect ไป /auth/login', async ({ page }) => {
|
|
const u = uniqueUser();
|
|
|
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
|
await waitAppSettled(page);
|
|
|
|
await usernameInput(page).fill(u.username);
|
|
await emailInput(page).fill(u.email);
|
|
await pickPrefix(page, 'นาย');
|
|
|
|
await firstNameInput(page).fill(u.firstName);
|
|
await lastNameInput(page).fill(u.lastName);
|
|
await phoneInput(page).fill(u.phone);
|
|
|
|
await passwordInput(page).fill(u.password);
|
|
await confirmPasswordInput(page).fill(u.password);
|
|
|
|
await submitButton(page).click();
|
|
await waitAppSettled(page);
|
|
|
|
// ✅ รอ 3 อย่าง: ไป login / success toast / error
|
|
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 = errorBox(page)
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 25_000 })
|
|
.then(() => 'error' as const)
|
|
.catch(() => null);
|
|
|
|
const result = await Promise.race([navToLogin, successToast, anyError]);
|
|
|
|
// ถ้ามี error ให้ fail พร้อม log ชัดๆ
|
|
if (result === 'error') {
|
|
const errs = await errorBox(page).allInnerTexts().catch(() => []);
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-happy-error.png', fullPage: true });
|
|
throw new Error(`Register did not redirect. Errors: ${errs.join(' | ')}`);
|
|
}
|
|
|
|
// ถ้ามีแต่ toast success แต่ยังไม่ redirect ให้ไปหน้า login เอง (ตาม flow ที่คุณต้องการ)
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ✅ สุดท้ายต้องอยู่หน้า /auth/login แน่นอน
|
|
await expect(page).toHaveURL(/\/auth\/login/i, { timeout: 15_000 });
|
|
|
|
// ✅ แก้ strict mode: ระบุให้ชัดว่าเป็น heading และ button
|
|
await expect(page.getByRole('heading', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByRole('button', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
|
|
|
|
// optional: การ์ด TEST ACCOUNT (ถ้ามี)
|
|
await expect(page.getByText(/TEST ACCOUNT/i, { exact: false }))
|
|
.toBeVisible({ timeout: 10_000 })
|
|
.catch(() => {});
|
|
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-redirect-login.png', fullPage: true });
|
|
});
|
|
|
|
test('Invalid Email - ใส่ภาษาไทย ต้องขึ้น error', async ({ page }) => {
|
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
|
await waitAppSettled(page);
|
|
|
|
await emailInput(page).fill('ทดสอบภาษาไทย');
|
|
await usernameInput(page).click(); // blur trigger
|
|
|
|
const err = page
|
|
.getByText(/ห้ามใส่ภาษาไทย|อีเมลไม่ถูกต้อง|รูปแบบอีเมล/i, { exact: false })
|
|
.or(errorBox(page).filter({ hasText: /ไทย|อีเมล|email|invalid/i }));
|
|
|
|
await expect(err.first()).toBeVisible({ timeout: 12_000 });
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-invalid-email-thai.png', fullPage: true });
|
|
});
|
|
|
|
test('Password ไม่ตรงกัน ต้องขึ้น error (ต้องกดสร้างบัญชีก่อน)', async ({ page }) => {
|
|
const u = uniqueUser();
|
|
|
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
|
await waitAppSettled(page);
|
|
|
|
await usernameInput(page).fill(u.username);
|
|
await emailInput(page).fill(u.email);
|
|
await pickPrefix(page, 'นาย');
|
|
|
|
await firstNameInput(page).fill(u.firstName);
|
|
await lastNameInput(page).fill(u.lastName);
|
|
await phoneInput(page).fill(u.phone);
|
|
|
|
await passwordInput(page).fill('Admin12345!');
|
|
await confirmPasswordInput(page).fill('Admin12345?'); // mismatch
|
|
|
|
// ✅ ต้อง submit ก่อน error ถึงขึ้น
|
|
await submitButton(page).click();
|
|
await waitAppSettled(page);
|
|
|
|
const mismatchErr = page
|
|
.getByText(/รหัสผ่านไม่ตรงกัน/i, { exact: false })
|
|
.or(errorBox(page).filter({ hasText: /รหัสผ่านไม่ตรงกัน/i }));
|
|
|
|
await expect(mismatchErr.first()).toBeVisible({ timeout: 12_000 });
|
|
await page.screenshot({ path: 'tests/e2e/screenshots/register-password-mismatch.png', fullPage: true });
|
|
});
|
|
}); |