Skip to content

Instantly share code, notes, and snippets.

@dtinth
Last active October 19, 2025 10:29
Show Gist options
  • Select an option

  • Save dtinth/835d611ee3fc132d2f22decfa067bb69 to your computer and use it in GitHub Desktop.

Select an option

Save dtinth/835d611ee3fc132d2f22decfa067bb69 to your computer and use it in GitHub Desktop.
litelms automated test suite
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')
})
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