@a11y-oracle/playwright-plugin

中文文档
1.3.2发布于 2026/03/12MITTypeScriptES Module近 30 天下载 3178

Playwright test fixture for accessibility speech assertions, keyboard navigation, and focus indicator validation

原始使用文档
文档目录14 节
  1. Installation
  2. Usage
  3. Test Fixture (Recommended)
  4. Unified State API
  5. Tab Order and Keyboard Trap Detection
  6. Audit and Issue Reporting
  7. Customizing Options
  8. Manual Usage
  9. API Reference
  10. A11yOracle
  11. Test Fixture
  12. Exports
  13. Playwright Config
  14. How It Works

@a11y-oracle/playwright-plugin

Playwright integration for A11y-Oracle. Provides a test fixture and wrapper class that reads the browser's Accessibility Tree via Chrome DevTools Protocol, dispatches native keyboard events, and analyzes visual focus indicators.

✅ Recommended plugin. Playwright provides direct, first-class CDP session access, making this the most stable and performant integration for A11y-Oracle. If you have a choice of E2E framework, this is the one to use.

import { test, expect } from '@a11y-oracle/playwright-plugin';

test('dropdown button announces correctly', async ({ page, a11y }) => {
  await page.goto('/dropdown-nav.html');

  const speech = await a11y.press('Tab');
  expect(speech).toContain('Home');
  expect(speech).toContain('menu item');
});

Installation

npm install -D @a11y-oracle/playwright-plugin @a11y-oracle/core-engine @playwright/test

Chromium only. CDP sessions are not available for Firefox or WebKit in Playwright.

Usage

The plugin exports an extended test function that injects an a11y fixture. The CDP session is created before each test and cleaned up automatically after.

import { test, expect } from '@a11y-oracle/playwright-plugin';

test.describe('Navigation', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/my-page.html');
  });

  test('Tab to button announces name and role', async ({ a11y }) => {
    const speech = await a11y.press('Tab');
    expect(speech).toBe('Submit, button');
  });

  test('checkbox announces checked state', async ({ a11y }) => {
    await a11y.press('Tab');
    await a11y.press('Tab');
    const speech = await a11y.press('Space');
    expect(speech).toContain('checkbox');
    expect(speech).toContain('checked');
  });

  test('navigation landmark exists', async ({ a11y }) => {
    const tree = await a11y.getFullTreeSpeech();
    const nav = tree.find(r => r.speech.includes('navigation landmark'));
    expect(nav).toBeDefined();
  });
});

Unified State API

The pressKey() method returns a complete A11yState snapshot combining speech output, focused element info, and focus indicator analysis:

test('focus indicator meets WCAG 2.4.12 AA', async ({ page, a11y }) => {
  await page.goto('/my-page.html');

  const state = await a11y.pressKey('Tab');

  // Speech
  expect(state.speech).toContain('Submit');
  expect(state.speechResult?.role).toBe('button');

  // Focused element
  expect(state.focusedElement?.tag).toBe('BUTTON');
  expect(state.focusedElement?.id).toBe('submit-btn');
  expect(state.focusedElement?.tabIndex).toBe(0);

  // Focus indicator CSS analysis
  expect(state.focusIndicator.isVisible).toBe(true);
  expect(state.focusIndicator.contrastRatio).toBeGreaterThanOrEqual(3.0);
  expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
});

test('Shift+Tab navigates backward', async ({ page, a11y }) => {
  await page.goto('/my-page.html');

  await a11y.pressKey('Tab');
  const state1 = await a11y.pressKey('Tab');
  const state2 = await a11y.pressKey('Tab', { shift: true });

  expect(state2.focusedElement?.id).toBe(state1.focusedElement?.id);
});

Tab Order and Keyboard Trap Detection

test('page has correct tab order', async ({ page, a11y }) => {
  await page.goto('/my-page.html');

  const report = await a11y.traverseTabOrder();
  expect(report.totalCount).toBeGreaterThan(0);
  expect(report.entries[0].tag).toBe('A');
});

test('modal does not trap keyboard focus', async ({ page, a11y }) => {
  await page.goto('/modal.html');

  const result = await a11y.traverseSubTree('#modal-container', 20);
  expect(result.isTrapped).toBe(false);
  expect(result.escapeElement).not.toBeNull();
});

