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"