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= wrapperAxeBuilder: inject axe vào page hiện tại rồi.analyze()→AxeResultsgồmviolations,passes,incomplete,inapplicable.- Cấu trúc violation:
id(rule, vd'color-contrast'),impact(critical|serious|moderate|minor),description+ linkhelpUrl,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ùngwcag2a+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 <= 40và 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-contrasthay 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.tschạ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.
