From f05ca2e628689103bec006a4f8957f8c4a2118df Mon Sep 17 00:00:00 2001 From: Heather Date: Tue, 28 May 2024 13:46:03 -0400 Subject: [PATCH 1/5] add accessibility workflow --- .github/workflows/accessibility_scan.yml | 46 +++++++++++ .../scripts/check_for_changed_pages.js | 63 +++++++++++++++ .../scripts/run_accessibility_scan.js | 38 ++++++++++ package.json | 2 + yarn.lock | 76 ++++++++++++------- 5 files changed, 197 insertions(+), 28 deletions(-) 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..208d02ce63b --- /dev/null +++ b/.github/workflows/accessibility_scan.yml @@ -0,0 +1,46 @@ +name: axe +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@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 https://github.com/actions/checkout/commit/9bb56186c3b09b4f86b1c65136769dd318469633 + - name: Setup Node.js 20 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # 4.0.2 https://github.com/actions/setup-node/releases/tag/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 https://github.com/actions/github-script/commit/60a0d83039c74a4aee543508d2ffcb1c3799cdea + 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 https://github.com/actions/github-script/commit/60a0d83039c74a4aee543508d2ffcb1c3799cdea + 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 runAxe(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..d8d0dfe61ec --- /dev/null +++ b/.github/workflows/scripts/check_for_changed_pages.js @@ -0,0 +1,63 @@ +module.exports = { + getChangedPages: ({ github, context, buildDir }) => { + const fs = require('fs'); + const cheerio = require('cheerio'); + + const urlList = []; + + const { + issue: { number: issue_number }, + repo: { owner, repo } + } = context; + + // Use the Github API to query for the list of files from the PR + return 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')) + ) + .then((files) => { + const possiblePages = []; + const platforms = [ + 'android', + 'angular', + 'flutter', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue', + ] + files.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); + } + } + }); + + 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()); + }); + + 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..692a730fd4d --- /dev/null +++ b/.github/workflows/scripts/run_accessibility_scan.js @@ -0,0 +1,38 @@ +module.exports = { + runAccessibilityScan: (pages) => { + const core = require('@actions/core'); + const { AxePuppeteer } = require('@axe-core/puppeteer'); + const puppeteer = require('puppeteer'); + + const violations = []; + + async function runAxeAnalyze(pages) { + for (const page of pages) { + console.log(`testing page http://localhost:3000${page}/`); + const browser = await puppeteer.launch(); + const pageToVisit = await browser.newPage(); + await pageToVisit.goto(`http://localhost:3000${page}/`); + try { + const results = await new AxePuppeteer(pageToVisit).analyze(); + if(results.violations.length > 0) { + results.violations.forEach(violation => { + console.log(violation); + violations.push(violation); + }) + } else { + console.log('No violations found.'); + } + + } catch (error) { + core.setFailed(`There was an error running the accessibility scan: `, error); + } + await browser.close(); + } + if(violations.length > 0) { + core.setFailed(`Please fix the above accessibility violations.`); + } + } + + runAxeAnalyze(pages); + } +}; diff --git a/package.json b/package.json index f7b9db0b0fb..5fda02c84cf 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "react-icons": "^4.7.1" }, "devDependencies": { + "@actions/core": "^1.10.1", + "@axe-core/puppeteer": "^4.9.1", "@mdx-js/loader": "^2.3.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 47f54f3143f..bdd12409432 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,22 @@ resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@actions/core@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.1.tgz#61108e7ac40acae95ee36da074fa5850ca4ced8a" + integrity sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g== + dependencies: + "@actions/http-client" "^2.0.1" + uuid "^8.3.2" + +"@actions/http-client@^2.0.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.1.tgz#ed3fe7a5a6d317ac1d39886b0bb999ded229bb38" + integrity sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw== + dependencies: + tunnel "^0.0.6" + undici "^5.25.4" + "@adobe/css-tools@4.3.2", "@adobe/css-tools@^4.3.2": version "4.3.2" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" @@ -904,6 +920,13 @@ dependencies: tslib "^2.3.1" +"@axe-core/puppeteer@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@axe-core/puppeteer/-/puppeteer-4.9.1.tgz#93952d1acea839c623c56899d2cfb8b8ae8fa19d" + integrity sha512-eakSzSS0Zmk7EfX2kUn1jfZsO7gmvjhNnwvBxv9o6HXvwZE5ME/CTi3v2HJMvC+dn3LlznEEdzBB87AyHvcP5A== + dependencies: + axe-core "~4.9.1" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz" @@ -1655,6 +1678,11 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz" integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ== +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + "@floating-ui/core@^0.7.3": version "0.7.3" resolved "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz" @@ -3761,6 +3789,11 @@ axe-core@=4.7.0: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axe-core@~4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae" + integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw== + axios@^1.3.4: version "1.6.7" resolved "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz" @@ -10318,16 +10351,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10411,14 +10435,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10798,6 +10815,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -10928,6 +10950,13 @@ unbzip2-stream@1.4.3: buffer "^5.2.1" through "^2.3.8" +undici@^5.25.4: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + unified@^10.0.0: version "10.1.2" resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz" @@ -11499,7 +11528,7 @@ winston@^3.3.3: triple-beam "^1.3.0" winston-transport "^4.5.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11517,15 +11546,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From aa57488bae71baf9e8f1b9085a0739ec5136b9b8 Mon Sep 17 00:00:00 2001 From: Heather Date: Tue, 28 May 2024 13:52:18 -0400 Subject: [PATCH 2/5] Update name --- .github/workflows/accessibility_scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/accessibility_scan.yml b/.github/workflows/accessibility_scan.yml index 208d02ce63b..0c1b6973bac 100644 --- a/.github/workflows/accessibility_scan.yml +++ b/.github/workflows/accessibility_scan.yml @@ -1,4 +1,4 @@ -name: axe +name: Accessibility Scan on: pull_request: branches: [main] From ca6d5155a2a5b3c1a2a696521226f6436e810757 Mon Sep 17 00:00:00 2001 From: Heather Date: Tue, 28 May 2024 14:03:03 -0400 Subject: [PATCH 3/5] fix function name --- .github/workflows/accessibility_scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/accessibility_scan.yml b/.github/workflows/accessibility_scan.yml index 0c1b6973bac..e651888c2be 100644 --- a/.github/workflows/accessibility_scan.yml +++ b/.github/workflows/accessibility_scan.yml @@ -43,4 +43,4 @@ jobs: script: | const { runAccessibilityScan } = require('./.github/workflows/scripts/run_accessibility_scan.js'); const pages = ${{ steps.pages-to-a11y-test.outputs.result }} - return await runAxe(pages) + return await runAccessibilityScan(pages) From 9920b7bd9e5b7abbf566bca12ab6877354fab62a Mon Sep 17 00:00:00 2001 From: Heather Date: Tue, 28 May 2024 15:58:55 -0400 Subject: [PATCH 4/5] add light mode/dark mode testing, clean up logging --- .../scripts/run_accessibility_scan.js | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scripts/run_accessibility_scan.js b/.github/workflows/scripts/run_accessibility_scan.js index 692a730fd4d..ff815016223 100644 --- a/.github/workflows/scripts/run_accessibility_scan.js +++ b/.github/workflows/scripts/run_accessibility_scan.js @@ -6,26 +6,68 @@ module.exports = { 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) + }) + }) + + } + async function runAxeAnalyze(pages) { for (const page of pages) { - console.log(`testing page http://localhost:3000${page}/`); const browser = await puppeteer.launch(); const pageToVisit = await browser.newPage(); - await pageToVisit.goto(`http://localhost:3000${page}/`); + 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 => { - console.log(violation); + logViolation(violation); violations.push(violation); }) } else { - console.log('No violations found.'); + console.log('No violations found. \n'); } } catch (error) { - core.setFailed(`There was an error running the accessibility scan: `, 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) { From 55a80b9191028c84e95075f2b9c429198e825a21 Mon Sep 17 00:00:00 2001 From: Heather Date: Tue, 28 May 2024 16:10:22 -0400 Subject: [PATCH 5/5] test violations --- src/pages/[platform]/build-a-backend/index.mdx | 2 ++ src/pages/[platform]/build-ui/index.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pages/[platform]/build-a-backend/index.mdx b/src/pages/[platform]/build-a-backend/index.mdx index 74a0e1ac4a2..e50a08c3128 100644 --- a/src/pages/[platform]/build-a-backend/index.mdx +++ b/src/pages/[platform]/build-a-backend/index.mdx @@ -32,4 +32,6 @@ export function getStaticProps(context) { }; } +### Test out of order heading + diff --git a/src/pages/[platform]/build-ui/index.mdx b/src/pages/[platform]/build-ui/index.mdx index bab6cab5a2f..eceef19d10b 100644 --- a/src/pages/[platform]/build-ui/index.mdx +++ b/src/pages/[platform]/build-ui/index.mdx @@ -32,6 +32,8 @@ export function getStaticProps(context) { }; } +
Test contrast issue
+ Amplify offers a [UI Library](https://ui.docs.amplify.aws) that makes it easy to build web app user interfaces that are connected to the backend. Amplify UI offers: * **Connected components** that are designed to work seamlessly with AWS Amplify backend services, allowing you to quickly add common UX patterns for authentication, storage etc. without having to build them from scratch.