Audit and Issue Reporting

Use OracleAuditor from @a11y-oracle/audit-formatter to automatically check WCAG rules on every interaction and accumulate any issues:

npm install -D @a11y-oracle/audit-formatter
import { test, expect } from '@a11y-oracle/playwright-plugin';
import { OracleAuditor } from '@a11y-oracle/audit-formatter';

test('all focus indicators pass oracle rules', async ({ page, a11y }) => {
  await page.goto('/my-page.html');

  const auditor = new OracleAuditor(a11y, {
    project: 'my-app',
    specName: 'navigation.spec.ts',
  });

  // Each pressKey() automatically checks all 5 state-based rules
  await auditor.pressKey('Tab');
  await auditor.pressKey('Tab');
  await auditor.pressKey('Tab');

  // checkTrap() automatically checks keyboard-trap
  await auditor.checkTrap('#modal-container');

  // Assert no issues found across all interactions
  expect(auditor.getIssues()).toHaveLength(0);
});

To write issues to a JSON report file at the end of the suite:

import { test } from '@a11y-oracle/playwright-plugin';
import { OracleAuditor, type OracleIssue } from '@a11y-oracle/audit-formatter';
import * as fs from 'fs';

const allIssues: OracleIssue[] = [];

test('check page focus indicators', async ({ page, a11y }) => {
  await page.goto('/my-page.html');
  const auditor = new OracleAuditor(a11y, {
    project: 'my-app',
    specName: 'nav.spec.ts',
  });

  await auditor.pressKey('Tab');
  await auditor.pressKey('Tab');
  allIssues.push(...auditor.getIssues());
});

test.afterAll(() => {
  if (allIssues.length > 0) {
    fs.writeFileSync('oracle-results.json', JSON.stringify(allIssues, null, 2));
  }
});

For detailed remediation guidance on each rule, see the Remediation Guide.

Customizing Options

Override speech engine options per test group using test.use():

test.describe('without landmark suffix', () => {
  test.use({ a11yOptions: { includeLandmarks: false } });

  test('nav role without landmark', async ({ page, a11y }) => {
    await page.goto('/my-page.html');
    const tree = await a11y.getFullTreeSpeech();
    const nav = tree.find(r => r.role === 'navigation');
    expect(nav?.speech).toBe('Main, navigation');
  });
});

Available options:

OptionTypeDefaultDescription
includeLandmarksbooleantrueAppend "landmark" to landmark roles
includeDescriptionbooleanfalseInclude aria-describedby text in output
focusSettleMsnumber50Delay (ms) after key press for focus/CSS to settle

Manual Usage

If you need more control over the lifecycle (e.g., attaching to a specific page mid-test), use the A11yOracle class directly:

import { A11yOracle } from '@a11y-oracle/playwright-plugin';
import { test, expect } from '@playwright/test';

test('manual setup', async ({ page }) => {
  await page.goto('/my-page.html');

  const a11y = new A11yOracle(page, { includeDescription: true });
  await a11y.init();

  const speech = await a11y.press('Tab');
  expect(speech).toContain('button');

  await a11y.dispose();
});

API Reference

A11yOracle

Manages a CDP session and provides accessibility testing for the current page.

constructor(page: Page, options?: A11yOrchestratorOptions)

Create a new instance.

  • page — Playwright Page to attach to.
  • options.includeLandmarks — Append "landmark" to landmark roles. Default true.
  • options.includeDescription — Include description text. Default false.
  • options.focusSettleMs — Delay after key press for focus/CSS to settle. Default 50.

init(): Promise<void>

Open a CDP session and enable the Accessibility domain. Must be called before any other method. The test fixture calls this automatically.

Speech-Only API

press(key: string): Promise<string>

