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
|
|
@ -40,7 +40,7 @@ export const useAuth = () => {
|
||||||
// ฟังก์ชันเข้าสู่ระบบ (Login)
|
// ฟังก์ชันเข้าสู่ระบบ (Login)
|
||||||
const login = async (credentials: { email: string; password: string }) => {
|
const login = async (credentials: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
// API returns { code: 200, message: "...", data: { token, user, ... } }
|
// API returns { code: 200, message: "...", data: { token, refreshToken } }
|
||||||
const response = await $fetch<any>(`${API_BASE_URL}/auth/login`, {
|
const response = await $fetch<any>(`${API_BASE_URL}/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: credentials
|
body: credentials
|
||||||
|
|
@ -49,16 +49,35 @@ export const useAuth = () => {
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
// Validation: Ensure user and role exist, then check for Role 'STUDENT'
|
// บันทึก Token ก่อน เพื่อใช้เรียก /user/me
|
||||||
if (!data.user || !data.user.role || data.user.role.code !== 'STUDENT') {
|
token.value = data.token
|
||||||
|
refreshToken.value = data.refreshToken
|
||||||
|
|
||||||
|
// ดึงข้อมูลผู้ใช้จาก /user/me (เพราะ API login ไม่ส่ง user กลับมาแล้ว)
|
||||||
|
try {
|
||||||
|
const userData = await $fetch<any>(`${API_BASE_URL}/user/me`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${data.token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validation: ตรวจสอบ Role ต้องเป็น STUDENT เท่านั้น
|
||||||
|
if (!userData || !userData.role || userData.role.code !== 'STUDENT') {
|
||||||
|
// ถ้า Role ไม่ใช่ STUDENT ให้ล้าง Token ออก
|
||||||
|
token.value = null
|
||||||
|
refreshToken.value = null
|
||||||
return { success: false, error: 'Email ไม่ถูกต้อง' }
|
return { success: false, error: 'Email ไม่ถูกต้อง' }
|
||||||
}
|
}
|
||||||
|
|
||||||
token.value = data.token
|
// เก็บข้อมูล User ลง Cookie
|
||||||
refreshToken.value = data.refreshToken // บันทึก Refresh Token
|
user.value = userData
|
||||||
|
} catch (profileErr) {
|
||||||
// API ส่งข้อมูล profile มาใน user object
|
// ดึงข้อมูลผู้ใช้ไม่สำเร็จ ให้ล้าง Token ออก
|
||||||
user.value = data.user
|
console.error('Failed to fetch user profile after login:', profileErr)
|
||||||
|
token.value = null
|
||||||
|
refreshToken.value = null
|
||||||
|
return { success: false, error: 'ไม่สามารถดึงข้อมูลผู้ใช้ได้' }
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const sortBy = ref('ยอดนิยม');
|
||||||
const sortOptions = ['ยอดนิยม', 'ล่าสุด', 'ราคาต่ำ-สูงสุด', 'ราคาสูง-ต่ำสุด'];
|
const sortOptions = ['ยอดนิยม', 'ล่าสุด', 'ราคาต่ำ-สูงสุด', 'ราคาสูง-ต่ำสุด'];
|
||||||
|
|
||||||
const categories = ref<any[]>([]);
|
const categories = ref<any[]>([]);
|
||||||
const courses = ref<any[]>([]);
|
const allCourses = ref<any[]>([]); // เก็บคอร์สทั้งหมดเพื่อกรอง client-side
|
||||||
const selectedCourse = ref<any>(null);
|
const selectedCourse = ref<any>(null);
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
@ -76,20 +76,17 @@ const loadCategories = async () => {
|
||||||
if (res.success) categories.value = res.data || [];
|
if (res.success) categories.value = res.data || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCourses = async (page = 1) => {
|
const loadCourses = async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const categoryId = activeCategory.value === 'all' ? undefined : activeCategory.value as number;
|
|
||||||
|
|
||||||
|
// โหลดคอร์สทั้งหมดครั้งเดียว (limit สูงๆ เพื่อ client-side filter)
|
||||||
const res = await fetchCourses({
|
const res = await fetchCourses({
|
||||||
category_id: categoryId,
|
limit: 500,
|
||||||
search: searchQuery.value,
|
|
||||||
page: page,
|
|
||||||
limit: itemsPerPage,
|
|
||||||
forceRefresh: true,
|
forceRefresh: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
courses.value = (res.data || []).map(c => {
|
allCourses.value = (res.data || []).map(c => {
|
||||||
const cat = categories.value.find(cat => cat.id === c.category_id);
|
const cat = categories.value.find(cat => cat.id === c.category_id);
|
||||||
return {
|
return {
|
||||||
...c,
|
...c,
|
||||||
|
|
@ -100,12 +97,33 @@ const loadCourses = async (page = 1) => {
|
||||||
reviews_count: c.total_lessons ? c.total_lessons * 123 : Math.floor(Math.random() * 2000) + 100
|
reviews_count: c.total_lessons ? c.total_lessons * 123 : Math.floor(Math.random() * 2000) + 100
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
totalPages.value = res.totalPages || 1;
|
|
||||||
currentPage.value = res.page || 1;
|
|
||||||
}
|
}
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Computed: กรองคอร์สแบบ real-time ตาม searchQuery + activeCategory
|
||||||
|
const filteredCourses = computed(() => {
|
||||||
|
let result = allCourses.value;
|
||||||
|
|
||||||
|
// กรองตามหมวดหมู่
|
||||||
|
if (activeCategory.value !== 'all') {
|
||||||
|
result = result.filter(c => c.category_id === activeCategory.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// กรองตามคำค้นหา (ค้นจากชื่อทั้ง th และ en)
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
const query = searchQuery.value.trim().toLowerCase();
|
||||||
|
result = result.filter(c => {
|
||||||
|
const titleTh = (c.title?.th || '').toLowerCase();
|
||||||
|
const titleEn = (c.title?.en || '').toLowerCase();
|
||||||
|
const titleStr = (typeof c.title === 'string' ? c.title : '').toLowerCase();
|
||||||
|
return titleTh.includes(query) || titleEn.includes(query) || titleStr.includes(query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
const selectCourse = async (id: number) => {
|
const selectCourse = async (id: number) => {
|
||||||
isLoadingDetail.value = true;
|
isLoadingDetail.value = true;
|
||||||
selectedCourse.value = null;
|
selectedCourse.value = null;
|
||||||
|
|
@ -137,10 +155,10 @@ watch(
|
||||||
activeCategory,
|
activeCategory,
|
||||||
() => {
|
() => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
loadCourses(1);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadCategories();
|
await loadCategories();
|
||||||
|
|
||||||
|
|
@ -150,7 +168,7 @@ onMounted(async () => {
|
||||||
activeCategory.value = Number(route.query.category_id);
|
activeCategory.value = Number(route.query.category_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadCourses(1);
|
await loadCourses();
|
||||||
|
|
||||||
if (route.query.course_id) {
|
if (route.query.course_id) {
|
||||||
selectCourse(Number(route.query.course_id));
|
selectCourse(Number(route.query.course_id));
|
||||||
|
|
@ -169,8 +187,8 @@ onMounted(async () => {
|
||||||
<h2 class="text-[1.35rem] font-bold text-slate-900 dark:text-white tracking-tight">คอร์สเรียนทั้งหมด</h2>
|
<h2 class="text-[1.35rem] font-bold text-slate-900 dark:text-white tracking-tight">คอร์สเรียนทั้งหมด</h2>
|
||||||
<div class="flex flex-wrap sm:flex-nowrap items-center gap-3 w-full md:w-auto">
|
<div class="flex flex-wrap sm:flex-nowrap items-center gap-3 w-full md:w-auto">
|
||||||
<div class="relative w-full sm:w-[260px] flex-1">
|
<div class="relative w-full sm:w-[260px] flex-1">
|
||||||
<q-icon name="search" size="18px" class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-[#3B6BE8]" />
|
<q-icon name="search" size="18px" class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||||
<input v-model="searchQuery" @keyup.enter="loadCourses(1)" class="w-full bg-slate-100 dark:bg-slate-800 border-none rounded-xl py-2.5 pl-11 pr-4 text-sm font-medium text-slate-700 dark:text-slate-200 placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500/20 outline-none transition-all shadow-sm" placeholder="ค้นหาคอร์ส..." />
|
<input v-model="searchQuery" class="w-full bg-slate-100 dark:bg-slate-800 border-none rounded-xl py-2.5 pl-11 pr-4 text-sm font-medium text-slate-700 dark:text-slate-200 placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500/20 outline-none transition-all shadow-sm" :placeholder="$t('discovery.searchPlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<button @click="viewMode = 'grid'" :class="viewMode === 'grid' ? 'bg-[#E9EFFD] dark:bg-blue-900/40 text-[#3B6BE8] border-[#3B6BE8]' : 'bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700 text-slate-400 hover:bg-slate-50'" class="w-[42px] h-[42px] flex items-center justify-center rounded-xl border transition-colors outline-none"><q-icon name="grid_view" size="20px" /></button>
|
<button @click="viewMode = 'grid'" :class="viewMode === 'grid' ? 'bg-[#E9EFFD] dark:bg-blue-900/40 text-[#3B6BE8] border-[#3B6BE8]' : 'bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700 text-slate-400 hover:bg-slate-50'" class="w-[42px] h-[42px] flex items-center justify-center rounded-xl border transition-colors outline-none"><q-icon name="grid_view" size="20px" /></button>
|
||||||
|
|
@ -208,10 +226,10 @@ onMounted(async () => {
|
||||||
<q-spinner size="3rem" color="primary" />
|
<q-spinner size="3rem" color="primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="courses.length > 0">
|
<div v-else-if="filteredCourses.length > 0">
|
||||||
<!-- GRID VIEW -->
|
<!-- GRID VIEW -->
|
||||||
<div v-if="viewMode === 'grid'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div v-if="viewMode === 'grid'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
<div v-for="course in courses" :key="course.id" class="flex flex-col rounded-[1.5rem] bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 overflow-hidden shadow-[0_2px_10px_rgb(0,0,0,0.03)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.08)] transition-all duration-300 group cursor-pointer" @click="selectCourse(course.id)">
|
<div v-for="course in filteredCourses" :key="course.id" class="flex flex-col rounded-[1.5rem] bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 overflow-hidden shadow-[0_2px_10px_rgb(0,0,0,0.03)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.08)] transition-all duration-300 group cursor-pointer" @click="selectCourse(course.id)">
|
||||||
<!-- Thumbnail -->
|
<!-- Thumbnail -->
|
||||||
<div class="relative w-full aspect-[16/10] bg-slate-100 dark:bg-slate-800 overflow-hidden">
|
<div class="relative w-full aspect-[16/10] bg-slate-100 dark:bg-slate-800 overflow-hidden">
|
||||||
<img :src="course.thumbnail_url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
<img :src="course.thumbnail_url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||||
|
|
@ -241,7 +259,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
<!-- LIST VIEW -->
|
<!-- LIST VIEW -->
|
||||||
<div v-else class="flex flex-col gap-5">
|
<div v-else class="flex flex-col gap-5">
|
||||||
<div v-for="course in courses" :key="course.id" class="flex flex-col sm:flex-row rounded-[1.5rem] bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 p-3 sm:p-4 gap-4 sm:gap-6 shadow-sm hover:shadow-[0_8px_30px_rgb(0,0,0,0.06)] transition-all duration-300 group cursor-pointer" @click="selectCourse(course.id)">
|
<div v-for="course in filteredCourses" :key="course.id" class="flex flex-col sm:flex-row rounded-[1.5rem] bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 p-3 sm:p-4 gap-4 sm:gap-6 shadow-sm hover:shadow-[0_8px_30px_rgb(0,0,0,0.06)] transition-all duration-300 group cursor-pointer" @click="selectCourse(course.id)">
|
||||||
<div class="relative w-full sm:w-[260px] aspect-[16/10] sm:aspect-auto sm:h-[160px] rounded-[1rem] bg-slate-100 dark:bg-slate-800 overflow-hidden shrink-0">
|
<div class="relative w-full sm:w-[260px] aspect-[16/10] sm:aspect-auto sm:h-[160px] rounded-[1rem] bg-slate-100 dark:bg-slate-800 overflow-hidden shrink-0">
|
||||||
<img :src="course.thumbnail_url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
<img :src="course.thumbnail_url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||||
<div v-if="course.category_name" class="absolute top-3 left-3 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md text-[#3B6BE8] dark:text-blue-400 font-bold text-[10px] px-3.5 py-1.5 rounded-full shadow-sm" style="border-radius: 9999px; padding: 4px 12px;">
|
<div v-if="course.category_name" class="absolute top-3 left-3 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md text-[#3B6BE8] dark:text-blue-400 font-bold text-[10px] px-3.5 py-1.5 rounded-full shadow-sm" style="border-radius: 9999px; padding: 4px 12px;">
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ await useAsyncData('categories-list', () => fetchCategories())
|
||||||
const { data: coursesResponse, pending: isLoading, error, refresh } = await useAsyncData(
|
const { data: coursesResponse, pending: isLoading, error, refresh } = await useAsyncData(
|
||||||
'browse-courses-list',
|
'browse-courses-list',
|
||||||
() => {
|
() => {
|
||||||
const params: any = {}
|
const params: any = { limit: 500 }
|
||||||
if (selectedCategory.value !== 'all') {
|
if (selectedCategory.value !== 'all') {
|
||||||
const category = categories.value.find(c => c.slug === selectedCategory.value)
|
const category = categories.value.find(c => c.slug === selectedCategory.value)
|
||||||
if (category) {
|
if (category) {
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,13 @@
|
||||||
|
/**
|
||||||
|
* @file auth.spec.ts
|
||||||
|
* @description ทดสอบระบบยืนยันตัวตน (Authentication) — Login, Register, Forgot Password
|
||||||
|
*/
|
||||||
import { test, expect, type Page, type Locator } from '@playwright/test';
|
import { test, expect, type Page, type Locator } from '@playwright/test';
|
||||||
|
import {
|
||||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
BASE_URL, TEST_EMAIL, TEST_PASSWORD, TIMEOUT,
|
||||||
|
waitAppSettled, expectAnyVisible,
|
||||||
async function waitAppSettled(page: Page) {
|
emailLocator, passwordLocator, loginButtonLocator,
|
||||||
await page.waitForLoadState('domcontentloaded');
|
} from './helpers';
|
||||||
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
|
// Helpers: Register
|
||||||
|
|
@ -53,6 +26,7 @@ function regLoginLink(page: Page) { return page.getByRole('link', { name: 'เ
|
||||||
function regErrorBox(page: Page) {
|
function regErrorBox(page: Page) {
|
||||||
return page.locator(['.q-field__messages', '.q-field__bottom', '.text-negative', '.q-notification', '.q-banner', '[role="alert"]'].join(', '));
|
return page.locator(['.q-field__messages', '.q-field__bottom', '.text-negative', '.q-notification', '.q-banner', '[role="alert"]'].join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickPrefix(page: Page, value: 'นาย' | 'นาง' | 'นางสาว' = 'นาย') {
|
async function pickPrefix(page: Page, value: 'นาย' | 'นาง' | 'นางสาว' = 'นาย') {
|
||||||
const combo = regPrefix(page);
|
const combo = regPrefix(page);
|
||||||
await combo.selectOption({ label: value }).catch(async () => {
|
await combo.selectOption({ label: value }).catch(async () => {
|
||||||
|
|
@ -60,6 +34,7 @@ async function pickPrefix(page: Page, value: 'นาย' | 'นาง' | 'นา
|
||||||
await page.getByRole('option', { name: value }).click();
|
await page.getByRole('option', { name: value }).click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueUser() {
|
function uniqueUser() {
|
||||||
const n = Date.now().toString().slice(-6);
|
const n = Date.now().toString().slice(-6);
|
||||||
const rand8 = Math.floor(Math.random() * 1e8).toString().padStart(8, '0');
|
const rand8 = Math.floor(Math.random() * 1e8).toString().padStart(8, '0');
|
||||||
|
|
@ -90,10 +65,12 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
test('Success Login แล้วเข้า /dashboard ได้', async ({ page }) => {
|
test('Success Login แล้วเข้า /dashboard ได้', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await loginEmailLocator(page).fill(LOGIN_EMAIL);
|
|
||||||
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
|
await emailLocator(page).fill(TEST_EMAIL);
|
||||||
|
await passwordLocator(page).fill(TEST_PASSWORD);
|
||||||
await loginButtonLocator(page).click();
|
await loginButtonLocator(page).click();
|
||||||
await page.waitForURL('**/dashboard', { timeout: 25_000 });
|
|
||||||
|
await page.waitForURL('**/dashboard', { timeout: TIMEOUT.LOGIN });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const dashboardEvidence = [
|
const dashboardEvidence = [
|
||||||
|
|
@ -102,38 +79,45 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
page.locator('img[src*="avataaars"]').first(),
|
page.locator('img[src*="avataaars"]').first(),
|
||||||
page.locator('img[alt],[alt="User Avatar"]').first()
|
page.locator('img[alt],[alt="User Avatar"]').first()
|
||||||
];
|
];
|
||||||
await expectAnyVisible(page, dashboardEvidence, 20_000);
|
await expectAnyVisible(page, dashboardEvidence, TIMEOUT.PAGE_LOAD);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Invalid Email - Thai characters (พิมพ์ภาษาไทย)', async ({ page }) => {
|
test('Invalid Email - Thai characters (พิมพ์ภาษาไทย)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await loginEmailLocator(page).fill('ทดสอบภาษาไทย');
|
|
||||||
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
|
await emailLocator(page).fill('ทดสอบภาษาไทย');
|
||||||
const errorHint = page.getByText('ห้ามใส่ภาษาไทย');
|
await passwordLocator(page).fill(TEST_PASSWORD);
|
||||||
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
|
|
||||||
|
await expect(page.getByText('ห้ามใส่ภาษาไทย').first()).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Invalid Email Format (อีเมลผิดรูปแบบ)', async ({ page }) => {
|
test('Invalid Email Format (อีเมลผิดรูปแบบ)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await loginEmailLocator(page).fill('test@domain');
|
|
||||||
await loginPasswordLocator(page).fill(LOGIN_PASSWORD);
|
await emailLocator(page).fill('test@domain');
|
||||||
|
await passwordLocator(page).fill(TEST_PASSWORD);
|
||||||
await loginButtonLocator(page).click();
|
await loginButtonLocator(page).click();
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
const errorHint = page.getByText('กรุณากรอกอีเมลให้ถูกต้อง (you@example.com)');
|
|
||||||
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
|
await expect(
|
||||||
|
page.getByText('กรุณากรอกอีเมลให้ถูกต้อง (you@example.com)').first()
|
||||||
|
).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Wrong Password (รหัสผ่านผิด หรืออีเมลไม่ถูกต้องในระบบ)', async ({ page }) => {
|
test('Wrong Password (รหัสผ่านผิด หรืออีเมลไม่ถูกต้องในระบบ)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await loginEmailLocator(page).fill(LOGIN_EMAIL);
|
|
||||||
await loginPasswordLocator(page).fill('wrong-password-123');
|
await emailLocator(page).fill(TEST_EMAIL);
|
||||||
|
await passwordLocator(page).fill('wrong-password-123');
|
||||||
await loginButtonLocator(page).click();
|
await loginButtonLocator(page).click();
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
const errorHint = page.getByText('กรุณาเช็ค Email หรือ รหัสผ่านใหม่อีกครั้ง');
|
|
||||||
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
|
await expect(
|
||||||
|
page.getByText('กรุณาเช็ค Email หรือ รหัสผ่านใหม่อีกครั้ง').first()
|
||||||
|
).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -142,21 +126,22 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
test('หน้า Register ต้องโหลดได้ (Smoke)', async ({ page }) => {
|
test('หน้า Register ต้องโหลดได้ (Smoke)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await expect(regHeading(page)).toBeVisible({ timeout: 15_000 });
|
await expect(regHeading(page)).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
await expect(regSubmit(page)).toBeVisible({ timeout: 15_000 });
|
await expect(regSubmit(page)).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ลิงก์ "เข้าสู่ระบบ" ต้องกดแล้วไปหน้า login ได้', async ({ page }) => {
|
test('ลิงก์ "เข้าสู่ระบบ" ต้องกดแล้วไปหน้า login ได้', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await regLoginLink(page).click();
|
await regLoginLink(page).click();
|
||||||
await page.waitForURL('**/auth/login', { timeout: 15_000 });
|
await page.waitForURL('**/auth/login', { timeout: TIMEOUT.PAGE_LOAD });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('สมัครสมาชิกสำเร็จ (Happy Path) → redirect ไป /auth/login', async ({ page }) => {
|
test('สมัครสมาชิกสำเร็จ (Happy Path) → redirect ไป /auth/login', async ({ page }) => {
|
||||||
const u = uniqueUser();
|
const u = uniqueUser();
|
||||||
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
|
|
||||||
await regUsername(page).fill(u.username);
|
await regUsername(page).fill(u.username);
|
||||||
await regEmail(page).fill(u.email);
|
await regEmail(page).fill(u.email);
|
||||||
await pickPrefix(page, 'นาย');
|
await pickPrefix(page, 'นาย');
|
||||||
|
|
@ -168,15 +153,18 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
await regSubmit(page).click();
|
await regSubmit(page).click();
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const navToLogin = page.waitForURL('**/auth/login', { timeout: 25_000, waitUntil: 'domcontentloaded' }).then(() => 'login' as const).catch(() => null);
|
// รอ 3 สัญญาณ: redirect ไป login / success toast / error
|
||||||
const successToast = page.getByText(/สมัครสมาชิกสำเร็จ|success/i, { exact: false }).first().waitFor({ state: 'visible', timeout: 25_000 }).then(() => 'success' as const).catch(() => null);
|
const navToLogin = page.waitForURL('**/auth/login', { timeout: TIMEOUT.LOGIN, waitUntil: 'domcontentloaded' }).then(() => 'login' as const).catch(() => null);
|
||||||
const anyError = regErrorBox(page).first().waitFor({ state: 'visible', timeout: 25_000 }).then(() => 'error' 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]);
|
const result = await Promise.race([navToLogin, successToast, anyError]);
|
||||||
if (result === 'error') {
|
if (result === 'error') {
|
||||||
throw new Error('Register errors visible');
|
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')) {
|
if (!page.url().includes('/auth/login')) {
|
||||||
const hasSuccess = await page.getByText(/สมัครสมาชิกสำเร็จ/i, { exact: false }).first().isVisible().catch(() => false);
|
const hasSuccess = await page.getByText(/สมัครสมาชิกสำเร็จ/i, { exact: false }).first().isVisible().catch(() => false);
|
||||||
if (hasSuccess) {
|
if (hasSuccess) {
|
||||||
|
|
@ -185,24 +173,28 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/auth\/login/i, { timeout: 15_000 });
|
await expect(page).toHaveURL(/\/auth\/login/i, { timeout: TIMEOUT.PAGE_LOAD });
|
||||||
await expect(page.getByRole('heading', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
|
await expect(page.getByRole('heading', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
await expect(page.getByRole('button', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: 15_000 });
|
await expect(page.getByRole('button', { name: 'เข้าสู่ระบบ' })).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Invalid Email - ใส่ภาษาไทย ต้องขึ้น error', async ({ page }) => {
|
test('Invalid Email - ใส่ภาษาไทย ต้องขึ้น error', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
await regEmail(page).fill('ทดสอบภาษาไทย');
|
await regEmail(page).fill('ทดสอบภาษาไทย');
|
||||||
await regUsername(page).click();
|
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: 12_000 });
|
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 }) => {
|
test('Password ไม่ตรงกัน ต้องขึ้น error (ต้องกดสร้างบัญชีก่อน)', async ({ page }) => {
|
||||||
const u = uniqueUser();
|
const u = uniqueUser();
|
||||||
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE_URL}/auth/register`, { waitUntil: 'domcontentloaded' });
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
|
|
||||||
await regUsername(page).fill(u.username);
|
await regUsername(page).fill(u.username);
|
||||||
await regEmail(page).fill(u.email);
|
await regEmail(page).fill(u.email);
|
||||||
await pickPrefix(page, 'นาย');
|
await pickPrefix(page, 'นาย');
|
||||||
|
|
@ -210,11 +202,14 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
await regLastName(page).fill(u.lastName);
|
await regLastName(page).fill(u.lastName);
|
||||||
await regPhone(page).fill(u.phone);
|
await regPhone(page).fill(u.phone);
|
||||||
await regPassword(page).fill('Admin12345!');
|
await regPassword(page).fill('Admin12345!');
|
||||||
await regConfirmPassword(page).fill('Admin12345?');
|
await regConfirmPassword(page).fill('Admin12345?'); // mismatch
|
||||||
await regSubmit(page).click();
|
await regSubmit(page).click();
|
||||||
await waitAppSettled(page);
|
await waitAppSettled(page);
|
||||||
const mismatchErr = page.getByText(/รหัสผ่านไม่ตรงกัน/i, { exact: false }).or(regErrorBox(page).filter({ hasText: /รหัสผ่านไม่ตรงกัน/i }));
|
|
||||||
await expect(mismatchErr.first()).toBeVisible({ timeout: 12_000 });
|
const mismatchErr = page
|
||||||
|
.getByText(/รหัสผ่านไม่ตรงกัน/i, { exact: false })
|
||||||
|
.or(regErrorBox(page).filter({ hasText: /รหัสผ่านไม่ตรงกัน/i }));
|
||||||
|
await expect(mismatchErr.first()).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -235,13 +230,12 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
test('Validation: ใส่อีเมลภาษาไทยแล้วขึ้น Error', async ({ page }) => {
|
test('Validation: ใส่อีเมลภาษาไทยแล้วขึ้น Error', async ({ page }) => {
|
||||||
await forgotEmail(page).fill('ฟฟฟฟ');
|
await forgotEmail(page).fill('ฟฟฟฟ');
|
||||||
await page.getByRole('heading', { name: /ลืมรหัสผ่าน/i }).click();
|
await page.getByRole('heading', { name: /ลืมรหัสผ่าน/i }).click();
|
||||||
const err = page.getByText(/ห้ามใส่ภาษาไทย/i).first();
|
await expect(page.getByText(/ห้ามใส่ภาษาไทย/i).first()).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
await expect(err).toBeVisible({ timeout: 10_000 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('กดลิงก์กลับไปหน้า Login ได้', async ({ page }) => {
|
test('กดลิงก์กลับไปหน้า Login ได้', async ({ page }) => {
|
||||||
await forgotBackLink(page).click();
|
await forgotBackLink(page).click();
|
||||||
await page.waitForURL('**/auth/login', { timeout: 10_000 });
|
await page.waitForURL('**/auth/login', { timeout: TIMEOUT.ELEMENT });
|
||||||
await expect(page).toHaveURL(/\/auth\/login/i);
|
await expect(page).toHaveURL(/\/auth\/login/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -257,9 +251,10 @@ test.describe('ระบบยืนยันตัวตน (Authentication)',
|
||||||
}
|
}
|
||||||
await route.continue();
|
await route.continue();
|
||||||
});
|
});
|
||||||
|
|
||||||
await forgotEmail(page).fill('test@gmail.com');
|
await forgotEmail(page).fill('test@gmail.com');
|
||||||
await forgotSubmit(page).click();
|
await forgotSubmit(page).click();
|
||||||
await expect(page.getByText(/ส่งลิงก์เรียบร้อยแล้ว/i, { exact: false })).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByText(/ส่งลิงก์เรียบร้อยแล้ว/i, { exact: false })).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
await expect(page.getByText(/กรุณาตรวจสอบกล่องจดหมาย/i, { exact: false })).toBeVisible();
|
await expect(page.getByText(/กรุณาตรวจสอบกล่องจดหมาย/i, { exact: false })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,77 @@
|
||||||
|
/**
|
||||||
|
* @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 { 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 route.fulfill({
|
||||||
await page.waitForLoadState('domcontentloaded');
|
status: 200,
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
contentType: 'application/json',
|
||||||
await page.waitForTimeout(200);
|
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) {
|
// Tests
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
// ==========================================
|
||||||
await waitAppSettled(page);
|
test.describe('ระบบห้องเรียนออนไลน์และแบบทดสอบ (Classroom & Quiz)', () => {
|
||||||
|
|
||||||
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)', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await setupLogin(page);
|
await setupLogin(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Section 1: ห้องเรียน (Classroom & Learning)
|
||||||
|
// --------------------------------------------------
|
||||||
|
test.describe('ห้องเรียน (Classroom Layout & Access)', () => {
|
||||||
|
|
||||||
test('6.1 เข้าห้องเรียนหลัก (Classroom Basic Layout)', async ({ page }) => {
|
test('6.1 เข้าห้องเรียนหลัก (Classroom Basic Layout)', async ({ page }) => {
|
||||||
// สมมติว่ามี Course ID: 1 ทดสอบแบบเปิดหน้าตรงๆ
|
|
||||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||||
|
|
||||||
// 1. โครงร่างของหน้า (Top Bar) ควรมีปุ่มกลับ กับไอคอนแผงด้านข้าง
|
// 1. โครงร่างของหน้า — ปุ่มกลับ + ไอคอนแผงด้านข้าง
|
||||||
const backBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'arrow_back' }) }).first();
|
const backBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'arrow_back' }) }).first();
|
||||||
await expect(backBtn).toBeVisible({ timeout: 15_000 });
|
await expect(backBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
|
|
||||||
const menuCurriculumBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'menu_open' }) }).first();
|
const menuCurriculumBtn = page.getByRole('button').filter({ has: page.locator('i.q-icon', { hasText: 'menu_open' }) }).first();
|
||||||
await expect(menuCurriculumBtn).toBeVisible({ timeout: 15_000 });
|
await expect(menuCurriculumBtn).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
|
|
||||||
// 2. เช็คว่ามีพื้นที่ Sidebar หลักสูตร (CurriculumSidebar Component) โผล่ขึ้นมาหรือมีอยู่ใน DOM
|
// 2. Sidebar หลักสูตร
|
||||||
const sidebar = page.locator('.q-drawer').first();
|
const sidebar = page.locator('.q-drawer').first();
|
||||||
if (!await sidebar.isVisible()) {
|
if (!await sidebar.isVisible()) {
|
||||||
await menuCurriculumBtn.click();
|
await menuCurriculumBtn.click();
|
||||||
|
|
@ -47,49 +80,96 @@ test.describe('ระบบห้องเรียนออนไลน์ (Cla
|
||||||
});
|
});
|
||||||
|
|
||||||
test('6.2 เช็คสถานะการเข้าถึงเนื้อหา (Access Control)', async ({ page }) => {
|
test('6.2 เช็คสถานะการเข้าถึงเนื้อหา (Access Control)', async ({ page }) => {
|
||||||
// ลองสุ่ม Course ID สูงๆ ที่อาจจะไม่อนุญาตให้เรียน (ไม่มีสิทธิ์) ควรรองรับกล่องแจ้งเตือนด้วย Alert ของระบบ
|
|
||||||
// ใน learning.vue จะมีการสั่ง `alert(msg)` แต่อาจจะต้องพึ่งกลไก Intercepter
|
|
||||||
|
|
||||||
page.on('dialog', async dialog => {
|
page.on('dialog', async dialog => {
|
||||||
// หน้าต่าง Alert ถ้ามีสิทธิ์ไม่อนุญาตมันจะเด้งอันนี้
|
|
||||||
expect(dialog.message()).toBeTruthy();
|
expect(dialog.message()).toBeTruthy();
|
||||||
await dialog.accept();
|
await dialog.accept();
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=99999`);
|
await page.goto(`${BASE_URL}/classroom/learning?course_id=99999`);
|
||||||
|
|
||||||
// รอดู Loading หายไป
|
|
||||||
const loadingMask = page.locator('.animate-pulse, .q-spinner');
|
const loadingMask = page.locator('.animate-pulse, .q-spinner');
|
||||||
await loadingMask.first().waitFor({ state: 'hidden', timeout: 20_000 }).catch(() => {});
|
await loadingMask.first().waitFor({ state: 'hidden', timeout: TIMEOUT.PAGE_LOAD }).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('6.3 การแสดงผลช่องวิดีโอ (Video Player) หรือ พื้นที่ทำข้อสอบ (Quiz)', async ({ page }) => {
|
test('6.3 การแสดงผลช่องวิดีโอ หรือ พื้นที่ทำข้อสอบ (Video / Quiz)', async ({ page }) => {
|
||||||
// เข้าหน้าห้องเรียน Course id: 1
|
|
||||||
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
await page.goto(`${BASE_URL}/classroom/learning?course_id=1`);
|
||||||
|
|
||||||
// กรณีที่ 1: อาจแสดง Video ถ้าเป็นบทเรียนวิดีโอ
|
|
||||||
const videoLocator = page.locator('video').first();
|
const videoLocator = page.locator('video').first();
|
||||||
|
|
||||||
// กรณีที่ 2: ถ้าบทแรกเป็น Quiz จะแสดงไอคอนแบบทดสอบ
|
|
||||||
const quizLocator = page.getByText(/เริ่มทำแบบทดสอบ|แบบทดสอบ/i).first();
|
const quizLocator = page.getByText(/เริ่มทำแบบทดสอบ|แบบทดสอบ/i).first();
|
||||||
|
|
||||||
// กรณีที่ 3: ไม่มีบทเรียนเนื้อหาใดๆ เลยให้แสดง
|
|
||||||
const errorLocator = page.getByText(/ไม่สามารถเข้าถึง/i).first();
|
const errorLocator = page.getByText(/ไม่สามารถเข้าถึง/i).first();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
videoLocator.waitFor({ state: 'visible', timeout: 20_000 }),
|
videoLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD }),
|
||||||
quizLocator.waitFor({ state: 'visible', timeout: 20_000 }),
|
quizLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD }),
|
||||||
errorLocator.waitFor({ state: 'visible', timeout: 20_000 })
|
errorLocator.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isOkay = (await videoLocator.isVisible()) || (await quizLocator.isVisible()) || (await errorLocator.isVisible());
|
const isOkay = (await videoLocator.isVisible()) || (await quizLocator.isVisible()) || (await errorLocator.isVisible());
|
||||||
expect(isOkay).toBeTruthy();
|
expect(isOkay).toBeTruthy();
|
||||||
} catch {
|
} catch {
|
||||||
// ถ้าไม่มีเลยใน 20 วิ ถือว่าหน้าอาจจะล้มเหลว หรือเป็น Content เปล่า
|
|
||||||
// ให้ลอง Capture เพื่อเก็บข้อมูลไปใช้งาน
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/classroom-blank-state.png', fullPage: true });
|
await page.screenshot({ path: 'tests/e2e/screenshots/classroom-blank-state.png', fullPage: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Section 2: แบบทดสอบ (Quiz System)
|
||||||
|
// --------------------------------------------------
|
||||||
|
test.describe('แบบทดสอบ (Quiz System)', () => {
|
||||||
|
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @file discovery.spec.ts
|
||||||
|
* @description ทดสอบหมวดหน้าค้นหาคอร์สและผลลัพธ์ (Discovery & Browse)
|
||||||
|
*/
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { BASE_URL, TIMEOUT, waitAppSettled } from './helpers';
|
||||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
|
||||||
|
|
||||||
test.describe('หมวดหน้าค้นหาคอร์สและผลลัพธ์ (Discovery & Browse)', () => {
|
test.describe('หมวดหน้าค้นหาคอร์สและผลลัพธ์ (Discovery & Browse)', () => {
|
||||||
|
|
||||||
test.describe('ส่วนหน้าแรก (Home)', () => {
|
test.describe('ส่วนหน้าแรก (Home)', () => {
|
||||||
test('โหลดหน้าแรก และตรวจสอบแสดงผลครบถ้วน (Hero, Cards, Categories)', async ({ page }) => {
|
test('โหลดหน้าแรก และตรวจสอบแสดงผลครบถ้วน (Hero, Cards, Categories)', async ({ page }) => {
|
||||||
await page.goto(BASE_URL);
|
await page.goto(BASE_URL);
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const heroTitle = page.locator('h1, h2, .hero-title').first();
|
const heroTitle = page.locator('h1, h2, .hero-title').first();
|
||||||
await expect(heroTitle).toBeVisible({ timeout: 15_000 });
|
await expect(heroTitle).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
|
|
||||||
const ctaButton = page.locator('a[href="/browse"]').first();
|
const ctaButton = page.locator('a[href="/browse"]').first();
|
||||||
if (await ctaButton.isVisible()) {
|
if (await ctaButton.isVisible()) {
|
||||||
|
|
@ -16,55 +21,63 @@ test.describe('หมวดหน้าค้นหาคอร์สและ
|
||||||
}
|
}
|
||||||
|
|
||||||
const courseSectionHeading = page.getByText(/คอร์สออนไลน์|Online Courses/i).first();
|
const courseSectionHeading = page.getByText(/คอร์สออนไลน์|Online Courses/i).first();
|
||||||
await expect(courseSectionHeading).toBeVisible({ timeout: 10_000 });
|
await expect(courseSectionHeading).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
const allCategoryBtn = page.getByRole('button', { name: /ทั้งหมด|All/i }).first();
|
const allCategoryBtn = page.getByRole('button', { name: /ทั้งหมด|All/i }).first();
|
||||||
await expect(allCategoryBtn).toBeVisible();
|
await expect(allCategoryBtn).toBeVisible();
|
||||||
|
|
||||||
const courseCards = page.locator('div.cursor-pointer').filter({ has: page.locator('img') });
|
const courseCards = page.locator('div.cursor-pointer').filter({ has: page.locator('img') });
|
||||||
await expect(courseCards.first()).toBeVisible({ timeout: 15_000 });
|
await expect(courseCards.first()).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
expect(await courseCards.count()).toBeGreaterThan(0);
|
expect(await courseCards.count()).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/discovery-home.png', fullPage: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('ส่วนค้นหาและแคตตาล็อก (Browse)', () => {
|
test.describe('ส่วนค้นหาและแคตตาล็อก (Browse)', () => {
|
||||||
test('ค้นหาหลักสูตร (Search Course)', async ({ page }) => {
|
test('ค้นหาหลักสูตร (Search Course)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/browse`);
|
await page.goto(`${BASE_URL}/browse`);
|
||||||
const searchInput = page.locator('input[placeholder="ค้นหาคอร์ส..."]').first();
|
await waitAppSettled(page);
|
||||||
await searchInput.fill('การเขียนโปรแกรม');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
|
|
||||||
// ในหน้า browse จะใช้ <NuxtLink> ซึ่ง render เป็น tag <a>
|
const searchInput = page.locator('input[placeholder="ค้นหาคอร์ส..."]').first();
|
||||||
|
await expect(searchInput).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
await searchInput.fill('Python');
|
||||||
|
await searchInput.press('Enter');
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
|
// ต้องเจออย่างใดอย่างหนึ่ง: ผลลัพธ์คอร์ส หรือ empty state
|
||||||
const searchResults = page.locator('a[href*="/course/"]').filter({ has: page.locator('img') }).first();
|
const searchResults = page.locator('a[href*="/course/"]').filter({ has: page.locator('img') }).first();
|
||||||
await expect(searchResults).toBeVisible({ timeout: 15_000 });
|
const emptyState = page.getByText(/ไม่พบ|ไม่เจอ|No result|not found/i).first()
|
||||||
|
.or(page.locator('i.q-icon').filter({ hasText: 'search_off' }).first());
|
||||||
|
|
||||||
|
await expect(searchResults.or(emptyState)).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/discovery-search.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ตัวกรองหมวดหมู่คอร์ส (Category Filter)', async ({ page }) => {
|
test('ตัวกรองหมวดหมู่คอร์ส (Category Filter)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/browse`);
|
await page.goto(`${BASE_URL}/browse`);
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const categoryButton = page.locator('button').filter({ hasText: 'การออกแบบ' }).first();
|
const categoryButton = page.locator('button').filter({ hasText: 'การออกแบบ' }).first();
|
||||||
|
|
||||||
if (await categoryButton.isVisible()) {
|
if (await categoryButton.isVisible()) {
|
||||||
await categoryButton.click();
|
await categoryButton.click();
|
||||||
// ในหน้า browse จะใช้ <NuxtLink> ซึ่ง render เป็น tag <a>
|
|
||||||
const courseCard = page.locator('a[href*="/course/"]').filter({ has: page.locator('img') }).first();
|
const courseCard = page.locator('a[href*="/course/"]').filter({ has: page.locator('img') }).first();
|
||||||
await expect(courseCard).toBeVisible({ timeout: 15_000 });
|
await expect(courseCard).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/discovery-filter.png', fullPage: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('หน้ารายละเอียดคอร์ส (Course Detail)', () => {
|
test.describe('หน้ารายละเอียดคอร์ส (Course Detail)', () => {
|
||||||
test('โหลดเนื้อหาวิชา (Curriculum) แถบรายละเอียดขึ้นปกติ', async ({ page }) => {
|
test('โหลดเนื้อหาวิชา (Curriculum) แถบรายละเอียดขึ้นปกติ', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}`);
|
await page.goto(`${BASE_URL}/course/1`);
|
||||||
const courseCard = page.locator('div.cursor-pointer').filter({ has: page.locator('img') }).first();
|
await waitAppSettled(page);
|
||||||
await expect(courseCard).toBeVisible({ timeout: 10_000 });
|
|
||||||
// Get URL from navigating when clicking the div or finding another link. Since it's a div, we cannot easily get href.
|
|
||||||
// So let's click it or fallback to /course/1
|
|
||||||
const targetUrl = '/course/1';
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}${targetUrl}`);
|
|
||||||
|
|
||||||
const courseTitle = page.locator('h1').first();
|
const courseTitle = page.locator('h1').first();
|
||||||
await expect(courseTitle).toBeVisible({ timeout: 15_000 });
|
await expect(courseTitle).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||||
|
|
||||||
const curriculumTab = page.getByRole('tab', { name: /เนื้อหาวิชา|ส่วนหลักสูตร|Curriculum/i }).first();
|
const curriculumTab = page.getByRole('tab', { name: /เนื้อหาวิชา|ส่วนหลักสูตร|Curriculum/i }).first();
|
||||||
if (await curriculumTab.isVisible()) {
|
if (await curriculumTab.isVisible()) {
|
||||||
|
|
@ -73,19 +86,18 @@ test.describe('หมวดหน้าค้นหาคอร์สและ
|
||||||
|
|
||||||
const lessonItems = page.locator('.q-expansion-item, .lesson-item, [role="listitem"]');
|
const lessonItems = page.locator('.q-expansion-item, .lesson-item, [role="listitem"]');
|
||||||
await expect(lessonItems.first()).toBeVisible().catch(() => {});
|
await expect(lessonItems.first()).toBeVisible().catch(() => {});
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/discovery-curriculum.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('การแสดงผลปุ่ม เข้าเรียน/ลงทะเบียน (Enroll / Start Learning)', async ({ page }) => {
|
test('การแสดงผลปุ่ม เข้าเรียน/ลงทะเบียน (Enroll / Start Learning)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}`);
|
await page.goto(`${BASE_URL}/course/1`);
|
||||||
const courseCard = page.locator('div.cursor-pointer').filter({ has: page.locator('img') }).first();
|
await waitAppSettled(page);
|
||||||
await expect(courseCard).toBeVisible({ timeout: 10_000 });
|
|
||||||
const targetUrl = '/course/1';
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}${targetUrl}`);
|
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
|
||||||
|
|
||||||
const enrollStartBtn = page.locator('button').filter({ hasText: /(เข้าสู่ระบบ|ลงทะเบียน|เข้าเรียน|เรียนต่อ|ซื้อ|ฟรี|Enroll|Start|Buy|Login)/i }).first();
|
const enrollStartBtn = page.locator('button').filter({ hasText: /(เข้าสู่ระบบ|ลงทะเบียน|เข้าเรียน|เรียนต่อ|ซื้อ|ฟรี|Enroll|Start|Buy|Login)/i }).first();
|
||||||
await expect(enrollStartBtn).toBeVisible({ timeout: 10_000 });
|
await expect(enrollStartBtn).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/discovery-enroll-btn.png', fullPage: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import { test, expect, type Page } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
|
||||||
|
|
||||||
// ✅ หน้าจริงคือ /auth/forgot-password (อ้างอิงจากรูป)
|
|
||||||
const FORGOT_URL = `${BASE_URL}/auth/forgot-password`;
|
|
||||||
|
|
||||||
async function waitAppSettled(page: Page) {
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emailInput(page: Page) {
|
|
||||||
// เผื่อบางที input ไม่ได้ type=email แต่เป็น textbox ธรรมดา
|
|
||||||
return page.locator('input[type="email"]').or(page.getByRole('textbox')).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitBtn(page: Page) {
|
|
||||||
// ปุ่มในรูปเป็น “ส่งลิงก์รีเซ็ต”
|
|
||||||
return page.getByRole('button', { name: /ส่งลิงก์รีเซ็ต/i }).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
function backToLoginLink(page: Page) {
|
|
||||||
// ในรูปเป็นลิงก์ “กลับไปหน้าเข้าสู่ระบบ”
|
|
||||||
return page.getByRole('link', { name: /กลับไปหน้าเข้าสู่ระบบ/i }).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('หน้าลืมรหัสผ่าน (Forgot Password)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto(FORGOT_URL, { waitUntil: 'domcontentloaded' });
|
|
||||||
await waitAppSettled(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('3.1 โหลดหน้าลืมรหัสผ่านได้ครบถ้วน (Smoke Test)', async ({ page }) => {
|
|
||||||
await expect(page.getByRole('heading', { name: /ลืมรหัสผ่าน/i })).toBeVisible();
|
|
||||||
await expect(emailInput(page)).toBeVisible();
|
|
||||||
await expect(submitBtn(page)).toBeVisible();
|
|
||||||
await expect(backToLoginLink(page)).toBeVisible();
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/forgot-01-smoke.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('3.2 Validation: ใส่อีเมลภาษาไทยแล้วขึ้น Error', async ({ page }) => {
|
|
||||||
await emailInput(page).fill('ฟฟฟฟ');
|
|
||||||
|
|
||||||
// trigger blur
|
|
||||||
await page.getByRole('heading', { name: /ลืมรหัสผ่าน/i }).click();
|
|
||||||
|
|
||||||
// ข้อความจริงในระบบ “ห้ามใส่ภาษาไทย”
|
|
||||||
const err = page.getByText(/ห้ามใส่ภาษาไทย/i).first();
|
|
||||||
await expect(err).toBeVisible({ timeout: 10_000 });
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/forgot-02-thai-email.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('3.3 กดลิงก์กลับไปหน้า Login ได้', async ({ page }) => {
|
|
||||||
await backToLoginLink(page).click();
|
|
||||||
await page.waitForURL('**/auth/login', { timeout: 10_000 });
|
|
||||||
await expect(page).toHaveURL(/\/auth\/login/i);
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/forgot-03-back-login.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('3.4 ทดลองส่งลิงก์รีเซ็ตรหัสผ่าน (API Mock)', async ({ page }) => {
|
|
||||||
// ✅ ดัก request แบบกว้างขึ้น: POST ที่ URL มี forgot/reset
|
|
||||||
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) &&
|
|
||||||
// กันไม่ให้ไป intercept asset
|
|
||||||
!/\.(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 emailInput(page).fill('test@gmail.com');
|
|
||||||
await submitBtn(page).click();
|
|
||||||
|
|
||||||
// ✅ ตรวจหน้าสำเร็จตามที่คุณคาดหวัง
|
|
||||||
await expect(page.getByText(/ส่งลิงก์เรียบร้อยแล้ว/i, { exact: false })).toBeVisible({ timeout: 10_000 });
|
|
||||||
await expect(page.getByText(/กรุณาตรวจสอบกล่องจดหมาย/i, { exact: false })).toBeVisible();
|
|
||||||
|
|
||||||
// ปุ่ม “ส่งอีกครั้ง” (ถ้ามี)
|
|
||||||
await expect(page.getByRole('button', { name: /ส่งอีกครั้ง/i })).toBeVisible({ timeout: 10_000 }).catch(() => {});
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/forgot-04-mock-success.png', fullPage: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
129
Frontend-Learner/tests/e2e/helpers.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* @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<string, number> = {
|
||||||
|
/** รอหน้าโหลด */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import { test, expect, type Page, type Locator } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
|
||||||
|
|
||||||
// ใช้ account ตามที่คุณให้มา
|
|
||||||
const EMAIL = 'studentedtest@example.com';
|
|
||||||
const PASSWORD = 'admin123';
|
|
||||||
|
|
||||||
async function waitAppSettled(page: Page) {
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emailLocator(page: Page): Locator {
|
|
||||||
return page
|
|
||||||
.locator('input[type="email"]')
|
|
||||||
.or(page.getByRole('textbox', { name: /อีเมล|email/i }))
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
|
|
||||||
function passwordLocator(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 dashboard locators became visible.');
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Login -> Dashboard', () => {
|
|
||||||
test('Success Login แล้วเข้า /dashboard ได้', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
|
||||||
await waitAppSettled(page);
|
|
||||||
|
|
||||||
await emailLocator(page).fill(EMAIL);
|
|
||||||
await passwordLocator(page).fill(PASSWORD);
|
|
||||||
await loginButtonLocator(page).click();
|
|
||||||
|
|
||||||
await page.waitForURL('**/dashboard', { timeout: 25_000 });
|
|
||||||
await waitAppSettled(page);
|
|
||||||
|
|
||||||
// ✅ ใช้ Locator ที่พบเจอแน่นอนใน Layout/Page โดยไม่ยึดติดกับภาษาปัจจุบัน (I18n)
|
|
||||||
const dashboardEvidence = [
|
|
||||||
// มองหา Layout container ฝั่ง Dashboard
|
|
||||||
page.locator('.q-page-container').first(),
|
|
||||||
page.locator('.q-drawer').first(),
|
|
||||||
// มองหารูปโปรไฟล์ (UserAvatar)
|
|
||||||
page.locator('img[src*="avataaars"]').first(),
|
|
||||||
page.locator('img[alt],[alt="User Avatar"]').first()
|
|
||||||
];
|
|
||||||
|
|
||||||
await expectAnyVisible(page, dashboardEvidence, 20_000);
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/login-to-dashboard.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
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(PASSWORD);
|
|
||||||
|
|
||||||
const errorHint = page.getByText('ห้ามใส่ภาษาไทย');
|
|
||||||
|
|
||||||
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/login-thai-email.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Invalid Email Format (อีเมลผิดรูปแบบ)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
|
||||||
await waitAppSettled(page);
|
|
||||||
|
|
||||||
// *สำคัญ*: HTML5 จะดักจับ invalid-email-format ตั้งแต่กด Submit (native validation)
|
|
||||||
// ทำให้ Vue Form ไม่เริ่มทำงาน
|
|
||||||
// ดังนั้นเพื่อให้ทดสอบเจอ Error จาก useFormValidation จริงๆ เราใช้ 'test@domain'
|
|
||||||
// ซึ่ง HTML5 <input type="email"> ปล่อยผ่าน แต่ /regex/ ของระบบตรวจเจอว่าไม่มี .com
|
|
||||||
await emailLocator(page).fill('test@domain');
|
|
||||||
await passwordLocator(page).fill(PASSWORD);
|
|
||||||
await loginButtonLocator(page).click();
|
|
||||||
await waitAppSettled(page);
|
|
||||||
|
|
||||||
const errorHint = page.getByText('กรุณากรอกอีเมลให้ถูกต้อง (you@example.com)');
|
|
||||||
|
|
||||||
await expect(errorHint.first()).toBeVisible({ timeout: 12_000 });
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/login-invalid-email.png', fullPage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Wrong Password (รหัสผ่านผิด หรืออีเมลไม่ถูกต้องในระบบ)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/auth/login`, { waitUntil: 'domcontentloaded' });
|
|
||||||
await waitAppSettled(page);
|
|
||||||
|
|
||||||
await emailLocator(page).fill(EMAIL);
|
|
||||||
await passwordLocator(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 });
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/login-wrong-password.png', fullPage: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
|
||||||
|
|
||||||
async function waitAppSettled(page: any) {
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ฟังก์ชันจำลองล็อกอิน (เพราะทำข้อสอบต้องล็อกอินเสมอ)
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ฟังก์ชัน Mock ข้อมูลข้อสอบให้ Playwright ไม่ต้องไปดึงจากฐานข้อมูลจริงๆ (เพื่อป้องกันปัญหาคอร์ส/บทเรียนไม่มีอยู่จริง)
|
|
||||||
async function mockQuizData(page: any) {
|
|
||||||
await page.route('**/lessons/*', async (route: any) => {
|
|
||||||
// สมมติข้อมูลข้อสอบจำลองให้มี 15 ข้อเพื่อเทส Pagination ได้
|
|
||||||
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' } }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
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: {}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('ระบบทำแบบทดสอบ (Quiz System)', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// ต้อง Login ก่อนเรียน!
|
|
||||||
await setupLogin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('โหลดหน้า Quiz และคลิกระบบเริ่มทำข้อสอบได้ (Start Screen)', async ({ page }) => {
|
|
||||||
await mockQuizData(page);
|
|
||||||
|
|
||||||
// สมมติเอาที่ quiz ใน course 2 lesson 17 (ซึ่ง API เสาะหาจะถูกดักจับและ Mock ไว้ด้านบนแล้ว)
|
|
||||||
await page.goto(`${BASE_URL}/classroom/quiz?course_id=2&lesson_id=17`);
|
|
||||||
|
|
||||||
// หน้าจอ Start Screen ต้องขึ้นมา
|
|
||||||
const startBtn = page.getByRole('button', { name: /เริ่มทำแบบทดสอบ|Start/i }).first();
|
|
||||||
await expect(startBtn).toBeVisible({ timeout: 15_000 });
|
|
||||||
|
|
||||||
// ลองกดเริ่มทำ
|
|
||||||
await startBtn.click();
|
|
||||||
|
|
||||||
// เช็คว่าหน้า Taking (พื้นที่ทำข้อสอบข้อที่ 1) โผล่มา
|
|
||||||
const questionText = page.locator('h3').first(); // ชื่อคำถาม
|
|
||||||
await expect(questionText).toBeVisible({ timeout: 10_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ทดสอบระบบแถบข้อสอบ แบ่งหน้า (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: 15_000 });
|
|
||||||
await startBtn.click();
|
|
||||||
|
|
||||||
// เช็คว่ามีลูกศรเลื่อนหน้าข้อสอบ (Paginations) สำหรับแบบทดสอบเกิน 10 ข้อที่สร้างมาใหม่
|
|
||||||
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('การแสดงผลปุ่มถัดไป/ส่งคำตอบ (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: 15_000 });
|
|
||||||
await startBtn.click();
|
|
||||||
|
|
||||||
// รอให้หน้าโหลดคำถามเสร็จก่อน ค่อยหาปุ่ม
|
|
||||||
await expect(page.locator('h3').first()).toBeVisible({ timeout: 10_000 });
|
|
||||||
|
|
||||||
const submitBtn = page.locator('button').filter({ hasText: /(ส่งคำตอบ|Submit)/i }).first();
|
|
||||||
const nextBtn = page.locator('button').filter({ hasText: /(ถัดไป|Next)/i }).first();
|
|
||||||
|
|
||||||
// แบบทดสอบข้อแรก ต้องมีปุ่ม ถัดไป(Next) หรือปุ่มส่ง ถ้ามีแค่ 1 ข้อ
|
|
||||||
await expect(submitBtn.or(nextBtn)).toBeVisible({ timeout: 10_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,54 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @file student-account.spec.ts
|
||||||
|
* @description ทดสอบระบบพื้นที่ส่วนตัวผู้เรียน (Student Account / Portal)
|
||||||
|
*/
|
||||||
import { test, expect } from '@playwright/test';
|
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';
|
|
||||||
|
|
||||||
async function waitAppSettled(page: any) {
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ฟังก์ชันจำลองล็อกอิน (เพื่อที่จะเข้า Dashboard ได้)
|
|
||||||
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('ระบบพื้นที่ส่วนตัวผู้เรียน (Student Account / Portal)', () => {
|
test.describe('ระบบพื้นที่ส่วนตัวผู้เรียน (Student Account / Portal)', () => {
|
||||||
|
|
||||||
test.describe('การตั้งค่าและส่วนติดต่อผู้ใช้ (Settings & UI Theme)', () => {
|
test.describe('การตั้งค่าและส่วนติดต่อผู้ใช้ (Settings & UI Theme)', () => {
|
||||||
test('เปลี่ยนภาษาการแสดงผล (Localisation/i18n)', async ({ page }) => {
|
test('เปลี่ยนภาษาการแสดงผล (Localisation/i18n)', async ({ page }) => {
|
||||||
await page.goto(BASE_URL);
|
await page.goto(BASE_URL);
|
||||||
const langBtn = page.getByRole('button', { name: 'Language' }).or(page.locator('button').filter({ hasText: /TH|EN/ })).first();
|
await waitAppSettled(page);
|
||||||
|
|
||||||
|
// หาปุ่มภาษา — ถ้าไม่เจอให้ test.skip แทนที่จะผ่านเงียบๆ
|
||||||
|
const langBtn = page.getByRole('button', { name: 'Language' })
|
||||||
|
.or(page.locator('button').filter({ hasText: /TH|EN/ }))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const isLangBtnVisible = await langBtn.isVisible().catch(() => false);
|
||||||
|
if (!isLangBtnVisible) {
|
||||||
|
test.skip(true, 'Language button not found on page — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (await langBtn.isVisible()) {
|
|
||||||
await langBtn.click();
|
await langBtn.click();
|
||||||
const englishOpt = page.locator('text=English, text=EN').first();
|
const englishOpt = page.locator('text=English, text=EN').first();
|
||||||
await englishOpt.click();
|
await englishOpt.click();
|
||||||
|
|
||||||
const loginLink = page.locator('a[href*="login"], button').filter({ hasText: /Log in|Sign In/i });
|
const loginLink = page.locator('a[href*="login"], button').filter({ hasText: /Log in|Sign In/i });
|
||||||
await expect(loginLink).toBeVisible({ timeout: 5000 });
|
await expect(loginLink).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
}
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-i18n.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('เปลี่ยนโหมดมืดสว่าง (Theme Switcher)', async ({ page }) => {
|
test('เปลี่ยนโหมดมืดสว่าง (Theme Switcher)', async ({ page }) => {
|
||||||
await page.goto(BASE_URL);
|
await page.goto(BASE_URL);
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
|
// หาปุ่ม Theme — ถ้าไม่เจอให้ test.skip แทนที่จะผ่านเงียบๆ
|
||||||
const themeBtn = page.locator('.dark-mode-toggle, button[aria-label*="theme"]').first();
|
const themeBtn = page.locator('.dark-mode-toggle, button[aria-label*="theme"]').first();
|
||||||
|
|
||||||
if (await themeBtn.isVisible()) {
|
const isThemeBtnVisible = await themeBtn.isVisible().catch(() => false);
|
||||||
|
if (!isThemeBtnVisible) {
|
||||||
|
test.skip(true, 'Theme toggle button not found on page — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const htmlBefore = await page.evaluate(() => document.documentElement.className);
|
const htmlBefore = await page.evaluate(() => document.documentElement.className);
|
||||||
await themeBtn.click();
|
await themeBtn.click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
const htmlAfter = await page.evaluate(() => document.documentElement.className);
|
const htmlAfter = await page.evaluate(() => document.documentElement.className);
|
||||||
expect(htmlBefore).not.toEqual(htmlAfter);
|
expect(htmlBefore).not.toEqual(htmlAfter);
|
||||||
}
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-theme.png', fullPage: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -59,60 +63,77 @@ test.describe('ระบบพื้นที่ส่วนตัวผู้
|
||||||
|
|
||||||
test('หน้าแรกของ Dashboard โหลดได้ปกติ', async ({ page }) => {
|
test('หน้าแรกของ Dashboard โหลดได้ปกติ', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/dashboard`);
|
await page.goto(`${BASE_URL}/dashboard`);
|
||||||
await page.waitForTimeout(1000);
|
await waitAppSettled(page, 1000);
|
||||||
|
|
||||||
const welcomeText = page.getByText(/ยินดีต้อนรับกลับ/i, { exact: false });
|
const welcomeText = page.getByText(/ยินดีต้อนรับกลับ/i, { exact: false });
|
||||||
const profileSummary = page.locator('.q-avatar, img[alt*="Profile"], img[src*="avatar"]').first();
|
const profileSummary = page.locator('.q-avatar, img[alt*="Profile"], img[src*="avatar"]').first();
|
||||||
|
|
||||||
await expect(welcomeText.first().or(profileSummary)).toBeVisible({ timeout: 10_000 });
|
await expect(welcomeText.first().or(profileSummary)).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-dashboard.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('โหลดหน้า คอร์สของฉัน (My Courses)', async ({ page }) => {
|
test('โหลดหน้า คอร์สของฉัน (My Courses)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/dashboard/my-courses`);
|
await page.goto(`${BASE_URL}/dashboard/my-courses`);
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const heading = page.locator('h2').filter({ hasText: /คอร์สของฉัน|My Courses/i }).first();
|
const heading = page.locator('h2').filter({ hasText: /คอร์สของฉัน|My Courses/i }).first();
|
||||||
await expect(heading).toBeVisible();
|
await expect(heading).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
const searchInput = page.getByPlaceholder(/ค้นหา|Search/i).first();
|
const searchInput = page.getByPlaceholder(/ค้นหา|Search/i).first();
|
||||||
await expect(searchInput).toBeVisible({ timeout: 10_000 });
|
await expect(searchInput).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
await expect(page.locator('i.q-icon').filter({ hasText: 'grid_view' }).first()).toBeVisible();
|
await expect(page.locator('i.q-icon').filter({ hasText: 'grid_view' }).first()).toBeVisible();
|
||||||
await expect(page.locator('i.q-icon').filter({ hasText: 'view_list' }).first()).toBeVisible();
|
await expect(page.locator('i.q-icon').filter({ hasText: 'view_list' }).first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-my-courses.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ลองค้นหาคอร์ส (Search Input) ไม่พบข้อมูล', async ({ page }) => {
|
test('ลองค้นหาคอร์ส (Search Input) ไม่พบข้อมูล', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/dashboard/my-courses`);
|
await page.goto(`${BASE_URL}/dashboard/my-courses`);
|
||||||
|
await waitAppSettled(page);
|
||||||
|
|
||||||
const searchInput = page.getByPlaceholder(/ค้นหา|Search/i).first();
|
const searchInput = page.getByPlaceholder(/ค้นหา|Search/i).first();
|
||||||
await expect(searchInput).toBeVisible({ timeout: 10_000 });
|
await expect(searchInput).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
await searchInput.fill('คอร์สที่ไม่มีอยู่จริงแน่นอน1234');
|
await searchInput.fill('คอร์สที่ไม่มีอยู่จริงแน่นอน1234');
|
||||||
|
|
||||||
const emptyState = page.locator('h3').filter({ hasText: /ไม่พบ|ไม่เจอ|No result/i }).first()
|
const emptyState = page.locator('h3').filter({ hasText: /ไม่พบ|ไม่เจอ|No result/i }).first()
|
||||||
.or(page.locator('i.q-icon').filter({ hasText: 'search_off' }));
|
.or(page.locator('i.q-icon').filter({ hasText: 'search_off' }));
|
||||||
|
|
||||||
await expect(emptyState.first()).toBeVisible({ timeout: 10_000 });
|
await expect(emptyState.first()).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-search-empty.png', fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('แก้ไขและบันทึกข้อมูลส่วนตัว (Edit Profile)', async ({ page }) => {
|
test('แก้ไขและบันทึกข้อมูลส่วนตัว (Edit Profile)', async ({ page }) => {
|
||||||
await page.goto(`${BASE_URL}/dashboard/profile`);
|
await page.goto(`${BASE_URL}/dashboard/profile`);
|
||||||
|
await waitAppSettled(page, 1000);
|
||||||
|
|
||||||
const nameInput = page.locator('input[type="text"]').first();
|
// หา input ชื่อ — ใช้ textbox "First Name" หรือ input[type="text"] ตัวแรก
|
||||||
|
const nameInput = page.getByRole('textbox', { name: /First Name|ชื่อ/i }).first()
|
||||||
|
.or(page.locator('input[type="text"]').first());
|
||||||
|
|
||||||
|
const isNameVisible = await nameInput.isVisible().catch(() => false);
|
||||||
|
if (!isNameVisible) {
|
||||||
|
test.skip(true, 'Profile name input not found — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (await nameInput.isVisible()) {
|
|
||||||
const oldName = await nameInput.inputValue();
|
const oldName = await nameInput.inputValue();
|
||||||
|
|
||||||
await nameInput.clear();
|
await nameInput.clear();
|
||||||
await nameInput.fill(`${oldName}แก้ไข`);
|
await nameInput.fill(`${oldName}แก้ไข`);
|
||||||
|
|
||||||
const saveBtn = page.getByRole('button', { name: /บันทึก/i }).first();
|
// ปุ่มบันทึก — รองรับทั้งภาษาไทยและอังกฤษ
|
||||||
if(await saveBtn.isVisible()) {
|
const saveBtn = page.getByRole('button', { name: /บันทึก|Save Changes|Save/i }).first();
|
||||||
|
await expect(saveBtn).toBeVisible({ timeout: TIMEOUT.ELEMENT });
|
||||||
await saveBtn.click();
|
await saveBtn.click();
|
||||||
const successNotify = page.locator('.q-notification__message, text=อัปเดตข้อมูลสำเร็จ').first();
|
|
||||||
await expect(successNotify).toBeVisible({ timeout: 5000 }).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Toast สำเร็จ — รองรับทั้ง 2 ภาษา
|
||||||
|
const successNotify = page.getByText(/อัปเดตข้อมูลสำเร็จ|บันทึกข้อมูล|updated|saved|success/i).first();
|
||||||
|
await expect(successNotify).toBeVisible({ timeout: TIMEOUT.ELEMENT }).catch(() => {});
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'tests/e2e/screenshots/student-edit-profile.png', fullPage: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
BIN
tests/e2e/screenshots/discovery-curriculum.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
tests/e2e/screenshots/discovery-enroll-btn.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
tests/e2e/screenshots/discovery-filter.png
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
tests/e2e/screenshots/discovery-home.png
Normal file
|
After Width: | Height: | Size: 981 KiB |
BIN
tests/e2e/screenshots/discovery-search.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
BIN
tests/e2e/screenshots/student-dashboard.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
tests/e2e/screenshots/student-edit-profile.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
tests/e2e/screenshots/student-my-courses.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
tests/e2e/screenshots/student-search-empty.png
Normal file
|
After Width: | Height: | Size: 60 KiB |