Skip to content

Commit

Permalink
Merge pull request #2532 from alphagov/puppeteer-18-axe
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrotherham authored Jan 17, 2023
2 parents aa8e385 + ea4ec2b commit 40c3ae0
Show file tree
Hide file tree
Showing 21 changed files with 1,629 additions and 881 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module.exports = {
files: ['**/*.test.{cjs,js,mjs}'],
env: {
jest: true
},
globals: {
page: true,
browser: true
}
}
]
Expand Down
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ updates:
- dependency-type: direct

ignore:
# Ignore major updates (@axe-core/puppeteer peer puppeteer <= 18)
- dependency-name: puppeteer
update-types: ['version-update:semver-major']

# Ignore major/minor updates (Marked parser changes output)
- dependency-name: jstransformer-marked
update-types: ['version-update:semver-major', 'version-update:semver-minor']
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
run: npm run build

- name: Lint and test
run: npm test -- --runInBand
run: npm test -- --color --maxWorkers=2

# Share data between the build and deploy jobs so we don't need to run `npm run build` again on deploy
# Upload the deploy folder as an artifact so it can be downloaded and used in the deploy job
Expand All @@ -52,4 +52,3 @@ jobs:
name: build
path: deploy/**
retention-days: 1

Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
const { AxePuppeteer } = require('@axe-core/puppeteer')

const { AxePuppeteer } = require('axe-puppeteer')
const { goTo } = require('../lib/puppeteer-helpers.js')

const { setupPage } = require('../lib/jest-utilities.js')
const configPaths = require('../lib/paths.js')
const PORT = configPaths.testPort
async function analyze (page, path) {
await goTo(page, path)

let page
const baseUrl = 'http://localhost:' + PORT

async function audit (page) {
const axe = new AxePuppeteer(page)
.include('body')
// axe reports there is "no label associated with the text field", when there is one.
Expand All @@ -20,58 +16,47 @@ async function audit (page) {
.exclude('.govuk-skip-link')
// axe reports that the back to top button is not inside a landmark, which is intentional.
.exclude('.app-back-to-top')
// axe reports that the frame "does not have a main landmark" and example <h1> headings
// violate "Heading levels should only increase by one", which is intentional.
// https://github.com/alphagov/govuk-design-system/pull/2442#issuecomment-1326600528
.exclude('.app-example__frame')

const results = await axe.analyze()

return results.violations
return axe.analyze()
}

beforeAll(async () => {
page = await setupPage()
})

afterAll(async () => {
await page.close()
})

describe('Accessibility Audit', () => {
describe('Home page - layout.njk', () => {
it('validates', async () => {
await page.goto(baseUrl + '/', { waitUntil: 'load' })
const violations = await audit(page)
expect(violations).toEqual([])
const results = await analyze(page, '/')
expect(results).toHaveNoViolations()
})
})

describe('Component page - layout-pane.njk', () => {
it('validates', async () => {
await page.goto(baseUrl + '/components/radios/', { waitUntil: 'load' })
const violations = await audit(page)
expect(violations).toEqual([])
const results = await analyze(page, '/components/radios/')
expect(results).toHaveNoViolations()
})
})

describe('Patterns page - layout-pane.njk', () => {
it('validates', async () => {
await page.goto(baseUrl + '/patterns/gender-or-sex/', { waitUntil: 'load' })
const violations = await audit(page)
expect(violations).toEqual([])
const results = await analyze(page, '/patterns/gender-or-sex/')
expect(results).toHaveNoViolations()
})
})

describe('Get in touch page - layout-single-page.njk', () => {
it('validates', async () => {
await page.goto(baseUrl + '/get-in-touch/', { waitUntil: 'load' })
const violations = await audit(page)
expect(violations).toEqual([])
const results = await analyze(page, '/get-in-touch/')
expect(results).toHaveNoViolations()
})
})

describe('Site Map page - layout-sitemap.njk', () => {
it('validates', async () => {
await page.goto(baseUrl + '/sitemap/', { waitUntil: 'load' })
const violations = await audit(page)
expect(violations).toEqual([])
const results = await analyze(page, '/sitemap/')
expect(results).toHaveNoViolations()
})
})
})
79 changes: 47 additions & 32 deletions __tests__/back-to-top.test.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,63 @@

const { setupPage } = require('../lib/jest-utilities.js')
const configPaths = require('../lib/paths.js')
const PORT = configPaths.testPort
const { goTo, isVisible } = require('../lib/puppeteer-helpers.js')

let page
const baseUrl = 'http://localhost:' + PORT
describe('Back to top', () => {
let $module
let $backToTopLink
let pageHeight

beforeAll(async () => {
page = await setupPage()
})
async function setup (page) {
$module = await page.$('[data-module="app-back-to-top"]')
$backToTopLink = await $module.$('a')

afterAll(async () => {
await page.close()
})
// Scrollable height of body
pageHeight = await page.$eval('body', ($element) => $element.scrollHeight) ?? 0
}

const BACK_TO_TOP_LINK_SELECTOR = '[data-module="app-back-to-top"] a'
function scrollTo (page, scrollY) {
return page.evaluate((y) => window.scroll(0, y), scrollY)
}

beforeEach(async () => {
await page.setJavaScriptEnabled(true)

await goTo(page, '/styles/colour/')
await scrollTo(page, 0)
await setup(page)
})

describe('Back to top', () => {
it('is always visible when JavaScript is disabled', async () => {
await page.setJavaScriptEnabled(false)
await page.goto(`${baseUrl}/styles/colour/`, { waitUntil: 'load' })
const isBackToTopVisible = await page.waitForSelector(BACK_TO_TOP_LINK_SELECTOR, { visible: true })
expect(isBackToTopVisible).toBeTruthy()

// Reload page again
await page.reload()
await setup(page)

// Visible on page
await expect(isVisible($backToTopLink)).resolves.toBe(true)
})

it('is hidden when at the top of the page', async () => {
await page.goto(`${baseUrl}/styles/colour/`, { waitUntil: 'load' })
const isBackToTopHidden = await page.waitForSelector(BACK_TO_TOP_LINK_SELECTOR, { visible: false })
expect(isBackToTopHidden).toBeTruthy()
await scrollTo(page, 0)

// Visible on page, hidden from viewport
await expect(isVisible($backToTopLink)).resolves.toBe(true)
await expect($backToTopLink.isIntersectingViewport()).resolves.toBe(false)
})

it('is visible when at the bottom of the page', async () => {
await page.goto(`${baseUrl}/styles/colour/`, { waitUntil: 'load' })
// Scroll to the bottom of the page
await page.evaluate(() => window.scrollBy(0, document.body.scrollHeight))
const isBackToTopVisible = await page.waitForSelector(BACK_TO_TOP_LINK_SELECTOR, { visible: true })
expect(isBackToTopVisible).toBeTruthy()
await scrollTo(page, pageHeight)

// Visible on page, shown in viewport
await expect(isVisible($backToTopLink)).resolves.toBe(true)
await expect($backToTopLink.isIntersectingViewport()).resolves.toBe(true)
})

it('goes back to the top of the page when interacted with', async () => {
await page.goto(`${baseUrl}/styles/colour/`, { waitUntil: 'load' })
// Scroll to the bottom of the page
await page.evaluate(() => window.scrollBy(0, document.body.scrollHeight))
// Make sure the back to top component is available to click
await page.waitForSelector(BACK_TO_TOP_LINK_SELECTOR, { visible: true })
await page.click(BACK_TO_TOP_LINK_SELECTOR)
const isAtTopOfPage = await page.evaluate(() => window.scrollY === 0)
expect(isAtTopOfPage).toBeTruthy()
await scrollTo(page, pageHeight)
await $backToTopLink.click()

// Scrolled to top
await expect(page.evaluate(() => window.scrollY)).resolves.toBe(0)
})
})
37 changes: 13 additions & 24 deletions __tests__/component-options.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@

const { setupPage } = require('../lib/jest-utilities.js')
const configPaths = require('../lib/paths.js')
const PORT = configPaths.testPort

let page
const baseUrl = 'http://localhost:' + PORT

beforeEach(async () => {
page = await setupPage()
})

afterEach(async () => {
await page.close()
})
const { goTo } = require('../lib/puppeteer-helpers.js')

describe('Component page', () => {
it('should contain a "Nunjucks" tab heading', async () => {
await page.goto(baseUrl + '/components/back-link/', { waitUntil: 'load' })
await goTo(page, '/components/back-link/')

const nunjucksTabHeadings = await page.evaluate(() => Array.from(document.querySelectorAll('.js-tabs__item a'))
.filter(element => element.textContent === 'Nunjucks'))
.filter(element => element.textContent === 'Nunjucks')
)

expect(nunjucksTabHeadings[0]).toBeTruthy()
})

it('"Nunjucks" tab content should contain a details summary with "Nunjucks macro options" text', async () => {
await page.goto(baseUrl + '/components/back-link/', { waitUntil: 'load' })
await goTo(page, '/components/back-link/')

// Get "aria-controls" attributes from "Nunjucks" tab headings
const nunjucksTabHeadingControls = await page.evaluateHandle(() => Array.from(document.querySelectorAll('.js-tabs__item a'))
.filter(element => element.textContent === 'Nunjucks')
.map(element => element.getAttribute('aria-controls')))
.map(element => element.getAttribute('aria-controls'))
)

const tabContentIds = await nunjucksTabHeadingControls.jsonValue() // Returns Puppeteer JSONHandle

Expand All @@ -44,7 +33,7 @@ describe('Component page', () => {
})

it('"Nunjucks" tab content should contain a details element that has a table with "Name", "Type" and "Description" column headings', async () => {
await page.goto(baseUrl + '/components/back-link/', { waitUntil: 'load' })
await goTo(page, '/components/back-link/')

// Get "aria-controls" attributes from "Nunjucks" tab headings
const nunjucksTabHeadingControls = await page.evaluateHandle(() => Array.from(document.querySelectorAll('.js-tabs__item a'))
Expand All @@ -62,24 +51,24 @@ describe('Component page', () => {
})

it('macro options should be opened and in view when linked to', async () => {
await page.goto(baseUrl + '/components/back-link/#options-back-link-example', { waitUntil: 'load' })
await goTo(page, '/components/back-link/#options-back-link-example')

// Check if example's macro options details element is open
await page.waitForSelector('#options-back-link-example-details[open=open]')

// Check if the example has been scrolled into the viewport
const $example = await page.$('#options-back-link-example')
expect(await $example.isIntersectingViewport()).toBe(true)
const $example = await page.$('#options-back-link-example-details')
await expect($example.isIntersectingViewport()).resolves.toBe(true)
})

it('macro options subtable should be opened and in view when linked to', async () => {
await page.goto(baseUrl + '/components/text-input/#options-text-input-example--label', { waitUntil: 'load' })
await goTo(page, '/components/text-input/#options-text-input-example--label')

// Check if example's macro options details element is open
await page.waitForSelector('#options-text-input-example-details[open=open]')

// Check if the example has been scrolled into the viewport
const $example = await page.$('#options-text-input-example--label')
expect(await $example.isIntersectingViewport()).toBe(true)
await expect($example.isIntersectingViewport()).resolves.toBe(true)
})
})
Loading

0 comments on commit 40c3ae0

Please sign in to comment.