Wiki

← Tất cả bài wiki

Accessibility Testing với Axe (Kiểm thử WCAG tự động trong Playwright)

automationplaywright

Tích hợp @axe-core/playwright để quét trang theo bộ rule WCAG (color-contrast, label, ARIA...) ngay trong Playwright test — AxeBuilder trả kết quả violations có cấu trúc để assert hoặc đưa vào report. Axe bắt được phần lỗi a11y 'kiểm tra được bằng máy' (~30-50% tổng vấn đề); keyboard flow và screen-reader UX vẫn cần test tay.

Khái niệm & Bản chất

  • Axe (Deque) là engine a11y de-facto: bộ rule theo WCAG 2.x mức A/AA/AAA, chạy trực tiếp trên DOM đang render — check contrast, label form, alt text, ARIA attribute, heading order, landmark...
  • @axe-core/playwright = wrapper AxeBuilder: inject axe vào page hiện tại rồi .analyze() → AxeResults gồm violations, passes, incomplete, inapplicable.
  • Cấu trúc violation: id (rule, vd 'color-contrast'), impact (critical|serious|moderate|minor), description + link helpUrl, nodes[] — mỗi node có target (CSS selector), failureSummary, html.
  • Ranah giới hạn của automation: axe check 'cái check được' — không đánh giá được keyboard trap, logic focus, nội dung screen-reader có nghĩa không → là tầng lọc đầu, không phải chứng nhận a11y.

Cách hoạt động & API chính

  • Cài đặt: npm i -D @axe-core/playwright → import { AxeBuilder } from '@axe-core/playwright' → new AxeBuilder({ page }).analyze().
  • .withTags([...]) chọn bộ rule: 'wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice' — thực tế hay dùng wcag2a+wcag2aa.
  • .include(selector) / .exclude(selector): giới hạn vùng quét — exclude widget 3rd-party, include đúng modal đang mở.
  • .disableRules(['rule-id']) / .options({ rules }): tắt rule cụ thể (ví dụ color-contrast đang fix dần) hoặc config rule chi tiết.
  • .analyze() trên trang hiện tại: quét đúng trạng thái DOM lúc gọi — mở modal xong mới analyze thì mới thấy violation của modal.

Ví dụ & Cú pháp thực tế

  • Smoke a11y cơ bản: ``ts import { test, expect } from '@playwright/test'; import { AxeBuilder } from '@axe-core/playwright'; test('trang chủ không vi phạm WCAG A/AA', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .analyze(); expect(results.violations).toEqual([]); }); ``
  • Scope + tắt rule đang fix: ``ts const results = await new AxeBuilder({ page }) .exclude('.chat-widget') // vùng 3rd-party render .disableRules(['color-contrast']) // ticket a11y-112 đang xử lý .analyze(); ``
  • Report lỗi dễ đọc khi fail — map violation thành message gọn: ``ts const summary = results.violations.map(v => ${v.id} [${v.impact}] — ${v.nodes.length} node(s): ${v.helpUrl} ); expect(summary, 'a11y violations').toEqual([]); ``
  • Quét ở NHIỀU trạng thái UI — a11y bug nằm ở dynamic content: ``ts await page.getByRole('button', { name: 'Mở giỏ hàng' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); const modalScan = await new AxeBuilder({ page }) .include('#cart-modal') .analyze(); expect(modalScan.violations).toEqual([]); ``

Best practices & Khi nào dùng

  • Quét nhiều state: initial load, sau mở modal/drawer/dropdown, sau submit form có lỗi, trạng thái empty/loading — violation a11y thường ẩn trong dynamic UI.
  • Bắt đầu wcag2a + wcag2aa: phủ đúng chuẩn compliance phổ biến (hợp đồng/EO AA thường yêu cầu), tránh nhiễu từ rule AAA/best-practice.
  • Ratchet cho legacy: trang cũ đang có 40 violation → assert violations.length <= 40 và giảm dần, thay vì đòi [] fail hết suite.
  • Kết hợp expect(locator).toMatchAriaSnapshot(): Axe check rule; ARIA snapshot check name/role/structure thực tế screen-reader 'thấy' — hai góc bổ sung nhau.
  • Test keyboard riêng: page.keyboard.press('Tab') kiểm focus order/trap — phần Axe không cover.

Gotchas, So sánh & Lưu ý

  • Scan theo DOM tại thời điểm gọi: content lazy/async phải expect(...).toBeVisible() xong mới analyze — quét sớm bắt thiếu vùng.
  • color-contrast hay false positive: text trên gradient/background image Axe không đo chính xác → đôi khi phải disable hoặc manual review.
  • Đừng bỏ incomplete: đó là rule Axe không kết luận được (vd contrast trên ảnh phức tạp) — cần người review, không phải 'pass'.
  • Tốn thời gian: inject + quét toàn DOM mỗi lần — đừng nhét vào mọi test; tách spec a11y.spec.ts chạy theo job riêng.
  • Animation đang chạy → flake: element di chuyển/ẩn hiện giữa chừng làm kết quả không ổn định — reducedMotion: 'reduce' giúp.
  • Axe pass ≠ accessible: qua hết rule vẫn có thể dùng khó với screen reader — automation a11y là sàn, audit thủ công là trần.