Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow matcher to accept Axe results object #23

Merged
merged 15 commits into from
Sep 28, 2022
5 changes: 5 additions & 0 deletions .changeset/wild-shrimps-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'expect-axe-playwright': minor
---

Allow matcher to accept Axe results object
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ Or pass a locator to test part of the page:
await expect(page.locator('#my-element')).toPassAxe()
```

You can also pass an [Axe results
object](https://www.deque.com/axe/core-documentation/api-documentation/#results-object)
to the matcher:

```js
import { waitForAxeResults } from 'expect-axe-playwright'

test('should pass common accessibility checks', async ({ page }) => {
const { results } = await waitForAxeResults(page)
await expect(results).toPassAxe()
})
```

#### Word of Caution: Limitations to Accessibility Tests

```js
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import matchers from './matchers'
export { waitForAxeResults } from './waitForAxeResults'
export default matchers
12 changes: 12 additions & 0 deletions src/matchers/toPassAxe/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,16 @@ test.describe.parallel('toPassAxe', () => {
.catch(() => Promise.resolve())
expect(attachmentExists(filename)).toBe(true)
})

test.describe('with Axe results object', async () => {
test('positive', async () => {
const results = { violations: [] }
await expect(results).toPassAxe()
})

test('negative', async () => {
const results = { violations: [{ id: 'foo', nodes: [] }] }
await expect(results).not.toPassAxe()
})
})
})
44 changes: 20 additions & 24 deletions src/matchers/toPassAxe/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
import test from '@playwright/test'
import type { MatcherState } from '@playwright/test/types/expect-types'
import type { Result, RunOptions } from 'axe-core'
import type { AxeResults, Result } from 'axe-core'
import type { MatcherOptions } from '../../types'
import type { Handle } from '../../utils/locator'
import { getOptions } from '../../utils/options'
import createHTMLReport from 'axe-reporter-html'
import merge from 'merge-deep'
import { attach } from '../../utils/attachments'
import { injectAxe, runAxe } from '../../utils/axe'
import { Handle, resolveLocator } from '../../utils/matcher'
import { poll } from '../../utils/poll'
import { waitForAxeResults } from '../../waitForAxeResults'

const summarize = (violations: Result[]) =>
violations
.map((violation) => `${violation.id}(${violation.nodes.length})`)
.join(', ')

interface MatcherOptions extends RunOptions {
timeout?: number
filename?: string
async function getResults(obj: Handle | AxeResults, options: MatcherOptions) {
if ((obj as AxeResults).violations) {
const results = obj as AxeResults
return {
results,
ok: !results.violations.length,
}
} else {
return waitForAxeResults(obj as Handle, options)
}
}

export async function toPassAxe(
this: MatcherState,
handle: Handle,
{ timeout, ...options }: MatcherOptions = {}
obj: Handle | AxeResults,
options: MatcherOptions = {}
) {
try {
const locator = resolveLocator(handle)
await injectAxe(locator)

const info = test.info()
const opts = merge(info.project.use.axeOptions, options)

const { ok, results } = await poll(locator, timeout, async () => {
const results = await runAxe(locator, opts)

return {
ok: !results.violations.length,
results,
}
})
const { results, ok } = await getResults(obj, options)

// If there are violations, attach an HTML report to the test for additional
// visibility into the issue.
if (!ok) {
const info = test.info()
const opts = getOptions(options)
const html = await createHTMLReport(results)
const filename = opts.filename || 'axe-report.html'
await attach(info, filename, html)
Expand Down
6 changes: 6 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { RunOptions } from 'axe-core'

export interface MatcherOptions extends RunOptions {
timeout?: number
filename?: string
}
File renamed without changes.
12 changes: 12 additions & 0 deletions src/utils/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { MatcherOptions } from '../types'
import test from '@playwright/test'
import merge from 'merge-deep'

/**
* Overrides the default options with any user-provided options, and
* returns the final options object.
*/
export function getOptions(options: MatcherOptions = {}) {
const info = test.info()
return merge(info.project.use.axeOptions, options)
}
21 changes: 21 additions & 0 deletions src/waitForAxeResults.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test'
import { readFile } from './utils/file'
import { waitForAxeResults } from './waitForAxeResults'

test.describe('waitForAxeResults', () => {
test('should be ok for page with no axe violations', async ({ page }) => {
const content = await readFile('accessible.html')
await page.setContent(content)
const { ok, results } = await waitForAxeResults(page)
expect(results.violations).toHaveLength(0)
expect(ok).toBe(true)
})

test('should not be ok for page with axe violations', async ({ page }) => {
const content = await readFile('inaccessible.html')
await page.setContent(content)
const { ok, results } = await waitForAxeResults(page)
expect(results.violations.length).toBeGreaterThan(0)
expect(ok).toBe(false)
})
})
26 changes: 26 additions & 0 deletions src/waitForAxeResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { RunOptions } from 'axe-core'
import { Handle, resolveLocator } from './utils/locator'
import { poll } from './utils/poll'
import { injectAxe, runAxe } from './utils/axe'
import { getOptions } from './utils/options'

/**
* Injects axe onto page, waits for the page to be ready, then runs axe against
* the provided element handle (which could be the entire page).
*/
export async function waitForAxeResults(
handle: Handle,
{ timeout, ...options }: { timeout?: number } & RunOptions = {}
) {
const opts = getOptions(options)
const locator = resolveLocator(handle)
await injectAxe(locator)
return poll(locator, timeout, async () => {
const results = await runAxe(locator, opts)

return {
ok: !results.violations.length,
results,
}
})
}