Press a keyboard key (via Playwright's page.keyboard.press()) and return the speech for the newly focused element. Returns an empty string if no element has focus.

const speech = await a11y.press('Tab');
// "Products, button, collapsed"
getSpeech(): Promise<string>

Get the speech string for the currently focused element without pressing a key.

getSpeechResult(): Promise<SpeechResult | null>

Get the full structured result for the focused element.

getFullTreeSpeech(): Promise<SpeechResult[]>

Get speech for all non-ignored nodes in the accessibility tree.

Unified State API

pressKey(key: string, modifiers?: ModifierKeys): Promise<A11yState>

Dispatch a key via native CDP Input.dispatchKeyEvent and return the unified accessibility state. Unlike press(), this uses hardware-level key dispatch and returns the full state.

const state = await a11y.pressKey('Tab');
// state.speech           → "Products, button, collapsed"
// state.focusedElement   → { tag: 'BUTTON', id: '...', ... }
// state.focusIndicator   → { isVisible: true, meetsWCAG_AA: true, ... }
getA11yState(): Promise<A11yState>

Get the current unified state without pressing a key.

await page.focus('#my-button');
const state = await a11y.getA11yState();
traverseTabOrder(): Promise<TabOrderReport>

Extract all tabbable elements in DOM tab order.

traverseSubTree(selector: string, maxTabs?: number): Promise<TraversalResult>

Detect whether a container traps keyboard focus (WCAG 2.1.2).

Lifecycle

dispose(): Promise<void>

Detach the CDP session and free resources. The test fixture calls this automatically.

Test Fixture

The test export extends Playwright's test with two fixtures:

FixtureTypeDescription
a11yA11yOracleInitialized instance, auto-disposed after each test
a11yOptionsA11yOrchestratorOptionsOverride via test.use()

Exports

// Test fixture (recommended)
export { test, expect } from '@a11y-oracle/playwright-plugin';

// Manual usage
export { A11yOracle } from '@a11y-oracle/playwright-plugin';

// Fixture types
export type { A11yOracleFixtures } from '@a11y-oracle/playwright-plugin';

// Re-exported types from core-engine
export type {
  A11yState,
  A11yFocusedElement,
  A11yFocusIndicator,
  A11yOrchestratorOptions,
  SpeechResult,
  SpeechEngineOptions,
  ModifierKeys,
  TabOrderReport,
  TabOrderEntry,
  TraversalResult,
  FocusIndicator,
} from '@a11y-oracle/playwright-plugin';

Playwright Config

The plugin requires Chromium. A typical playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: 'http://localhost:4200',
  },
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
  ],
  webServer: {
    command: 'npm run serve',
    url: 'http://localhost:4200',
    reuseExistingServer: !process.env.CI,
  },
});

How It Works

  1. The fixture opens a CDP session via page.context().newCDPSession(page)
  2. It creates both a SpeechEngine and an A11yOrchestrator on that session
  3. press(key) — Uses Playwright's keyboard API, waits 50ms, then reads the AXTree for speech
  4. pressKey(key) — Uses native CDP Input.dispatchKeyEvent for hardware-level dispatch, waits focusSettleMs, then collects speech + focused element + focus indicator in parallel
  5. Focus indicator analysis runs Runtime.evaluate to read computed CSS styles and calculate contrast ratios
  6. On dispose, the CDP session is detached

The speech format follows: [Computed Name], [Role], [State/Properties]

For the full list of role and state mappings, see the @a11y-oracle/core-engine README.

版本与发布记录

@a11y-oracle/playwright-plugin 共发布 10 个版本,首次发布于 2026/03/05;过去 12 个月发布了 10 个版本。

版本发布日期距今
1.3.2latest2026/03/127 个月前
1.3.12026/03/117 个月前
1.3.02026/03/068 个月前
1.2.02026/03/068 个月前
1.1.42026/03/068 个月前
1.1.32026/03/068 个月前

关于 @a11y-oracle/playwright-plugin 的常见问题

@a11y-oracle/playwright-plugin 是什么?

Playwright test fixture for accessibility speech assertions, keyboard navigation, and focus indicator validation

如何安装 @a11y-oracle/playwright-plugin?

在项目目录运行 npm install @a11y-oracle/playwright-plugin,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@playwright/test(>=1.40.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@a11y-oracle/playwright-plugin 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@a11y-oracle/playwright-plugin 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@a11y-oracle/playwright-plugin 可以商用吗?

@a11y-oracle/playwright-plugin 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@a11y-oracle/playwright-plugin 的下载量有多少?

根据 npm 官方下载统计,@a11y-oracle/playwright-plugin 在 2026/10/05 – 2026/11/03 这 30 天内被下载 3,178 次,日均约 106 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@a11y-oracle/playwright-plugin 还在维护吗?

最新版本 1.3.2 发布于 2026/03/12(7 个月前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/03/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。