From db855813d6ffab4be68e3b5eaf337d223ac62538 Mon Sep 17 00:00:00 2001 From: Heather Buchel Date: Fri, 7 Jun 2024 11:52:28 -0400 Subject: [PATCH] feat: add accessibility Github Actions workflow (#7669) * add accessibility workflow * Update name * fix function name * add light mode/dark mode testing, clean up logging * add some pages to show violations * add spacing * revert page changes * make sure actions packages are up to date * add some initial readme guidance for accessibility violations * covert chain promises to async/await, some formatting/comments updated --- .github/workflows/accessibility_scan.yml | 46 +++++++++++ .../scripts/check_for_changed_pages.js | 74 +++++++++++++++++ .../scripts/run_accessibility_scan.js | 81 +++++++++++++++++++ Readme.md | 15 ++++ package.json | 2 + yarn.lock | 45 +++++++++++ 6 files changed, 263 insertions(+) create mode 100644 .github/workflows/accessibility_scan.yml create mode 100644 .github/workflows/scripts/check_for_changed_pages.js create mode 100644 .github/workflows/scripts/run_accessibility_scan.js diff --git a/.github/workflows/accessibility_scan.yml b/.github/workflows/accessibility_scan.yml new file mode 100644 index 00000000000..933fd2f136f --- /dev/null +++ b/.github/workflows/accessibility_scan.yml @@ -0,0 +1,46 @@ +name: Accessibility Scan +on: + pull_request: + branches: [main] + types: [opened, synchronize] +env: + BUILD_DIR: 'client/www/next-build' +jobs: + accessibility: + name: Runs accessibility scan on changed pages + runs-on: ubuntu-latest + steps: + - name: Checkout branch + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - name: Setup Node.js 20 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 20.x + - name: Install dependencies + run: yarn + - name: Build + run: yarn build + env: + NODE_OPTIONS: --max_old_space_size=4096 + - name: Get changed/new pages to run accessibility tests on + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: pages-to-a11y-test + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { getChangedPages } = require('./.github/workflows/scripts/check_for_changed_pages.js'); + const buildDir = process.env.BUILD_DIR; + return getChangedPages({github, context, buildDir}); + - name: Run site + run: | + python -m http.server 3000 -d ${{ env.BUILD_DIR }} & + sleep 5 + - name: Run accessibility tests on changed/new MDX pages + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: axeResults + with: + result-encoding: string + script: | + const { runAccessibilityScan } = require('./.github/workflows/scripts/run_accessibility_scan.js'); + const pages = ${{ steps.pages-to-a11y-test.outputs.result }} + return await runAccessibilityScan(pages) diff --git a/.github/workflows/scripts/check_for_changed_pages.js b/.github/workflows/scripts/check_for_changed_pages.js new file mode 100644 index 00000000000..bb2d41fb534 --- /dev/null +++ b/.github/workflows/scripts/check_for_changed_pages.js @@ -0,0 +1,74 @@ +module.exports = { + getChangedPages: async ({ github, context, buildDir }) => { + const fs = require('fs'); + const cheerio = require('cheerio'); + + const urlList = []; + + const { + issue: { number: issue_number }, + repo: { owner, repo } + } = context; + + const possiblePages = []; + const platforms = [ + 'android', + 'angular', + 'flutter', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue' + ]; + + const changedFiles = await github.paginate( + 'GET /repos/{owner}/{repo}/pulls/{pull_number}/files', + { owner, repo, pull_number: issue_number }, + (response) => + response.data.filter( + (file) => file.status === 'modified' || file.status === 'added' + ) + ); + + // Get only the changed files that are pages and build out the + // possiblePages array + changedFiles.forEach(({ filename }) => { + const isPage = + filename.startsWith('src/pages') && + (filename.endsWith('index.mdx') || filename.endsWith('index.tsx')); + if (isPage) { + const path = filename + .replace('src/pages', '') + .replace('/index.mdx', '') + .replace('/index.tsx', ''); + if (path.includes('[platform]')) { + platforms.forEach((platform) => { + possiblePages.push(path.replace('[platform]', platform)); + }); + } else { + possiblePages.push(path); + } + } + }); + + // Get the sitemap and parse for an array of site URLs + const siteMap = fs.readFileSync(`${buildDir}/sitemap.xml`); + + const siteMapParse = cheerio.load(siteMap, { + xml: true + }); + + siteMapParse('url').each(function () { + urlList.push(siteMapParse(this).find('loc').text()); + }); + + // Filter the possiblePages for only those that are part of the sitemap + const pages = possiblePages.filter((page) => + urlList.includes(`https://docs.amplify.aws${page}/`) + ); + + return pages; + } +}; diff --git a/.github/workflows/scripts/run_accessibility_scan.js b/.github/workflows/scripts/run_accessibility_scan.js new file mode 100644 index 00000000000..ce3692e2792 --- /dev/null +++ b/.github/workflows/scripts/run_accessibility_scan.js @@ -0,0 +1,81 @@ +module.exports = { + runAccessibilityScan: (pages) => { + const core = require('@actions/core'); + const { AxePuppeteer } = require('@axe-core/puppeteer'); + const puppeteer = require('puppeteer'); + + const violations = []; + + // When flipping from dark mode to light mode, we need to add a small timeout + // to account for css transitions otherwise there can be false contrast issues found. + // Usage: await sleep(300); + const sleep = ms => new Promise(res => setTimeout(res, ms)); + + const logViolation = (violation) => { + violation.nodes.forEach(node => { + console.log(node.failureSummary); + console.log(node.html); + node.target.forEach( target => { + console.log('CSS target: ', target) + }) + console.log('\n'); + }) + + } + + async function runAxeAnalyze(pages) { + for (const page of pages) { + const browser = await puppeteer.launch(); + const pageToVisit = await browser.newPage(); + await pageToVisit.goto(`http://localhost:3000${page}/`, {waitUntil: 'domcontentloaded'}); + await pageToVisit.click('button[title="Light mode"]'); + await pageToVisit.waitForSelector('[data-amplify-color-mode="light"]'); + await sleep(300); + + + try { + console.log(`\nTesting light mode: http://localhost:3000${page}/`) + const results = await new AxePuppeteer(pageToVisit).analyze(); + if(results.violations.length > 0) { + results.violations.forEach(violation => { + logViolation(violation); + violations.push(violation); + }) + } else { + console.log('No violations found. \n'); + } + + } catch (error) { + core.setFailed(`There was an error testing the page: ${error}`); + } + + await pageToVisit.click('button[title="Dark mode"]'); + await pageToVisit.waitForSelector('[data-amplify-color-mode="dark"]'); + await sleep(300); + + try { + console.log(`\nTesting dark mode: http://localhost:3000${page}/`) + const results = await new AxePuppeteer(pageToVisit).analyze(); + if(results.violations.length > 0) { + results.violations.forEach(violation => { + logViolation(violation); + violations.push(violation); + }) + } else { + console.log('No violations found. \n'); + } + + } catch (error) { + core.setFailed(`There was an error testing the page: ${error}`); + } + + await browser.close(); + } + if(violations.length > 0) { + core.setFailed(`Please fix the above accessibility violations.`); + } + } + + runAxeAnalyze(pages); + } +}; diff --git a/Readme.md b/Readme.md index 623de18b290..1552e43cfe0 100644 --- a/Readme.md +++ b/Readme.md @@ -176,6 +176,21 @@ Videos can be added using the `