บทที่ 14 · Part 3 — UI and API Capabilities
Accessibility, Visual and Emulation
ผสาน accessibility checks, screenshot assertions และ device emulation โดยรู้ขอบเขตของแต่ละหลักฐาน
Accessibility, Visual and Emulation
Functional test ที่คลิก Reserve ผ่าน mouse ได้อาจผ่านทั้งที่ keyboard focus เข้า dialog ไม่ได้ ส่วน screenshot ที่เหมือน baseline อาจยังไม่มี accessible name เครื่องมือ 3 กลุ่ม—semantic interaction, automated accessibility scan และ visual comparison—ให้หลักฐานคนละชนิดและแทนกันไม่ได้
จบบทนี้คุณจะ
ทดสอบ keyboard/ARIA semantics ใช้ axe และ ARIA snapshots อย่างมีขอบเขต สร้าง visual baseline ที่ review ได้ และเลือก browser/device/locale emulation ตาม product risk โดยไม่อ้างว่าเป็น real device
Accessibility Evidence 3 ชั้น
- Semantic interaction — role, accessible name, keyboard, focus, state
- Automated rules — axe scan หา violation ที่ rule engine ตรวจได้
- Manual evaluation — keyboard-only, zoom/reflow, screen reader และความเข้าใจ content
Playwright ระบุชัดว่า automated accessibility testing ตรวจพบปัญหาได้เพียงบางส่วน W3C ก็ต้องใช้ human judgement จึงห้ามสรุปว่า “ผ่าน WCAG” จาก scan สีเขียว ให้ report ว่า rules/scope/version ใดไม่พบ violation
Keyboard และ Dialog Semantics
test('A11Y-RESERVE-001 - keeps keyboard focus inside the reservation dialog', async ({ page }) => {
await page.goto(`/workshops/${workshop.slug}`)
await page.getByRole('button', { name: 'Reserve seat' }).focus()
await page.keyboard.press('Enter')
const dialog = page.getByRole('dialog', { name: 'Confirm reservation' })
await expect(dialog).toBeVisible()
await expect(dialog.getByRole('button', { name: 'Confirm' })).toBeFocused()
await page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Reserve seat' })).toBeFocused()
})
Focus target และ Escape behavior ต้องมาจาก component/product contract กับ ARIA pattern ที่ใช้ ไม่ใช่ universal assumption ทุก dialog ดู WAI-ARIA dialog pattern
Automated Scan ด้วย axe
ติดตั้ง @axe-core/playwright แล้ว scope หน้า/state ที่รู้ว่าโหลดเสร็จ:
import AxeBuilder from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
test('A11Y-CATALOG-001 - has no automatically detectable critical violations', async ({
page,
}) => {
await page.goto('/workshops')
await expect(page.getByRole('heading', { name: 'Workshops' })).toBeVisible()
const results = await new AxeBuilder({ page })
.include('main')
.analyze()
expect(results.violations).toEqual([])
})
ใน suite จริงอาจ filter tags/rules ตาม standard scope ที่เจ้าของ accessibility กำหนด Exception ต้องมี rule, element, reason, owner และ review date ไม่ disable rule ทั่ว suite เพื่อให้ build เขียว Scan หน้า initial state ไม่ครอบ dialog/error/menu ที่ยังไม่เปิด ให้สร้าง tests ต่อ important state
ARIA Snapshot
await expect(page.getByRole('navigation', { name: 'Workshop filters' }))
.toMatchAriaSnapshot(`
- navigation "Workshop filters":
- searchbox "Search workshops"
- combobox "Level"
- button "Apply filters"
`)
ARIA snapshot ตรวจ accessibility tree structure เหมาะกับ landmark/menu/form ที่ stable ใช้ partial snapshot เล็ก แทน snapshot ทั้งหน้าเพื่อลด approval โดยไม่อ่าน Playwright 1.62.1 แก้ regression ของ ARIA snapshot จึงควร pin patch ล่าสุดเมื่อพึ่ง feature นี้
Visual Assertions
const card = page.getByRole('article').filter({ hasText: workshop.title })
await expect(card).toHaveScreenshot('full-workshop-card.webp', {
animations: 'disabled',
mask: [card.getByTestId('live-view-count')],
maxDiffPixels: 20,
})
Playwright 1.62 รองรับ lossless WebP golden ผ่านนามสกุล .webp PNG ก็ใช้ได้ Baseline ต้องสร้างและรันบน OS,
browser version, fonts, headless mode และ environment เดียวกัน Fix data, clock, locale, viewport และ animation ก่อน
กำหนด threshold ค่า 20 เป็น illustrative ต้องวัด variance ของ component จริง ไม่เพิ่มจน defect หาย
ใช้ screenshot กับ region ที่ layout/visual state เป็น contract เช่น seat badge, overflow, focus ring หรือ responsive dialog ไม่ snapshot ทุกหน้า Baseline update ต้อง review expected/actual/diff และ commit พร้อมเหตุผล:
npx playwright test tests/ui/visual --update-snapshots
ห้ามให้ CI update snapshot อัตโนมัติ Mask เฉพาะ dynamic region ที่ไม่ใช่เรื่องของ test หาก mask ครึ่งหน้า visual assertion ไม่เหลือหลักฐาน
Device และ Environment Projects
projects: [
{
name: 'desktop-chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile-webkit',
use: { ...devices['iPhone 15'], colorScheme: 'light' },
},
{
name: 'thai-dark-chromium',
use: {
...devices['Desktop Chrome'],
locale: 'th-TH',
timezoneId: 'Asia/Bangkok',
colorScheme: 'dark',
},
},
]
Device descriptor จำลอง user agent, screen/viewport และ touch Options จำลอง locale, timezone, geolocation, permissions, media และ color scheme ได้ แต่มันไม่ใช่เครื่องจริง จึงไม่พิสูจน์ virtual keyboard, browser shell, memory pressure, hardware หรือ OS integration ให้เพิ่ม real-device smoke เมื่อ risk เหล่านั้นสำคัญ
Matrix ไม่ควรเป็น desktop browsers × mobile profiles × locales × themes สำหรับทุก test เลือก scenario ที่ behavior ต่างจริง เช่น responsive navigation รัน desktop/mobile, localization layout รัน locale เป้าหมาย ส่วน API contract ไม่ต้อง multiply ตาม browser
Visual กับ Functional แยก Failure Intent
Functional assertion บอกว่า button มี/กดได้ Visual assertion บอก layout/pixels ตรง baseline Accessibility assertion บอก semantic rule/tree บางส่วน หากรวมทุกอย่างใน test เดียว failure triage ยาก แยก tags/suites ได้:
@smoke critical functional journey
@a11y semantic interaction and automated rules
@visual focused screenshot baselines
@mobile responsive behaviors
Tags เป็น course convention ให้สร้างเฉพาะเมื่อ CI/filter ใช้จริง
รายการตรวจสอบ
- Keyboard/focus/state tests มาจาก explicit interaction contract
- Axe scan ระบุ scope/state และไม่เคลม compliance ทั้งหมด
- ARIA snapshots เล็กและ review ได้
- Visual baseline ใช้ pinned, consistent environment
- Dynamic data/time/animation ถูกควบคุมก่อนถ่าย
- Emulation limits ถูกบอก และ matrix มาจาก support/risk evidence
สรุปบทนี้
Accessibility, pixels และ emulated environment เป็นหลักฐานคนละแกน ใช้ร่วมกันเพื่อเห็น defect มากขึ้น แต่ต้องรักษาขอบเขต: automated scan ไม่แทน human evaluation และ emulation ไม่แทน real device