diff --git a/integration/e2e_tests/a11y-report-template.html b/integration/e2e_tests/a11y-report-template.html new file mode 100644 index 0000000000..16ffa422c9 --- /dev/null +++ b/integration/e2e_tests/a11y-report-template.html @@ -0,0 +1,298 @@ + + + + + + + + + axe-core test results + + +
+ +
+

+ Tested at +

+ +

+
+
+ + +
Failed
+ +
+ + + + + + +
Needs review
+ +
+ + + + These results were aborted and require further testing. This can happen either + because of technical restrictions to what the rule can test, or because a javascript error occurred. + + + + + +
Passed
+ +
+ + + + + +
Other checks
+ +
+ + + + These results indicate which rules did not run because no matching content was + found on the page. For example, with no video, those rules won't run. + + + +
+
+
+ + + + + + + + + diff --git a/integration/e2e_tests/axe.spec.js b/integration/e2e_tests/axe.spec.js new file mode 100644 index 0000000000..210118907d --- /dev/null +++ b/integration/e2e_tests/axe.spec.js @@ -0,0 +1,128 @@ +const { test, expect } = require("@playwright/test"); +const AxeBuilder = require('@axe-core/playwright').default; +const fs = require("fs"); +const path = require("path"); + +async function writeHTMLReport(pagePath, axeResult) { + const reportName = `axe-report-${pagePath.replaceAll("/", "-")}-${axeResult.timestamp}.html`; + const filePath = path.join(__dirname, "a11y-report-template.html"); + const html = await fs.promises.readFile(filePath, { encoding: 'utf8' }); + const $ = require('cheerio').load(html); + $('body').append(``); + const rendered = $.html(); + const reportPath = path.join(__dirname, reportName); + fs.writeFileSync(reportPath, rendered); + return { reportName, reportPath }; +} + +const axeTest = test.extend({ + makeAxeBuilder: async ({ page }, use, testInfo) => { + const makeAxeBuilder = () => new AxeBuilder({ page }) + .withTags(['wcag22aa']); + + await use(makeAxeBuilder); + } +}); + +function runAxeTest(pagePath) { + axeTest(`${pagePath} landing page`, async ({ page, makeAxeBuilder }, testInfo) => { + // default localhost:4001, specify HOST environment variable to change baseURL + await page.goto(`${pagePath}`); + + // Avoid repeatedly surfacing errors from the page template by only + // testing the main content area unless we're testing the homepage. + const axe = await makeAxeBuilder(); + if (pagePath !== "/") axe.include('main'); + + // Analyze the page! + const axeResult = await axe.analyze(); + + // Attach screenshots of target HTML elements noted in the test failures + const screenshots = await Promise.all(axeResult.violations + .flatMap(({ id, nodes }) => { + return nodes.flatMap(async ({ target }, index) => { + const shot = await page.locator(target[0]).screenshot(); + return { shot, shotName: `${id}-${index}` }; + }) + }) + ); + screenshots.forEach(async ({ shot, shotName }) => { + await testInfo.attach(shotName, { body: shot, contentType: 'image/png' }); + }); + + // Create custom HTML report from the axe output JSON and attach to test + const { reportName, reportPath } = await writeHTMLReport(pagePath, axeResult) + await testInfo.attach(reportName, { path: reportPath }); + fs.unlinkSync(reportPath); + + expect(axeResult.violations.length).toEqual(0); + }); +} + +axeTest.describe('has no automatically detectable accessibility issues', () => { + /** + * Top-level pages from the main navigation. + */ + [ + "/", + "/schedules/subway", + "/schedules/bus", + "/schedules/commuter-rail", + "/schedules/ferry", + "/accessibility/the-ride", + "/trip-planner", + "/alerts", + "/parking", + "/bikes", + "/guides", + "/holidays", + "/accessibility", + "/transit-near-me", + "/stops", + "/destinations", + "/maps", + "/fares", + "/fares/subway-fares", + "/fares/bus-fares", + "/fares/commuter-rail-fares", + "/fares/ferry-fares", + "/fares/charliecard-store", + "/fares/charliecard", + "/fares/retail-sales-locations", + "/customer-support", + "/customer-support/lost-and-found", + "/language-services", + "/transit-police", + "/transit-police/see-something-say-something", + "/mbta-at-a-glance", + "/leadership", + "/history", + "/financials", + "/events", + "/news", + "/policies", + "/safety", + "/quality-compliance-oversight", + "/careers", + "/pass-program", + "/business", + "/innovation", + "/engineering/design-standards-and-guidelines", + "/sustainability", + "/projects" + ].forEach(runAxeTest); + + /** + * Other specific pages and sub-pages to test. In the future, can write tests + * for different application states and interactions. + */ + [ + "/schedules/Red/line", + "/schedules/Orange/alerts", + "/schedules/111/line", + "/schedules/CR-Worcester/timetable", + "/schedules/Boat-F1", + "/stops/place-north", + "/stops/1" + ].forEach(runAxeTest); +});