-
-
Save dtinth/835d611ee3fc132d2f22decfa067bb69 to your computer and use it in GitHub Desktop.
litelms automated test suite
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { expect, test } from '@playwright/test' | |
| import { assignExamToUserByExamGroup } from '../src/backend/assignExamToUserByExamGroup' | |
| import { supabaseAdmin } from '../src/backend/supabaseAdmin' | |
| import { AppTester } from './lib/AppTester' | |
| test('auth', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| expect(user.email).toBeDefined() | |
| }) | |
| test('profile management', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| // Fill out profile form | |
| await page.getByRole('combobox', { name: 'คำนำหน้า' }).click() | |
| await page.getByRole('option', { name: 'นาย' }).click() | |
| await page.getByRole('textbox', { name: 'ชื่อ - นามสกุล' }).fill('ทดสอบ ระบบ') | |
| await page.getByRole('textbox', { name: 'ตำแหน่ง' }).fill('Software Developer') | |
| await page.getByRole('textbox', { name: 'สถานที่ทำงาน' }).fill('Test Company Ltd.') | |
| await page.getByRole('combobox', { name: 'สังกัด' }).click() | |
| await page.getByRole('option', { name: 'สอศ.' }).click() | |
| await page.getByRole('combobox', { name: 'จังหวัด' }).click() | |
| await page.getByRole('option', { name: 'กรุงเทพมหานคร' }).click() | |
| // Email should be readonly and populated | |
| const emailField = page.getByRole('textbox', { name: 'อีเมล' }) | |
| await expect(emailField).toHaveValue(user.email) | |
| await expect(emailField).toHaveAttribute('readonly') | |
| // Check privacy consent checkbox | |
| await page.getByRole('checkbox', { name: 'ข้าพเจ้ายินยอม' }).click() | |
| // Save the profile | |
| await page.getByRole('button', { name: 'บันทึกข้อมูล' }).click() | |
| // Should see success notification | |
| await expect( | |
| page.getByRole('alert').filter({ hasText: 'บันทึกข้อมูลเรียบร้อยแล้ว' }), | |
| ).toBeVisible() | |
| // Refresh page and verify data persists | |
| await page.reload() | |
| await expect(page.getByRole('combobox', { name: 'คำนำหน้า' })).toHaveText('นาย') | |
| await expect(page.getByRole('textbox', { name: 'ชื่อ - นามสกุล' })).toHaveValue('ทดสอบ ระบบ') | |
| await expect(page.getByRole('textbox', { name: 'ตำแหน่ง' })).toHaveValue('Software Developer') | |
| await expect(page.getByRole('textbox', { name: 'สถานที่ทำงาน' })).toHaveValue('Test Company Ltd.') | |
| }) | |
| test('class registration', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| await app.initializeProfile(user) | |
| await app.selectMenu('บทเรียน') | |
| await page | |
| .getByRole('cell', { name: 'Test Unit 2' }) | |
| .getByRole('button', { name: 'ลงทะเบียน' }) | |
| .click() | |
| await page | |
| .getByRole('cell', { name: 'Test Unit 2' }) | |
| .getByRole('button', { name: 'เข้าเรียน' }) | |
| .click() | |
| const dialog = page.getByRole('dialog').filter({ hasText: 'Test Unit 2' }).first() | |
| await expect(dialog).toBeVisible() | |
| // Expect for another page to open after clicking "ลงชื่อเข้าเรียน" | |
| await page.getByRole('button', { name: 'ลงชื่อเข้าเรียน' }).click() | |
| await expect(page.getByRole('button', { name: 'เข้าห้องเรียน' })).toBeVisible() | |
| await page.getByRole('button', { name: 'บันทึกเวลาเรียน' }).click() | |
| await page.getByRole('textbox', { name: 'รหัสสำหรับบันทึกเวลาเรียน' }).fill('WRONG') | |
| await page | |
| .getByRole('dialog') | |
| .filter({ has: page.getByRole('textbox', { name: 'รหัสสำหรับบันทึกเวลาเรียน' }) }) | |
| .getByRole('button', { name: 'บันทึกเวลาเรียน' }) | |
| .click() | |
| await expect(page.getByText('Invalid attendance code').first()).toBeVisible() | |
| await page.getByRole('textbox', { name: 'รหัสสำหรับบันทึกเวลาเรียน' }).fill('VERYGOOD') | |
| await page | |
| .getByRole('dialog') | |
| .filter({ has: page.getByRole('textbox', { name: 'รหัสสำหรับบันทึกเวลาเรียน' }) }) | |
| .getByRole('button', { name: 'บันทึกเวลาเรียน' }) | |
| .click() | |
| }) | |
| test('exam available', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| const examGroupId = await supabaseAdmin | |
| .from('exam_groups') | |
| .select('id') | |
| .eq('name', 'Test Exam 1') | |
| .then((r) => r.data?.[0]?.id) | |
| expect(examGroupId).toBeDefined() | |
| await assignExamToUserByExamGroup({ | |
| userId: user.userId!, | |
| examGroupId: examGroupId!, | |
| referenceId: 'e2e_' + user.userId!, | |
| }) | |
| await app.initializeProfile(user) | |
| await app.selectMenu('แบบทดสอบ') | |
| }) | |
| test('taking an exam', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| const examGroupId = await supabaseAdmin | |
| .from('exam_groups') | |
| .select('id') | |
| .eq('name', 'Test Exam 1') | |
| .then((r) => r.data?.[0]?.id) | |
| expect(examGroupId).toBeDefined() | |
| await assignExamToUserByExamGroup({ | |
| userId: user.userId!, | |
| examGroupId: examGroupId!, | |
| referenceId: 'e2e_' + user.userId!, | |
| }) | |
| await app.initializeProfile(user) | |
| await app.selectMenu('แบบทดสอบ') | |
| await app.autoDismissAlerts() | |
| await app.takeExam() | |
| await app.answerAllQuestions() | |
| await app.retakeExam() | |
| await app.answerAllQuestions() | |
| await app.retakeExam() | |
| await app.answerAllQuestions() | |
| }) | |
| test('resume an exam', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| const examGroupId = await supabaseAdmin | |
| .from('exam_groups') | |
| .select('id') | |
| .eq('name', 'Test Exam 1') | |
| .then((r) => r.data?.[0]?.id) | |
| expect(examGroupId).toBeDefined() | |
| await assignExamToUserByExamGroup({ | |
| userId: user.userId!, | |
| examGroupId: examGroupId!, | |
| referenceId: 'e2e_' + user.userId!, | |
| }) | |
| await app.initializeProfile(user) | |
| await app.selectMenu('แบบทดสอบ') | |
| await app.autoDismissAlerts() | |
| await app.takeExam() | |
| await app.answerQuestions(['a', 'b', 'c', 'd']) | |
| await expect(page.locator('[data-saved-answers-count]')).toHaveAttribute( | |
| 'data-saved-answers-count', | |
| '4', | |
| ) | |
| await page.goto('/dashboard/exams') | |
| await app.resumeExam() | |
| await app.expectSavedAnswers(['a', 'b', 'c', 'd']) | |
| }) | |
| test('exam time running out', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| const examGroupId = await supabaseAdmin | |
| .from('exam_groups') | |
| .select('id') | |
| .eq('name', 'Test Exam 1') | |
| .then((r) => r.data?.[0]?.id) | |
| expect(examGroupId).toBeDefined() | |
| const { assignmentId } = await assignExamToUserByExamGroup({ | |
| userId: user.userId!, | |
| examGroupId: examGroupId!, | |
| referenceId: 'e2e_' + user.userId!, | |
| }) | |
| await app.initializeProfile(user) | |
| await app.selectMenu('แบบทดสอบ') | |
| await app.autoDismissAlerts() | |
| await app.takeExam() | |
| await app.answerQuestions(['a', 'b', 'c', 'd']) | |
| await expect(page.locator('[data-saved-answers-count]')).toHaveAttribute( | |
| 'data-saved-answers-count', | |
| '4', | |
| ) | |
| const attemptId = (await page | |
| .locator('[data-exam-attempt-id]') | |
| .getAttribute('data-exam-attempt-id'))! | |
| await supabaseAdmin | |
| .from('exam_attempts') | |
| .update({ expires_at: new Date().toISOString() }) | |
| .eq('id', attemptId) | |
| await page.reload() | |
| await expect(page.getByText('คะแนนที่ได้').first()).toBeVisible() | |
| // Set assignment to expire immediately and verify cannot retake | |
| await supabaseAdmin | |
| .from('user_exam_assignments') | |
| .update({ expires_at: new Date().toISOString() }) | |
| .eq('id', assignmentId) | |
| await page.reload() | |
| }) | |
| test('real exam', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| const user = await app.signIn() | |
| const examGroupId = await supabaseAdmin | |
| .from('exam_groups') | |
| .select('id') | |
| .eq('name', 'B') | |
| .then((r) => r.data?.[0]?.id) | |
| expect(examGroupId).toBeDefined() | |
| await assignExamToUserByExamGroup({ | |
| userId: user.userId!, | |
| examGroupId: examGroupId!, | |
| referenceId: 'e2e_' + user.userId!, | |
| }) | |
| await app.initializeProfile(user) | |
| await app.selectMenu('แบบทดสอบ') | |
| }) | |
| test('admin setting class time', async ({ page, request }) => { | |
| const app = new AppTester(page, request) | |
| await app.signIn({ email: 'admin@localhost' }) | |
| await page.goto('/backoffice/classes') | |
| await page.getByRole('link', { name: 'test-1' }).click() | |
| await page | |
| .getByRole('button', { name: 'ตั้งค่าระยะเวลาเช็คอิน' }) | |
| .or(page.getByRole('button', { name: 'แก้ไขระยะเวลา' })) | |
| .click() | |
| await page.getByRole('textbox', { name: 'วันที่เริ่มเช็คอิน' }).fill('2025-01-01') | |
| await page.getByRole('textbox', { name: 'เวลาเริ่มเช็คอิน' }).fill('01:23') | |
| await page.getByRole('textbox', { name: 'วันที่สิ้นสุดเช็คอิน' }).fill('2026-01-01') | |
| await page.getByRole('textbox', { name: 'เวลาสิ้นสุดเช็คอิน' }).fill('02:34') | |
| await page.getByRole('textbox', { name: 'ลิงก์ประชุม Teams' }).fill('https://teams.microsoft.com') | |
| await page.getByRole('button', { name: 'บันทึก' }).click() | |
| await expect(page.getByText('เริ่ม: 1 ม.ค. 2568 01:23').first()).toBeVisible() | |
| await expect(page.getByText('สิ้นสุด: 1 ม.ค. 2569 02:34').first()).toBeVisible() | |
| await page.getByRole('button', { name: 'แก้ไขระยะเวลา' }).click() | |
| await expect(page.getByRole('textbox', { name: 'วันที่เริ่มเช็คอิน' })).toHaveValue('2025-01-01') | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { APIRequestContext, expect, Page } from '@playwright/test' | |
| import { createHash } from 'node:crypto' | |
| export class AppTester { | |
| constructor( | |
| public page: Page, | |
| public request: APIRequestContext, | |
| ) {} | |
| async signInAsAdmin() { | |
| return this.signIn({ email: 'admin@example.com' }) | |
| } | |
| async signIn(options: { email?: string } = {}) { | |
| const { page, request } = this | |
| await page.goto('/') | |
| await page.getByRole('button', { name: 'เข้าสู่ระบบ / สร้างบัญชี' }).click() | |
| await page.getByRole('textbox', { name: 'กรอกที่อยู่อีเมลของคุณ' }).click() | |
| const email = options.email || 'u' + Date.now() + '@example.com' | |
| await page.getByRole('textbox', { name: 'กรอกที่อยู่อีเมลของคุณ' }).fill(email) | |
| await page.getByRole('button', { name: 'ส่งรหัส OTP' }).click() | |
| if (options.email) { | |
| await page.waitForTimeout(1000) // wait a bit for the email to arrive | |
| } | |
| // Check http://localhost:54324/api/v1/messages?limit=50 to get the code | |
| let otp = '' | |
| await expect(async () => { | |
| const response = await request.get('http://localhost:54324/api/v1/messages?limit=50') | |
| const body = (await response.json()) as { | |
| messages: { Snippet: string; To: { Address: string }[] }[] | |
| } | |
| const messages = body.messages.filter((m) => m.To.find((t) => t.Address === email)) | |
| expect(messages.length).toBeGreaterThan(0) | |
| otp = messages[0].Snippet.match(/code: (\d{6})/)?.[1] || '' | |
| expect(otp).toHaveLength(6) | |
| }).toPass() | |
| await page.getByRole('textbox', { name: 'กรอกรหัส 6 หลัก' }).fill(otp) | |
| await page.getByRole('button', { name: 'ยืนยันรหัส' }).click() | |
| await expect(page).toHaveURL('/dashboard/home') | |
| await this.closeAllNotifications() | |
| await this.selectMenu('ข้อมูลส่วนตัว') | |
| await expect(page.locator('form[data-user-id]')).toBeVisible() | |
| const userId = await page.locator('form[data-user-id]').getAttribute('data-user-id') | |
| expect(userId).toBeTruthy() | |
| return { email, userId } | |
| } | |
| async closeAllNotifications() { | |
| const { page } = this | |
| await page | |
| .getByRole('alert') | |
| .getByRole('button', { name: 'Close' }) | |
| .evaluateAll((buttons) => { | |
| buttons.forEach((btn) => btn.click()) | |
| }) | |
| } | |
| async initializeProfile(user: { email: string }) { | |
| const { page } = this | |
| await this.selectMenu('ข้อมูลส่วนตัว') | |
| await expect(page).toHaveURL('/dashboard/profile') | |
| const hash = createHash('md5').update(user.email).digest('hex') | |
| const prefixOptions = ['นาย', 'นาง', 'นางสาว', 'ศาสตราจารย์'] | |
| const prefix = prefixOptions[parseInt(hash.slice(0, 2), 16) % prefixOptions.length] | |
| // Fill out profile form | |
| await page.getByRole('combobox', { name: 'คำนำหน้า' }).click() | |
| await page.getByRole('option', { name: prefix }).first().click() | |
| await page.getByRole('textbox', { name: 'ชื่อ - นามสกุล' }).fill('ทดสอบ ระบบ') | |
| await page.getByRole('textbox', { name: 'ตำแหน่ง' }).fill('Software Developer') | |
| await page.getByRole('textbox', { name: 'สถานที่ทำงาน' }).fill('Test Company Ltd.') | |
| await page.getByRole('combobox', { name: 'สังกัด' }).click() | |
| await page.getByRole('option', { name: 'สอศ.' }).click() | |
| await page.getByRole('combobox', { name: 'จังหวัด' }).click() | |
| await page.getByRole('option', { name: 'กรุงเทพมหานคร' }).click() | |
| await page.getByRole('checkbox', { name: 'ข้าพเจ้ายินยอม' }).click() | |
| await page.getByRole('button', { name: 'บันทึกข้อมูล' }).click() | |
| await expect( | |
| page.getByRole('alert').filter({ hasText: 'บันทึกข้อมูลเรียบร้อยแล้ว' }), | |
| ).toBeVisible() | |
| } | |
| async selectMenu(menuName: 'บทเรียน' | 'แบบทดสอบ' | 'ข้อมูลส่วนตัว') { | |
| await this.page.getByRole('navigation').getByRole('button', { name: menuName }).click() | |
| } | |
| async autoDismissAlerts() { | |
| await this.page.addLocatorHandler( | |
| this.page.getByRole('alert').getByRole('button', { name: 'Close' }), | |
| (button) => button.click(), | |
| ) | |
| } | |
| async takeExam() { | |
| await this.page.getByRole('button', { name: 'เริ่มสอบ' }).click() | |
| await expect(this.page.getByRole('heading', { name: 'ทำข้อสอบ' })).toBeVisible() | |
| } | |
| async resumeExam() { | |
| await this.page.getByRole('button', { name: 'ทำต่อ' }).click() | |
| await expect(this.page.getByRole('heading', { name: 'ทำข้อสอบ' })).toBeVisible() | |
| } | |
| async retakeExam() { | |
| await this.page.getByRole('button', { name: 'ทำอีกครั้ง' }).click() | |
| await expect(this.page.getByRole('heading', { name: 'ทำข้อสอบ' })).toBeVisible() | |
| } | |
| async answerAllQuestions() { | |
| const questionCount = await this.page | |
| .locator('[data-total-questions]') | |
| .getAttribute('data-total-questions') | |
| await this.answerQuestions(Array(parseInt(questionCount || '0')).fill('b')) | |
| } | |
| async answerQuestions(answers: ('a' | 'b' | 'c' | 'd')[]) { | |
| for (let i = 0; i < answers.length; i++) { | |
| await expect(this.page.locator('[data-current-question-index]')).toHaveAttribute( | |
| 'data-current-question-index', | |
| String(i), | |
| ) | |
| await this.page.locator(`[data-choice-key="${answers[i]}"]`).click() | |
| await this.page | |
| .getByRole('button', { name: 'ส่งคำตอบ' }) | |
| .or(this.page.getByRole('button', { name: 'ถัดไป' })) | |
| .first() | |
| .click() | |
| } | |
| } | |
| async expectSavedAnswers(answers: ('a' | 'b' | 'c' | 'd')[]) { | |
| for (let i = 0; i < answers.length; i++) { | |
| await expect(this.page.locator('[data-current-question-index]')).toHaveAttribute( | |
| 'data-current-question-index', | |
| String(i), | |
| ) | |
| await expect(this.page.locator(`[data-choice-key="${answers[i]}"]`)).toBeChecked() | |
| await this.page | |
| .getByRole('button', { name: 'ส่งคำตอบ' }) | |
| .or(this.page.getByRole('button', { name: 'ถัดไป' })) | |
| .first() | |
| .click() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment