บทที่ 11 · Part 3 — UI and API Capabilities
UI Workflows
ทดสอบ form, dialog, table, upload และ download ด้วย journey ที่สั้นและ assert ผลลัพธ์ที่ผู้ใช้เห็น
UI Workflows
Browser test มีค่าเมื่อมันพิสูจน์ interaction ที่ API test มองไม่เห็น เช่น dialog focus, inline validation, download filename หรือผลลัพธ์ที่ผู้ใช้เห็นหลัง refresh แต่ flow ที่ยาวเกินไปจะทำให้ defect หนึ่งบังอีก 5 ส่วน บทนี้จึงสร้าง journey ที่สั้นและมี precondition ผ่าน API
จบบทนี้คุณจะ
ทดสอบ form, dialog, table, upload, download และ multi-page flow ด้วย semantic locators แยก setup ออกจาก UI behavior และเลือก component test หรือ E2E ตาม boundary ที่ต้องพิสูจน์
Scenario: Organizer สร้าง Workshop
Fixture สร้าง organizer session ไว้แล้ว Test นี้จึงเริ่มที่ form และพิสูจน์ UI contract:
test('WORKSHOP-CREATE-001 - publishes a workshop with valid details', async ({
organizerPage,
}) => {
const input = aWorkshop({
title: 'Reliable Browser Tests',
capacity: 24,
})
await test.step('Open the new workshop form', async () => {
await organizerPage.goto('/organizer/workshops')
await organizerPage.getByRole('button', { name: 'New workshop' }).click()
await expect(
organizerPage.getByRole('dialog', { name: 'New workshop' }),
).toBeVisible()
})
await test.step('Enter valid details and publish', async () => {
const dialog = organizerPage.getByRole('dialog', { name: 'New workshop' })
await dialog.getByRole('textbox', { name: 'Title' }).fill(input.title)
await dialog.getByLabel('Start time').fill('2030-06-15T09:00')
await dialog.getByRole('spinbutton', { name: 'Capacity' }).fill(String(input.capacity))
await dialog.getByRole('button', { name: 'Publish workshop' }).click()
})
await test.step('Show the published workshop in the organizer list', async () => {
await expect(organizerPage.getByRole('dialog')).toHaveCount(0)
const row = organizerPage.getByRole('row').filter({ hasText: input.title })
await expect(row.getByRole('cell', { name: 'Open' })).toBeVisible()
await expect(row.getByText('24 seats')).toBeVisible()
})
})
Input date ตัวอย่างเป็น course contract; application จริงต้องกำหนด timezone/locale ให้ชัด Test assert output ของ action ไม่ assert ว่าทุก keystroke เกิดขึ้น Page object เหมาะเมื่อ form interaction นี้ถูก reuse
Validation Test ไม่ควร Submit ได้สำเร็จ
test('WORKSHOP-CREATE-010 - identifies every missing required field', async ({
organizerPage,
}) => {
await organizerPage.goto('/organizer/workshops/new')
await organizerPage.getByRole('button', { name: 'Publish workshop' }).click()
const summary = organizerPage.getByRole('alert', { name: 'Form errors' })
await expect(summary).toContainText('Title is required')
await expect(summary).toContainText('Start time is required')
await expect(organizerPage.getByLabel('Title')).toHaveAttribute('aria-invalid', 'true')
await expect(organizerPage.getByLabel('Title')).toBeFocused()
})
ถ้า requirement ระบุ focus ไป error แรก ให้ assert focus ด้วย Negative API validation tests แยกตรวจ status/code/schema เพราะ browser native validation หรือ frontend validation อาจกัน request ก่อนถึง server
Table Interaction ต้อง Scope ด้วย Row Identity
const workshopRow = page
.getByRole('row')
.filter({ has: page.getByRole('cell', { name: 'TypeScript Clinic' }) })
await workshopRow.getByRole('button', { name: 'More actions' }).click()
await page.getByRole('menuitem', { name: 'Close registration' }).click()
const dialog = page.getByRole('dialog', { name: 'Close registration' })
await expect(dialog).toContainText('TypeScript Clinic')
await dialog.getByRole('button', { name: 'Confirm close' }).click()
await expect(workshopRow.getByRole('cell', { name: 'Closed' })).toBeVisible()
Row scope ทำให้ test ไม่ขึ้นกับ sort/order Menu/dialog ใช้ role ตาม interaction model และ final assertion กลับไปที่ row เดิม หาก dialog title ซ้ำให้ scope exact accessible name
File Upload
Organizer อัปโหลด outline เป็น PDF โดย input มี label:
await page.getByLabel('Workshop outline').setInputFiles({
name: 'outline.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('%PDF-1.4 synthetic test file'),
})
await expect(page.getByRole('status')).toContainText('outline.pdf ready')
Synthetic buffer ทำให้ test ไม่พึ่ง path เครื่อง หาก parser ต้องการ PDF valid จริง ให้เก็บ fixture เล็กที่ review ได้ และไม่มีข้อมูลจริง Test แยก size/type rejection ผ่าน UI; malware scanning/business pipeline ต้องมี integration seam ที่เหมาะกว่า browser อย่างเดียว
Download ต้องเริ่ม Wait ก่อน Click
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Export attendees' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('attendees.csv')
const failure = await download.failure()
expect(failure).toBeNull()
ถ้าต้องตรวจ content ให้อ่าน stream/path ใน temporary output และ parse เฉพาะ contract เช่น headers กับ exact workshop ids ไม่ snapshot CSV ที่มี timestamp dynamic ทั้งไฟล์ ห้ามเขียนทับ path กว้างหรือ commit downloaded data
Popup และ Navigation
const helpPagePromise = page.waitForEvent('popup')
await page.getByRole('link', { name: 'Open workshop guide' }).click()
const helpPage = await helpPagePromise
await expect(helpPage).toHaveURL(/\/guides\/workshops$/)
await expect(helpPage.getByRole('heading', { name: 'Workshop guide' })).toBeVisible()
เหมือน download ต้องตั้ง listener ก่อน action เพื่อไม่พลาด event แต่ถ้า link navigation อยู่ tab เดิม
ใช้ toHaveURL/heading assertion โดยไม่รอ networkidle เป็น default เพราะ background polling อาจไม่เคย idle
Component Testing ใน Playwright 1.62
Playwright 1.62 มี Component Testing model แบบ stable ที่ใช้ story/gallery page ของ application และ built-in
mount จาก @playwright/test แทน legacy experimental framework packages เหมาะกับ component interaction/layout
ที่ต้อง real browser เช่น Reservation dialog หลาย state โดยไม่ต้อง boot routing/auth/backend เต็มระบบ
ใช้ component test เมื่อ claim อยู่ใน component boundary และ story สร้าง props/state ได้ตรง ใช้ E2E เมื่อ claim ต้อง routing, session, server authorization หรือ end-to-end integration อย่าทำ assertion เดียวซ้ำทุก layer Callback/live object ข้าม Node/browser boundary ไม่ได้ ให้ story ทำ observable result ใน DOM แล้ว assert ผ่าน Locator ดู current component testing model และ migration notes ก่อนย้าย suite เก่า
UI Flow Review Questions
ก่อนเพิ่ม browser test ถามว่า:
- UI-specific evidence คืออะไร: focus, accessible state, navigation, rendered feedback หรือ file behavior?
- Precondition สร้างผ่าน API/fixture ได้หรือไม่?
- Test จบที่ observable result ใด?
- Failure ต้องใช้ trace/network/attachment อะไรจึงวิเคราะห์ได้?
- Scenario นี้ต้องทุก browser/device หรือเพียง project เป้าหมาย?
รายการตรวจสอบ
- Journey สั้นและ precondition ไม่ทำผ่าน UI โดยไม่จำเป็น
- Form/dialog/table ใช้ roles, labels และ scoped locators
- Validation ตรวจ message, field state และ focus ตาม requirement
- Upload ใช้ synthetic fixture; download/popup listener เริ่มก่อน action
- Component test ใช้เฉพาะ component boundary; E2E พิสูจน์ integration
- Assertion ปลายทางตรวจ state ที่ผู้ใช้ใช้ตัดสินใจ
สรุปบทนี้
UI test ที่ดีใช้ browser เพื่อพิสูจน์สิ่งที่ browser เท่านั้นให้หลักฐานได้ API fixtures ตัด setup ที่ไม่ใช่เรื่อง ของ test ส่วน semantic scope และ event-first waiting ทำให้ flow อ่านได้และไม่แข่งกับ runtime