diff --git a/.github/workflows/accessibility_test.yml b/.github/workflows/accessibility_test.yml new file mode 100644 index 00000000000..9e1700c3b2e --- /dev/null +++ b/.github/workflows/accessibility_test.yml @@ -0,0 +1,45 @@ +name: Accessibility +on: + pull_request: + branches: [main] + types: [opened, synchronize] +env: + BUILD_DIR: 'client/www/next-build' +jobs: + accessibility: + name: Runs axe accessibility testing 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'); + return getChangedPages({github, context}, env.BUILD_DIR); + - 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 { runAxe } = require('./.github/workflows/scripts/run_axe.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..10a9391ba55 --- /dev/null +++ b/.github/workflows/scripts/check_for_changed_pages.js @@ -0,0 +1,64 @@ +module.exports = { + getChangedPages: ({ github, context }, buildDir) => { + console.log('buildDir: ', 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_axe.js b/.github/workflows/scripts/run_axe.js new file mode 100644 index 00000000000..1437ba96524 --- /dev/null +++ b/.github/workflows/scripts/run_axe.js @@ -0,0 +1,38 @@ +module.exports = { + runAxe: (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) { + results.violations.forEach(violation => { + console.log(violation); + violations.push(violation); + }) + } else { + console.log('No violations found.'); + } + + } catch (e) { + // do something with the 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 b5d0104eaf5..47cb3072e78 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "react-icons": "^4.7.1" }, "devDependencies": { + "@actions/core": "^1.10.1", + "@axe-core/puppeteer": "^4.9.0", + "@axe-core/react": "^4.9.0", "@mdx-js/loader": "^2.3.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", diff --git a/src/pages/[platform]/build-a-backend/index.mdx b/src/pages/[platform]/build-a-backend/index.mdx index 74a0e1ac4a2..b01510c7727 100644 --- a/src/pages/[platform]/build-a-backend/index.mdx +++ b/src/pages/[platform]/build-a-backend/index.mdx @@ -33,3 +33,5 @@ export function getStaticProps(context) { } + +![](/images/cli/user-creation/access-keys-done.png) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index fbf461c76be..022c6e2cf93 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,6 +6,7 @@ import { Layout } from '@/components/Layout'; import { useRouter } from 'next/router'; import { useEffect } from 'react'; import { trackPageVisit } from '../utils/track'; +import { accessibilityScanner } from '@/utils/accessibilityScanner'; function MyApp({ Component, pageProps }) { const { @@ -186,4 +187,6 @@ function MyApp({ Component, pageProps }) { ); } +accessibilityScanner(MyApp); + export default MyApp; diff --git a/src/utils/accessibilityScanner.ts b/src/utils/accessibilityScanner.ts new file mode 100644 index 00000000000..02c8a2a05f9 --- /dev/null +++ b/src/utils/accessibilityScanner.ts @@ -0,0 +1,10 @@ +export const accessibilityScanner = async ( + App, + config?: Record +): Promise => { + if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') { + const axe = await import('@axe-core/react'); + const ReactDOM = await import('react-dom'); + axe.default(App, ReactDOM, 1000, config); + } +}; diff --git a/yarn.lock b/yarn.lock index 175427c4800..7d80d0e1596 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" @@ -903,6 +919,21 @@ dependencies: tslib "^2.3.1" +"@axe-core/puppeteer@^4.9.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@axe-core/puppeteer/-/puppeteer-4.9.0.tgz#13db7765e86e362f48c9958132f92ec9c24679b2" + integrity sha512-hSlFjfJ6SzrE/XLJllNYbnz5ZgD6MI5+WsNXFfBf1c1I/Zq0jsl147RvHtgMCkgZMIltV5LDupnqj5Uyt8i6Lw== + dependencies: + axe-core "~4.9.0" + +"@axe-core/react@^4.9.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@axe-core/react/-/react-4.9.0.tgz#51197c20a9ef72ebef0dbf3c35a1e49c0f01c00d" + integrity sha512-xtqnkFcdxT/T6JD9/hc5Wzv15+m0Qj6VaQCebeIBEveZBOY9nfD6/JIuYuCgWLrgjX+TFgb74nni8XMJvAhVMA== + dependencies: + axe-core "~4.9.0" + requestidlecallback "^0.3.0" + "@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" @@ -1654,6 +1685,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" @@ -3719,6 +3755,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.0: + 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" @@ -9733,6 +9774,11 @@ repeat-string@^1.6.1: resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== +requestidlecallback@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/requestidlecallback/-/requestidlecallback-0.3.0.tgz#6fb74e0733f90df3faa4838f9f6a2a5f9b742ac5" + integrity sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" @@ -10746,6 +10792,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" @@ -10876,6 +10927,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"