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')).toBeAccessible()
```

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 be accessible', async ({ page }) => {
const { results } = await waitForAxeResults(page)
await expect(results).toBeAccessible()
gabalafou marked this conversation as resolved.
Show resolved Hide resolved
})
```

#### Axe run options

You can configure options that should be passed to aXe at the project or
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/toBeAccessible/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,16 @@ test.describe.parallel('toBeAccessible', () => {
.catch(() => Promise.resolve())
expect(attachmentExists(filename)).toBe(true)
})

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

test('negative', async () => {
const results = { violations: [{ id: 'foo', nodes: [] }] }
await expect(results).not.toBeAccessible()
})
})
})
37 changes: 17 additions & 20 deletions src/matchers/toBeAccessible/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,41 @@
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 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 toBeAccessible(
this: MatcherState,
handle: Handle,
{ timeout, ...options }: MatcherOptions = {}
obj: Handle | AxeResults,
options: MatcherOptions = {}
) {
try {
const locator = resolveLocator(handle)
await injectAxe(locator)
const { results, ok } = await getResults(obj, options)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This merge needs to happen before calling getResults and the opts object needs to be passed to getResults. This allows for defining config at the project level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, looks like getResults is doing that already. Interesting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there's some repetition, but I'm not sure how to avoid it.

Do you want me to try to refactor this in a way that avoids the repetition? Or do you want some time to think about it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, it's fine. You could make a function that does it so the test.info and merge calls are consistent if you want.


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

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

// If there are violations, attach an HTML report to the test for additional
// visibility into the issue.
if (!ok) {
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.
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)
})
})
28 changes: 28 additions & 0 deletions src/waitForAxeResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import test from '@playwright/test'
import type { AxeResults, RunOptions } from 'axe-core'
gabalafou marked this conversation as resolved.
Show resolved Hide resolved
import { Handle, resolveLocator } from './utils/locator'
import merge from 'merge-deep'
import { poll } from './utils/poll'
import { injectAxe, runAxe } from './utils/axe'

/**
* 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 info = test.info()
const opts = merge(info.project.use.axeOptions, 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,
}
})
}