feat: Establish Playwright testing infrastructure with initial tests for authentication, admin, and instructor modules, and fix instructor video and quiz lesson management pages.
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 1m17s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 8s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 2s
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 1m17s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 8s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 2s
This commit is contained in:
parent
734d922393
commit
9bc24fbe8a
18 changed files with 1344 additions and 7 deletions
7
frontend_management/.gitignore
vendored
7
frontend_management/.gitignore
vendored
|
|
@ -26,10 +26,9 @@ deploy.ps1
|
|||
*.tar
|
||||
|
||||
# Playwright
|
||||
tests
|
||||
tests/.auth/
|
||||
/test-results/
|
||||
test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
playwright.config.ts
|
||||
/tests/fixtures/test-data.ts
|
||||
.auth
|
||||
18
frontend_management/package-lock.json
generated
18
frontend_management/package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
|||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.3.0",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^25.0.3",
|
||||
|
|
@ -1075,6 +1076,23 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz",
|
||||
"integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@
|
|||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.3.0",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"nuxt-quasar-ui": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ const savePrerequisiteSettings = async () => {
|
|||
...form.value,
|
||||
prerequisite_lesson_ids: prerequisiteSettings.value.prerequisite_lesson_ids.length > 0
|
||||
? prerequisiteSettings.value.prerequisite_lesson_ids
|
||||
: null
|
||||
: []
|
||||
});
|
||||
$q.notify({ type: 'positive', message: response.message || 'บันทึกการตั้งค่าสำเร็จ', position: 'top' });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -506,7 +506,7 @@ const savePrerequisiteSettings = async () => {
|
|||
...form.value,
|
||||
prerequisite_lesson_ids: prerequisiteSettings.value.prerequisite_lesson_ids.length > 0
|
||||
? prerequisiteSettings.value.prerequisite_lesson_ids
|
||||
: null
|
||||
: []
|
||||
});
|
||||
$q.notify({ type: 'positive', message: response.message || 'บันทึกการตั้งค่าสำเร็จ', position: 'top' });
|
||||
} catch (error) {
|
||||
|
|
|
|||
93
frontend_management/playwright.config.ts
Normal file
93
frontend_management/playwright.config.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
timeout: 60000, // timeout ต่อ test (ms)
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
// workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1,
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000/',// ปรับเป็น URL ที่ทดสอบ
|
||||
headless: false, // false = เห็น browser ขณะรัน
|
||||
screenshot: 'only-on-failure', // เก็บ screenshot เมื่อ fail
|
||||
trace: 'retain-on-failure', // เก็บ trace เพื่อดีบักเมื่อ fail
|
||||
// launchOptions: {
|
||||
// slowMo: 1000,
|
||||
// }, // ช้าลง 10 วินาที
|
||||
},
|
||||
|
||||
/* ──── Setup Projects: login ครั้งเดียว แล้ว save cookies ──── */
|
||||
projects: [
|
||||
{
|
||||
name: 'admin-setup',
|
||||
testMatch: /admin\.setup\.ts/,
|
||||
testDir: './tests',
|
||||
},
|
||||
{
|
||||
name: 'instructor-setup',
|
||||
testMatch: /instructor\.setup\.ts/,
|
||||
testDir: './tests',
|
||||
},
|
||||
|
||||
/* ──── Auth tests: ไม่ต้อง login ก่อน ──── */
|
||||
{
|
||||
name: 'auth-tests',
|
||||
testDir: './tests/auth',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
/* ──── Admin tests: ใช้ cookies จาก admin-setup ──── */
|
||||
{
|
||||
name: 'admin-tests',
|
||||
testDir: './tests/admin',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: path.resolve(__dirname, 'tests/.auth/admin.json'),
|
||||
},
|
||||
dependencies: ['admin-setup'],
|
||||
},
|
||||
|
||||
/* ──── Instructor tests: ใช้ cookies จาก instructor-setup ──── */
|
||||
{
|
||||
name: 'instructor-tests',
|
||||
testDir: './tests/instructor',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: path.resolve(__dirname, 'tests/.auth/instructor.json'),
|
||||
},
|
||||
dependencies: ['instructor-setup'],
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
96
frontend_management/tests/admin/audit-log.spec.ts
Normal file
96
frontend_management/tests/admin/audit-log.spec.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Admin Audit Log Page Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Audit Log', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminAuditLog);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('check display page', async ({ page }) => {
|
||||
// Header
|
||||
await expect(page.getByText('Audit Logs', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('ประวัติการใช้งานระบบและกิจกรรมต่างๆ')).toBeVisible();
|
||||
|
||||
// Stats cards
|
||||
await expect(page.getByText('Logs ทั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('Logs วันนี้')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'รีเฟรช' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'ล้างประวัติเก่า' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should refresh data when clicking refresh button', async ({ page }) => {
|
||||
const refreshBtn = page.getByRole('button', { name: 'รีเฟรช' });
|
||||
await refreshBtn.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Table should still be visible after refresh
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter by Action', async ({ page }) => {
|
||||
// Click the Action select
|
||||
const actionSelect = page.locator('label').filter({ hasText: 'Action' });
|
||||
await actionSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Select an option from dropdown
|
||||
const option = page.locator('.q-item__label').filter({ hasText: 'LOGIN' }).first();
|
||||
await option.click();
|
||||
|
||||
// Click search
|
||||
await page.getByRole('button', { name: 'ค้นหา' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Table should be visible
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'ล้างตัวกรอง' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Entity type input should be cleared
|
||||
await expect(option).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('check open details dialog', async ({ page }) => {
|
||||
// Wait for table data to load
|
||||
const viewBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await viewBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await viewBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Details dialog should be visible
|
||||
await expect(page.getByText('รายละเอียด Audit Log')).toBeVisible();
|
||||
|
||||
// Close button
|
||||
await page.getByRole('button', { name: 'ปิด' }).click();
|
||||
}
|
||||
});
|
||||
|
||||
test('check close cleanup dialog on cancel', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'ล้างประวัติเก่า' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('.q-dialog')).toBeVisible();
|
||||
|
||||
// Click cancel
|
||||
await page.getByRole('button', { name: 'ยกเลิก' }).click();
|
||||
await expect(page.locator('.q-dialog')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('check close cleanup dialog on confirm', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'ล้างประวัติเก่า' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('.q-dialog')).toBeVisible();
|
||||
|
||||
// Click confirm
|
||||
await page.getByRole('button', { name: 'ลบข้อมูล', exact: true }).click();
|
||||
await expect(page.locator('.q-dialog')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
158
frontend_management/tests/admin/categories.spec.ts
Normal file
158
frontend_management/tests/admin/categories.spec.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* Admin Categories Page Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Categories', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminCategories);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('check display page title and table', async ({ page }) => {
|
||||
await expect(page.getByText('รายการหมวดหมู่')).toBeVisible();
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
await expect(page.getByText('ชื่อหมวดหมู่')).toBeVisible();
|
||||
await expect(page.getByText('คำอธิบาย')).toBeVisible();
|
||||
await expect(page.getByText('วันที่สร้าง')).toBeVisible();
|
||||
await expect(page.getByText('การจัดการ')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
test('check search input', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหาหมวดหมู่"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
await searchInput.fill('ธุรกิจ');
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByText('ธุรกิจ', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('check filter categories by search', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหาหมวดหมู่"]');
|
||||
await searchInput.fill('ไม่มีหมวดหมู่นี้แน่นอน');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// ตารางควรไม่มีข้อมูล
|
||||
await expect(page.getByText('No data available')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
test('should add new category successfully', async ({ page }) => {
|
||||
// สร้างข้อมูลสุ่มด้วย faker
|
||||
const nameTh = faker.word.noun();
|
||||
const nameEn = faker.commerce.department();
|
||||
const slug = faker.helpers.slugify(nameEn.toLowerCase()) + '-' + faker.string.alpha({ length: 6, casing: 'lower' });
|
||||
const descTh = faker.lorem.sentence();
|
||||
const descEn = faker.lorem.sentence();
|
||||
|
||||
// 1. กดปุ่มเพิ่มหมวดหมู่
|
||||
await page.getByRole('button', { name: /เพิ่มหมวดหมู่ใหม่/ }).click();
|
||||
await expect(page.getByText('เพิ่ม', { exact: true })).toBeVisible();
|
||||
|
||||
// 2. กรอกข้อมูลในฟอร์ม
|
||||
const dialog = page.locator('.q-dialog');
|
||||
const inputs = dialog.locator('input');
|
||||
await inputs.nth(0).fill(nameTh);
|
||||
await inputs.nth(1).fill(nameEn);
|
||||
await inputs.nth(2).fill(slug);
|
||||
|
||||
const textareas = dialog.locator('textarea');
|
||||
await textareas.nth(0).fill(descTh);
|
||||
await textareas.nth(1).fill(descEn);
|
||||
|
||||
// 3. กดปุ่ม "เพิ่ม"
|
||||
await page.getByRole('button', { name: 'เพิ่ม', exact: true }).click();
|
||||
|
||||
// 4. ตรวจสอบว่ามี notification สำเร็จ
|
||||
await expect(page.locator('.q-notification')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// 5. modal ต้องปิด
|
||||
await expect(dialog).not.toBeVisible();
|
||||
|
||||
// 6. ตรวจสอบว่าหมวดหมู่ใหม่แสดงในตาราง
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหาหมวดหมู่"]');
|
||||
await searchInput.fill(nameTh);
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByText(nameTh, { exact: true })).toBeVisible();
|
||||
|
||||
const editBtn = page.locator('.q-table button .q-icon').filter({ hasText: 'edit' }).first();
|
||||
const deleteBtn = page.locator('.q-table button .q-icon').filter({ hasText: 'delete' }).first();
|
||||
await expect(editBtn).toBeVisible();
|
||||
await expect(deleteBtn).toBeVisible();
|
||||
|
||||
await editBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByText('แก้ไขหมวดหมู่', { exact: true })).toBeVisible();
|
||||
await inputs.nth(0).fill(nameTh + ' edit');
|
||||
await inputs.nth(1).fill(nameEn + ' edit');
|
||||
await inputs.nth(2).fill(slug + 'edit');
|
||||
await textareas.nth(0).fill(descTh + ' edit');
|
||||
await textareas.nth(1).fill(descEn + ' edit');
|
||||
await page.getByRole('button', { name: 'บันทึก', exact: true }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await searchInput.fill(nameTh + ' edit');
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByText(nameTh + ' edit', { exact: true })).toBeVisible();
|
||||
|
||||
await deleteBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('.q-notification').filter({ hasText: 'deleted' })).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('should show validation error when submitting empty form', async ({ page }) => {
|
||||
// 1. เปิด modal เพิ่มหมวดหมู่
|
||||
await page.getByRole('button', { name: /เพิ่มหมวดหมู่ใหม่/ }).click();
|
||||
await expect(page.getByText('เพิ่ม', { exact: true })).toBeVisible();
|
||||
|
||||
// 2. กดปุ่ม "เพิ่ม" โดยไม่กรอกข้อมูล
|
||||
await page.getByRole('button', { name: 'เพิ่ม', exact: true }).click();
|
||||
|
||||
// 3. ตรวจสอบ validation error
|
||||
await expect(page.getByText('กรุณากรอกชื่อ')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should close add modal on cancel', async ({ page }) => {
|
||||
// 1. เปิด modal
|
||||
await page.getByRole('button', { name: /เพิ่มหมวดหมู่ใหม่/ }).click();
|
||||
await expect(page.getByText('เพิ่ม', { exact: true })).toBeVisible();
|
||||
|
||||
// 2. กดยกเลิก
|
||||
await page.getByRole('button', { name: 'ยกเลิก' }).click();
|
||||
|
||||
// 3. modal ต้องหายไป
|
||||
await expect(page.locator('.q-dialog')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should open edit modal when clicking edit button', async ({ page }) => {
|
||||
// 1. กดปุ่ม edit ของแถวแรก
|
||||
const editBtn = page.locator('.q-table button[class*="q-btn"]').first();
|
||||
await expect(editBtn).toBeVisible();
|
||||
await editBtn.click();
|
||||
|
||||
// 2. ตรวจสอบว่า modal แก้ไขเปิดขึ้น
|
||||
await expect(page.getByText('แก้ไขหมวดหมู่')).toBeVisible();
|
||||
|
||||
// 3. ฟอร์มต้องมีข้อมูลอยู่แล้ว (ไม่ว่าง)
|
||||
const nameInput = page.locator('.q-dialog input').first();
|
||||
await expect(nameInput).not.toHaveValue('');
|
||||
|
||||
// 4. ปุ่มต้องเป็น "บันทึก" ไม่ใช่ "เพิ่ม"
|
||||
await expect(page.getByRole('button', { name: 'บันทึก' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show delete confirmation dialog', async ({ page }) => {
|
||||
// 1. กดปุ่ม delete ของแถวแรก (ปุ่มที่สองใน actions)
|
||||
const deleteBtn = page.locator('.q-table .flex.gap-2 button').nth(1);
|
||||
await expect(deleteBtn).toBeVisible();
|
||||
await deleteBtn.click();
|
||||
|
||||
// 2. ตรวจสอบว่า dialog ยืนยันการลบแสดง
|
||||
await expect(page.getByText('ยืนยันการลบ')).toBeVisible();
|
||||
});
|
||||
});
|
||||
117
frontend_management/tests/admin/courses-pending.spec.ts
Normal file
117
frontend_management/tests/admin/courses-pending.spec.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Admin Pending Courses Page Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Pending Courses', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminPendingCourses);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should display stats cards', async ({ page }) => {
|
||||
await expect(page.getByText('รอตรวจสอบ')).toBeVisible();
|
||||
await expect(page.getByText('บททั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('บทเรียนทั้งหมด')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should search courses by keyword "พื้นฐาน"', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหา"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// ค้นหาคำว่า "พื้นฐาน"
|
||||
await searchInput.fill('พื้นฐาน');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// ตรวจสอบว่าแสดงผลลัพธ์ที่ตรงกัน หรือแสดง empty state
|
||||
const emptyState = page.getByText('ไม่มีคอร์สที่รอการอนุมัติ');
|
||||
const isEmpty = await emptyState.isVisible().catch(() => false);
|
||||
|
||||
if (isEmpty) {
|
||||
await expect(emptyState).toBeVisible();
|
||||
} else {
|
||||
// ถ้ามีผลลัพธ์ ชื่อคอร์สต้องมีคำว่า "พื้นฐาน"
|
||||
const courseCards = page.locator('.bg-white.rounded-xl.shadow-sm.overflow-hidden');
|
||||
const count = await courseCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(courseCards.nth(i)).toContainText('พื้นฐาน');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter courses by search text', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหา"]');
|
||||
await searchInput.fill('nonexistent-course-xyz');
|
||||
|
||||
// Should show empty state or filtered results
|
||||
// Wait briefly for filter to apply
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Either shows "ไม่มีคอร์สที่รอการอนุมัติ" or filtered results
|
||||
const emptyState = page.getByText('ไม่มีคอร์สที่รอการอนุมัติ');
|
||||
const courseCards = page.locator('.bg-white.rounded-xl.shadow-sm.overflow-hidden');
|
||||
|
||||
// One of these should be visible
|
||||
const isEmpty = await emptyState.isVisible().catch(() => false);
|
||||
if (isEmpty) {
|
||||
await expect(emptyState).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle between card and table view', async ({ page }) => {
|
||||
// Find the view toggle buttons
|
||||
const toggleGroup = page.locator('.q-btn-toggle');
|
||||
await expect(toggleGroup).toBeVisible();
|
||||
|
||||
// Click table view
|
||||
await page.locator('.q-btn-toggle button').last().click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Table should be visible
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
// Switch back to card view
|
||||
await page.locator('.q-btn-toggle button').first().click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.locator('.q-btn-toggle button').last().click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Table should have expected columns
|
||||
await expect(page.getByText('ชื่อคอร์ส')).toBeVisible();
|
||||
await expect(page.getByText('ผู้สอน')).toBeVisible();
|
||||
await expect(page.getByText('วันที่ส่ง')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have refresh button', async ({ page }) => {
|
||||
const refreshBtn = page.getByRole('button', { name: /รีเฟรช/ });
|
||||
await expect(refreshBtn).toBeVisible();
|
||||
|
||||
// Click refresh
|
||||
await refreshBtn.click();
|
||||
|
||||
// Should trigger loading (spinner appears briefly)
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should navigate to course detail on click if ', async ({ page }) => {
|
||||
// Check if there are any courses
|
||||
const courseExists = await page.locator('.bg-white.rounded-xl').first().isVisible().catch(() => false);
|
||||
|
||||
if (courseExists) {
|
||||
// Click "ดูรายละเอียด" button on first course
|
||||
const viewBtn = page.locator('.q-table button[class*="q-btn"]').first();
|
||||
if (await viewBtn.isVisible()) {
|
||||
await viewBtn.click();
|
||||
await page.waitForURL('**/admin/courses/**');
|
||||
await expect(page).toHaveURL(/\/admin\/courses\/\d+/);
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
44
frontend_management/tests/admin/dashboard.spec.ts
Normal file
44
frontend_management/tests/admin/dashboard.spec.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Admin Dashboard Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminDashboard);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('show display dashboard', async ({ page }) => {
|
||||
await expect(page.locator('h1')).toContainText('สวัสดี');
|
||||
await expect(page.locator("//p[normalize-space(text())='คอร์สรออนุมัติ']")).toBeVisible();
|
||||
await expect(page.getByText('กิจกรรมวันนี้')).toBeVisible();
|
||||
await expect(page.getByText('ผู้ใช้งานทั้งหมด')).toBeVisible();
|
||||
});
|
||||
|
||||
test('navigate to pending courses and audit-log page', async ({ page }) => {
|
||||
await page.locator('a[href*="pending"]', { hasText: 'ดูทั้งหมด' }).click();
|
||||
await page.waitForURL('**/admin/courses/pending**');
|
||||
await expect(page).toHaveURL(/\/admin\/courses\/pending/);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.goto(TEST_URLS.adminDashboard);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.locator('a[href*="audit-log"]', { hasText: 'ดูทั้งหมด' }).click();
|
||||
await page.waitForURL('**/admin/audit-log**');
|
||||
await expect(page).toHaveURL(/\/admin\/audit-log/);
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('show user menu on avatar click', async ({ page }) => {
|
||||
// Click the avatar / user icon area
|
||||
await page.locator('.w-12.h-12.rounded-full').click();
|
||||
await page.waitForTimeout(500);
|
||||
// Menu should appear with profile and logout options
|
||||
await expect(page.getByText('โปรไฟล์')).toBeVisible();
|
||||
});
|
||||
});
|
||||
67
frontend_management/tests/admin/recommended-courses.spec.ts
Normal file
67
frontend_management/tests/admin/recommended-courses.spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Admin Recommended Courses Page Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Recommended Courses', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminRecommendedCourses);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('check display page', async ({ page }) => {
|
||||
// Header
|
||||
await expect(page.getByText('จัดการคอร์สแนะนำ')).toBeVisible();
|
||||
await expect(page.getByText('Recommended Courses Management')).toBeVisible();
|
||||
const toggles = page.locator('.q-table .q-toggle');
|
||||
const hasData = await toggles.first().isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await expect(toggles.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle recommendation status', async ({ page }) => {
|
||||
const toggle = page.locator('.q-table .q-toggle').first();
|
||||
const hasData = await toggle.isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should show notification (success or error)
|
||||
await expect(page.locator('.q-notification')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should show notification (success or error)
|
||||
await expect(page.locator('.q-notification').last()).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('open course details dialog', async ({ page }) => {
|
||||
const viewBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await viewBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await viewBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Dialog should be visible with course details
|
||||
await expect(page.getByText('รายละเอียดคอร์ส (Course Details)')).toBeVisible();
|
||||
|
||||
// Detail sections
|
||||
await expect(page.getByText('รายละเอียด (Description)')).toBeVisible();
|
||||
await expect(page.getByText('หมวดหมู่ (Category):')).toBeVisible();
|
||||
await expect(page.getByText('ผู้สอน (Instructors)')).toBeVisible();
|
||||
|
||||
// Close dialog via close button in q-bar
|
||||
await page.locator('.q-bar .q-btn').filter({ has: page.locator('[class*="q-icon"]') }).click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.getByText('รายละเอียดคอร์ส (Course Details)')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
213
frontend_management/tests/admin/users.spec.ts
Normal file
213
frontend_management/tests/admin/users.spec.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Admin Users Page Tests
|
||||
* ใช้ cookies จาก admin-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Admin Users', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.adminUsers);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('check display page', async ({ page }) => {
|
||||
// Header
|
||||
await expect(page.getByText('จัดการผู้ใช้งาน')).toBeVisible();
|
||||
|
||||
// Search input
|
||||
await expect(page.locator('input[placeholder*="ค้นหาชื่อ"]')).toBeVisible();
|
||||
|
||||
// Stats cards
|
||||
await expect(page.getByText('ผู้ใช้ทั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('Admin', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Instructor', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Student', { exact: true })).toBeVisible();
|
||||
|
||||
// Table
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter users by search', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหาชื่อ"]');
|
||||
await searchInput.fill('admin');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Table should still be visible with filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter users by role', async ({ page }) => {
|
||||
// ใช้ outlined q-select (ตัว filter role ไม่ใช่ pagination)
|
||||
const roleSelect = page.locator('.q-select.q-field--outlined');
|
||||
await roleSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Select Instructor
|
||||
await page.locator('.q-item__label').filter({ hasText: 'Instructor' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// Table should show filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
await roleSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
// Select Student
|
||||
await page.locator('.q-item__label').filter({ hasText: 'Student' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// Table should show filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
await roleSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
// Select Admin
|
||||
await page.locator('.q-item__label').filter({ hasText: 'Admin' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// Table should show filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
// กลับไปเลือก "บทบาททั้งหมด"
|
||||
await roleSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.q-item__label').filter({ hasText: 'บทบาททั้งหมด' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('should open action menu', async ({ page }) => {
|
||||
// Click more_vert button on first row
|
||||
const actionBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await actionBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Menu should show options
|
||||
await expect(page.getByText('ดู', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('แก้ Role', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('ลบ', { exact: true })).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should open view user dialog', async ({ page }) => {
|
||||
const actionBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await actionBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasData) {
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Click "ดู" option
|
||||
await page.getByText('ดู', { exact: true }).click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Dialog should show user details
|
||||
await expect(page.getByText('รายละเอียดผู้ใช้')).toBeVisible();
|
||||
|
||||
// Close button
|
||||
await page.getByRole('button', { name: 'ปิด' }).click();
|
||||
await expect(page.locator('.q-dialog')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('open change role dialog', async ({ page }) => {
|
||||
const actionBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await actionBtn.isVisible().catch(() => false);
|
||||
|
||||
// Table should still be visible with filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
if (hasData) {
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Click "แก้ Role"
|
||||
await page.getByText('แก้ Role').click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Change role dialog should appear
|
||||
await expect(page.getByText('เปลี่ยน Role')).toBeVisible();
|
||||
|
||||
// Cancel the dialog
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
});
|
||||
|
||||
test('change role ', async ({ page }) => {
|
||||
const actionBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
const hasData = await actionBtn.isVisible().catch(() => false);
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหาชื่อ"]');
|
||||
await searchInput.fill('lertp');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Table should still be visible with filtered results
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
if (hasData) {
|
||||
// Click "แก้ Role Student"
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByText('แก้ Role').click();
|
||||
await page.waitForTimeout(300);
|
||||
// Change role dialog should appear
|
||||
await expect(page.getByText('เปลี่ยน Role')).toBeVisible();
|
||||
// select role Student
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.q-dialog .q-radio__label').filter({ hasText: 'Student' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// save
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Click "แก้ Role Admin"
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByText('แก้ Role').click();
|
||||
await page.waitForTimeout(300);
|
||||
// Change role dialog should appear
|
||||
await expect(page.getByText('เปลี่ยน Role')).toBeVisible();
|
||||
// select role Admin
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.q-dialog .q-radio__label').filter({ hasText: 'Admin' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// save
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
|
||||
// Click "แก้ Role Instructor"
|
||||
await actionBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByText('แก้ Role').click();
|
||||
await page.waitForTimeout(300);
|
||||
// Change role dialog should appear
|
||||
await expect(page.getByText('เปลี่ยน Role')).toBeVisible();
|
||||
// select role Instructor
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.q-dialog .q-radio__label').filter({ hasText: 'Instructor' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// save
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
});
|
||||
|
||||
// test('should open delete confirmation dialog', async ({ page }) => {
|
||||
// const actionBtn = page.locator('.q-table .q-btn').filter({ has: page.locator('.q-icon') }).first();
|
||||
// const hasData = await actionBtn.isVisible().catch(() => false);
|
||||
|
||||
// if (hasData) {
|
||||
// await actionBtn.click();
|
||||
// await page.waitForTimeout(300);
|
||||
|
||||
// // Click "ลบ"
|
||||
// await page.getByText('ลบ').click();
|
||||
// await page.waitForTimeout(300);
|
||||
|
||||
// // Delete confirmation dialog
|
||||
// await expect(page.getByText('ยืนยันการลบ')).toBeVisible();
|
||||
|
||||
// // Cancel the dialog
|
||||
// await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
// await page.waitForTimeout(300);
|
||||
// }
|
||||
// });
|
||||
});
|
||||
124
frontend_management/tests/auth/login.spec.ts
Normal file
124
frontend_management/tests/auth/login.spec.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_ADMIN, TEST_INSTRUCTOR, TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
test.describe('Login Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.login);
|
||||
});
|
||||
|
||||
test('should display login form', async ({ page }) => {
|
||||
// Title
|
||||
await expect(page.locator('h1')).toContainText('E-Learning');
|
||||
|
||||
// Email & password fields
|
||||
await expect(page.locator('input[type="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]')).toBeVisible();
|
||||
|
||||
// Submit button
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
|
||||
// Register link
|
||||
await expect(page.locator('a[href="/register"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors for empty fields', async ({ page }) => {
|
||||
// Click submit without filling fields
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Expect validation messages in Thai
|
||||
await expect(page.getByText('กรุณากรอกอีเมล')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors for empty fields password', async ({ page }) => {
|
||||
// Click submit without filling fields
|
||||
await page.locator('input[type="email"]').fill('test@email.com');
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Expect validation messages in Thai
|
||||
await expect(page.getByText('กรุณากรอกรหัสผ่าน')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should toggle password visibility', async ({ page }) => {
|
||||
const passwordInput = page.locator('input[type="password"]');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await passwordInput.fill('test123');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
// Click the visibility toggle icon
|
||||
await page.locator('//i[normalize-space(text())="visibility"]').click();
|
||||
|
||||
// Password field should now be text type
|
||||
await expect(page.locator('input[type="text"]').last()).toHaveValue('test123');
|
||||
});
|
||||
|
||||
test('should show error for invalid credentials', async ({ page }) => {
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[type="email"]').fill('wrong@email.com');
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('input[type="password"]').fill('wrongpassword');
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should show error notification (Quasar Notify)
|
||||
await expect(page.locator('.q-notification')).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('should login as admin and redirect to /admin', async ({ page }) => {
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[type="email"]').fill(TEST_ADMIN.email);
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('input[type="password"]').fill(TEST_ADMIN.password);
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should redirect to admin dashboard
|
||||
await page.waitForURL('**/admin**', { timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/admin/);
|
||||
});
|
||||
|
||||
test('should login as instructor and redirect to /instructor', async ({ page }) => {
|
||||
await page.locator('input[type="email"]').fill(TEST_INSTRUCTOR.email);
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[type="password"]').fill(TEST_INSTRUCTOR.password);
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should redirect to instructor dashboard
|
||||
await page.waitForURL('**/instructor**', { timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/instructor/);
|
||||
});
|
||||
|
||||
test('should open forgot password modal', async ({ page }) => {
|
||||
await page.getByText('ลืมรหัสผ่าน?').click();
|
||||
|
||||
// Modal should be visible
|
||||
await expect(page.getByText('ลืมรหัสผ่าน').nth(1)).toBeVisible();
|
||||
await expect(page.locator('.q-dialog input[type="email"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should remember email after login with remember me checked', async ({ page }) => {
|
||||
// 1. Fill credentials
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('input[type="email"]').fill(TEST_ADMIN.email);
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('input[type="password"]').fill(TEST_ADMIN.password);
|
||||
await page.waitForTimeout(500);
|
||||
// 2. Check "remember me"
|
||||
await page.getByText('จดจำฉันไว้').click();
|
||||
|
||||
// 3. Login
|
||||
await page.locator('button[type="submit"]').click();
|
||||
await page.waitForURL('**/admin**', { timeout: 15_000 });
|
||||
|
||||
// 4. Logout - click avatar menu then logout
|
||||
await page.getByText('ออกจากระบบ').click();
|
||||
await page.waitForTimeout(1000);
|
||||
// 5. Confirm logout dialog
|
||||
await page.locator("(//button[@type='button'])[2]").click();
|
||||
|
||||
// 6. Should redirect back to login page
|
||||
await page.waitForURL('**/login**', { timeout: 15_000 });
|
||||
|
||||
// 7. Verify the email is still pre-filled
|
||||
await expect(page.locator('input[type="email"]')).toHaveValue(TEST_ADMIN.email);
|
||||
});
|
||||
});
|
||||
146
frontend_management/tests/auth/register.spec.ts
Normal file
146
frontend_management/tests/auth/register.spec.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* Register Page Tests
|
||||
* Test instructor registration flow
|
||||
*/
|
||||
test.describe('Register Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.register);
|
||||
});
|
||||
|
||||
test('check display register form', async ({ page }) => {
|
||||
// Header
|
||||
await expect(page.getByText('ลงทะเบียนเป็นผู้สอน')).toBeVisible();
|
||||
await expect(page.getByText('สร้างบัญชีเพื่อเริ่มสร้างหลักสูตร')).toBeVisible();
|
||||
|
||||
// Form fields
|
||||
await expect(page.locator('label').filter({ hasText: 'ชื่อผู้ใช้ (Username)' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'อีเมล' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'รหัสผ่าน *' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'ยืนยันรหัสผ่าน' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'คำนำหน้า' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'ชื่อจริง' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'นามสกุล' })).toBeVisible();
|
||||
await expect(page.locator('label').filter({ hasText: 'เบอร์โทรศัพท์' })).toBeVisible();
|
||||
|
||||
// Submit button
|
||||
await expect(page.getByRole('button', { name: 'ลงทะเบียน' })).toBeVisible();
|
||||
|
||||
// Login link
|
||||
await expect(page.getByText('มีบัญชีอยู่แล้ว?')).toBeVisible();
|
||||
await expect(page.getByText('เข้าสู่ระบบ')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors for empty fields', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
await expect(page.getByText('กรุณากรอก username')).toBeVisible();
|
||||
await expect(page.getByText('กรุณากรอกอีเมล')).toBeVisible();
|
||||
await expect(page.getByText('กรุณากรอกรหัสผ่าน')).toBeVisible();
|
||||
await expect(page.getByText('กรุณายืนยันรหัสผ่าน')).toBeVisible();
|
||||
await expect(page.getByText('กรุณากรอกชื่อ')).toBeVisible();
|
||||
await expect(page.getByText('กรุณากรอกนามสกุล')).toBeVisible();
|
||||
await expect(page.getByText('กรุณากรอกเบอร์โทร')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show username min length validation', async ({ page }) => {
|
||||
const usernameInput = page.locator('label').filter({ hasText: 'ชื่อผู้ใช้ (Username)' }).locator('input');
|
||||
await usernameInput.fill('ab');
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
await expect(page.getByText('อย่างน้อย 4 ตัวอักษร')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show email format validation', async ({ page }) => {
|
||||
const emailInput = page.locator('input[type="email"]');
|
||||
await emailInput.fill('invalid-email');
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
await expect(page.getByText('รูปแบบอีเมลไม่ถูกต้อง')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show password min length validation', async ({ page }) => {
|
||||
const passwordInput = page.locator('label').filter({ hasText: 'รหัสผ่าน *' }).locator('input');
|
||||
await passwordInput.fill('1234');
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
await expect(page.getByText('อย่างน้อย 8 ตัวอักษร')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show password mismatch validation', async ({ page }) => {
|
||||
const passwordInput = page.locator('label').filter({ hasText: 'รหัสผ่าน *' }).locator('input');
|
||||
const confirmInput = page.locator('label').filter({ hasText: 'ยืนยันรหัสผ่าน' }).locator('input');
|
||||
|
||||
await passwordInput.fill('password123');
|
||||
await confirmInput.fill('differentpass');
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
await expect(page.getByText('รหัสผ่านไม่ตรงกัน')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should toggle password visibility', async ({ page }) => {
|
||||
const passwordInput = page.locator('label').filter({ hasText: 'รหัสผ่าน *' }).locator('input');
|
||||
await passwordInput.fill('test1234');
|
||||
|
||||
// Click visibility icon
|
||||
await page.locator('label').filter({ hasText: 'รหัสผ่าน *' }).locator('.q-icon').filter({ hasText: 'visibility' }).click();
|
||||
|
||||
// Both password fields should now be text type
|
||||
await expect(passwordInput).toHaveAttribute('type', 'text');
|
||||
});
|
||||
|
||||
test('should navigate to login page', async ({ page }) => {
|
||||
await page.getByText('เข้าสู่ระบบ').click();
|
||||
await page.waitForURL('**/login**', { timeout: 10_000 });
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('should register successfully with valid data', async ({ page }) => {
|
||||
const username = faker.internet.username().toLowerCase().replace(/[^a-z0-9]/g, '') + faker.string.alpha({ length: 4, casing: 'lower' });
|
||||
const email = faker.internet.email().toLowerCase();
|
||||
const password = 'Test@1234';
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
const phone = `0${faker.string.numeric(9)}`;
|
||||
|
||||
// Fill form
|
||||
const usernameInput = page.locator('label').filter({ hasText: 'ชื่อผู้ใช้ (Username)' }).locator('input');
|
||||
await usernameInput.fill(username);
|
||||
|
||||
await page.locator('input[type="email"]').fill(email);
|
||||
|
||||
const passwordInput = page.locator('label').filter({ hasText: 'รหัสผ่าน *' }).locator('input');
|
||||
await passwordInput.fill(password);
|
||||
|
||||
const confirmInput = page.locator('label').filter({ hasText: 'ยืนยันรหัสผ่าน' }).locator('input');
|
||||
await confirmInput.fill(password);
|
||||
|
||||
// Select prefix
|
||||
const prefixSelect = page.locator('label').filter({ hasText: 'คำนำหน้า' });
|
||||
await prefixSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.q-item__label').filter({ hasText: 'นาย / Mr.' }).click();
|
||||
|
||||
// Fill name
|
||||
const firstNameInput = page.locator('label').filter({ hasText: 'ชื่อจริง' }).locator('input');
|
||||
await firstNameInput.fill(firstName);
|
||||
|
||||
const lastNameInput = page.locator('label').filter({ hasText: 'นามสกุล' }).locator('input');
|
||||
await lastNameInput.fill(lastName);
|
||||
|
||||
// Fill phone
|
||||
const phoneInput = page.locator('label').filter({ hasText: 'เบอร์โทรศัพท์' }).locator('input');
|
||||
await phoneInput.fill(phone);
|
||||
|
||||
// Submit
|
||||
await page.getByRole('button', { name: 'ลงทะเบียน' }).click();
|
||||
|
||||
// Should show success notification and redirect to login
|
||||
await expect(page.locator('.q-notification')).toBeVisible({ timeout: 10_000 });
|
||||
await page.waitForURL('**/login**', { timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
});
|
||||
30
frontend_management/tests/fixtures/admin.setup.ts
vendored
Normal file
30
frontend_management/tests/fixtures/admin.setup.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { test as setup, expect } from '@playwright/test';
|
||||
import { TEST_ADMIN, TEST_URLS } from './test-data';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ADMIN_AUTH_FILE = path.resolve(__dirname, '../.auth/admin.json');
|
||||
|
||||
/**
|
||||
* Admin authentication setup
|
||||
* Logs in as admin and saves browser state (cookies) for reuse
|
||||
*/
|
||||
setup('authenticate as admin', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.login);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500);
|
||||
// Fill login form
|
||||
await page.locator('input[type="email"]').fill(TEST_ADMIN.email);
|
||||
await page.locator('input[type="password"]').fill(TEST_ADMIN.password);
|
||||
|
||||
// Submit
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Wait for redirect to admin dashboard
|
||||
await page.waitForURL('**/admin**', { timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/admin/);
|
||||
|
||||
// Save auth state
|
||||
await page.context().storageState({ path: ADMIN_AUTH_FILE });
|
||||
});
|
||||
31
frontend_management/tests/fixtures/instructor.setup.ts
vendored
Normal file
31
frontend_management/tests/fixtures/instructor.setup.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { test as setup, expect } from '@playwright/test';
|
||||
import { TEST_INSTRUCTOR, TEST_URLS } from './test-data';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const INSTRUCTOR_AUTH_FILE = path.resolve(__dirname, '../.auth/instructor.json');
|
||||
|
||||
/**
|
||||
* Instructor authentication setup
|
||||
* Logs in as instructor and saves browser state (cookies) for reuse
|
||||
*/
|
||||
setup('authenticate as instructor', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.login);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Fill login form
|
||||
await page.locator('input[type="email"]').fill(TEST_INSTRUCTOR.email);
|
||||
await page.locator('input[type="password"]').fill(TEST_INSTRUCTOR.password);
|
||||
|
||||
// Submit
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Wait for redirect to instructor dashboard
|
||||
await page.waitForURL('**/instructor**', { timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/instructor/);
|
||||
|
||||
// Save auth state
|
||||
await page.context().storageState({ path: INSTRUCTOR_AUTH_FILE });
|
||||
});
|
||||
133
frontend_management/tests/instructor/courses-list.spec.ts
Normal file
133
frontend_management/tests/instructor/courses-list.spec.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Instructor Courses List Page Tests
|
||||
* ใช้ cookies จาก instructor-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Instructor Courses List', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.instructorCourses);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should display page header', async ({ page }) => {
|
||||
await expect(page.getByText('หลักสูตรของฉัน')).toBeVisible();
|
||||
await expect(page.getByText('จัดการหลักสูตรที่คุณสร้าง')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have create course button', async ({ page }) => {
|
||||
const createBtn = page.getByRole('button', { name: /สร้างหลักสูตรใหม่/ });
|
||||
await expect(createBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate to create course page', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /สร้างหลักสูตรใหม่/ }).click();
|
||||
await page.waitForURL('**/instructor/courses/create**');
|
||||
await expect(page).toHaveURL(/\/instructor\/courses\/create/);
|
||||
});
|
||||
|
||||
test('should display stats cards', async ({ page }) => {
|
||||
await expect(page.getByText('หลักสูตรทั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('เผยแพร่แล้ว')).toBeVisible();
|
||||
await expect(page.getByText('รอตรวจสอบ')).toBeVisible();
|
||||
await expect(page.getByText('แบบร่าง')).toBeVisible();
|
||||
await expect(page.getByText('ถูกปฏิเสธ')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have search input', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="ค้นหา"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have status filter dropdown', async ({ page }) => {
|
||||
// Click the status select to open dropdown
|
||||
const statusSelect = page.locator('.q-select').first();
|
||||
await expect(statusSelect).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter by status', async ({ page }) => {
|
||||
// Open status dropdown
|
||||
await page.locator('.q-select').first().click();
|
||||
|
||||
// Select "เผยแพร่แล้ว" (APPROVED)
|
||||
await page.getByText('เผยแพร่แล้ว').click();
|
||||
|
||||
// Wait for API re-fetch
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should toggle between card and table view', async ({ page }) => {
|
||||
// Switch to table view
|
||||
await page.locator('.q-btn-toggle button').last().click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Table should appear
|
||||
await expect(page.locator('.q-table')).toBeVisible();
|
||||
|
||||
// Table should have expected columns
|
||||
await expect(page.getByText('หลักสูตร')).toBeVisible();
|
||||
await expect(page.getByText('สถานะ')).toBeVisible();
|
||||
await expect(page.getByText('ราคา')).toBeVisible();
|
||||
|
||||
// Switch back to card view
|
||||
await page.locator('.q-btn-toggle button').first().click();
|
||||
await page.waitForTimeout(300);
|
||||
});
|
||||
|
||||
test('should show course cards with status badges', async ({ page }) => {
|
||||
// Wait for courses to load (either card or empty state)
|
||||
const hasCards = await page.locator('.q-badge').first().isVisible().catch(() => false);
|
||||
const isEmpty = await page.getByText('ยังไม่มีหลักสูตร').isVisible().catch(() => false);
|
||||
|
||||
// Either courses exist with badges or empty state shows
|
||||
expect(hasCards || isEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show course action menu', async ({ page }) => {
|
||||
const moreBtn = page.locator('button:has(.q-icon[class*="more_vert"])').first();
|
||||
const hasCourses = await moreBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasCourses) {
|
||||
await moreBtn.click();
|
||||
|
||||
// Menu should show duplicate and delete options
|
||||
await expect(page.getByText('ทำสำเนา')).toBeVisible();
|
||||
await expect(page.getByText('ลบ')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should open clone dialog from menu', async ({ page }) => {
|
||||
const moreBtn = page.locator('button:has(.q-icon[class*="more_vert"])').first();
|
||||
const hasCourses = await moreBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasCourses) {
|
||||
await moreBtn.click();
|
||||
await page.getByText('ทำสำเนา').click();
|
||||
|
||||
// Clone dialog should appear
|
||||
await expect(page.getByText('ทำสำเนาหลักสูตร')).toBeVisible();
|
||||
await expect(page.locator('.q-dialog input').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle rejected course view details', async ({ page }) => {
|
||||
// Filter by rejected status
|
||||
await page.locator('.q-select').first().click();
|
||||
await page.getByText('ถูกปฏิเสธ').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// If there are rejected courses, clicking view should show rejection dialog
|
||||
const viewBtn = page.locator('button:has(.q-icon[class*="visibility"])').first();
|
||||
const hasRejected = await viewBtn.isVisible().catch(() => false);
|
||||
|
||||
if (hasRejected) {
|
||||
await viewBtn.click();
|
||||
|
||||
// Rejection dialog should appear
|
||||
await expect(page.getByText('หลักสูตรถูกปฏิเสธ')).toBeVisible();
|
||||
await expect(page.getByText('เหตุผลการปฏิเสธ')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /คืนสถานะเป็นแบบร่าง/ })).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
67
frontend_management/tests/instructor/dashboard.spec.ts
Normal file
67
frontend_management/tests/instructor/dashboard.spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_URLS } from '../fixtures/test-data';
|
||||
|
||||
/**
|
||||
* Instructor Dashboard Tests
|
||||
* ใช้ cookies จาก instructor-setup project (ไม่ต้อง login ซ้ำ)
|
||||
*/
|
||||
test.describe('Instructor Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(TEST_URLS.instructorDashboard);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should display welcome message', async ({ page }) => {
|
||||
await expect(page.locator('h1')).toContainText('สวัสดี');
|
||||
});
|
||||
|
||||
test('should display stats cards', async ({ page }) => {
|
||||
await expect(page.getByText('หลักสูตรทั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('ผู้เรียนทั้งหมด')).toBeVisible();
|
||||
await expect(page.getByText('เรียนจบแล้ว')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display course status breakdown', async ({ page }) => {
|
||||
await expect(page.getByText('สถานะหลักสูตร')).toBeVisible();
|
||||
await expect(page.getByText('เผยแพร่แล้ว')).toBeVisible();
|
||||
await expect(page.getByText('รอตรวจสอบ')).toBeVisible();
|
||||
await expect(page.getByText('แบบร่าง')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display recent courses section', async ({ page }) => {
|
||||
await expect(page.getByText('หลักสูตร')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'ดูทั้งหมด' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate to courses list', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'ดูทั้งหมด' }).click();
|
||||
await page.waitForURL('**/instructor/courses**');
|
||||
await expect(page).toHaveURL(/\/instructor\/courses/);
|
||||
});
|
||||
|
||||
test('should show user menu on avatar click', async ({ page }) => {
|
||||
await page.locator('.w-12.h-12.rounded-full').click();
|
||||
|
||||
await expect(page.getByText('โปรไฟล์')).toBeVisible();
|
||||
await expect(page.getByText('ออกจากระบบ')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate to profile', async ({ page }) => {
|
||||
await page.locator('.w-12.h-12.rounded-full').click();
|
||||
await page.getByText('โปรไฟล์').click();
|
||||
|
||||
await page.waitForURL('**/instructor/profile**');
|
||||
await expect(page).toHaveURL(/\/instructor\/profile/);
|
||||
});
|
||||
|
||||
test('should logout and redirect to login', async ({ page }) => {
|
||||
await page.locator('.w-12.h-12.rounded-full').click();
|
||||
await page.getByText('ออกจากระบบ').click();
|
||||
|
||||
// Confirm logout dialog
|
||||
await page.locator('.q-dialog').getByText('ออกจากระบบ').click();
|
||||
|
||||
await page.waitForURL('**/login**', { timeout: 10_000 });
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue