diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 91a386a27ce..96c5380391b 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -206,3 +206,9 @@ jobs: name: e2e-tests-report path: e2e-tests/playwright-report/ retention-days: 30 + - name: Upload accessibility artifacts + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: a11y-artifacts + path: ${{ github.workspace }}/a11y-artifacts + retention-days: 30 diff --git a/e2e-tests/tests/headlampPage.ts b/e2e-tests/tests/headlampPage.ts index 806080ae1a7..30f9966e789 100644 --- a/e2e-tests/tests/headlampPage.ts +++ b/e2e-tests/tests/headlampPage.ts @@ -25,9 +25,71 @@ export class HeadlampPage { this.testURL = process.env.HEADLAMP_TEST_URL || '/'; } + /** + * Run an accessibility audit against the current Playwright page using AxeBuilder. + * + * Summary: + * - Executes axe-core analysis on this.page. + * - If any violations are found, creates an output directory + * ( or cwd)/a11y-artifacts and saves: + * - a full-page screenshot: a11y-.png + * - violations JSON: a11y--violations.json + * - page HTML: a11y-.html + * - Logs the saved artifact paths and the violations to stderr. + * - Fails the test by asserting that there are no accessibility violations. + * + * Notes: + * - Timestamp uses ISO format with ":" and "." replaced by "-" to make filenames safe. + * - The workspace path is resolved from process.env.GITHUB_WORKSPACE or falling back to process.cwd(). + * - This method performs file I/O and may throw if the runner/user has no write permissions. + * + * How to view the screenshot from a GitHub Actions run: + * + * After the workflow completes, open the workflow run in the GitHub Actions UI, + * download the "a11y-artifacts" artifact, extract it, and open the PNG file locally. + * + * @remarks + * - Intended to be used inside an e2e test/page object where `this.page` is a Playwright Page. + * - The final expect will cause the test to fail when violations are present, making CI fail fast. + * + * @returns Promise resolving when analysis, artifact creation, and the assertion complete. + */ async a11y() { const axeBuilder = new AxeBuilder({ page: this.page }); const accessibilityResults = await axeBuilder.analyze(); + + if (accessibilityResults.violations && accessibilityResults.violations.length > 0) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); + const { join } = await import('path'); + const fs = await import('fs/promises'); + + const outDir = join(workspace, 'a11y-artifacts'); + await fs.mkdir(outDir, { recursive: true }); + + const screenshotPath = join(outDir, `a11y-${timestamp}.png`); + await this.page.screenshot({ path: screenshotPath, fullPage: true }); + + const violationsPath = join(outDir, `a11y-${timestamp}-violations.json`); + await fs.writeFile( + violationsPath, + JSON.stringify(accessibilityResults.violations, null, 2), + 'utf8' + ); + + const html = await this.page.content(); + const htmlPath = join(outDir, `a11y-${timestamp}.html`); + await fs.writeFile(htmlPath, html, 'utf8'); + + console.error(`Accessibility violations saved to: ${violationsPath}`); + console.error(`Screenshot saved to: ${screenshotPath}`); + console.error(`Page HTML saved to: ${htmlPath}`); + console.error( + 'Accessibility violations:', + JSON.stringify(accessibilityResults.violations, null, 2) + ); + } + expect(accessibilityResults.violations).toStrictEqual([]); }