Skip to content

Commit

Permalink
feat: add accessibility Github Actions workflow (#7669)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hbuchel authored Jun 7, 2024
1 parent 6bf1edb commit db85581
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 0 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/accessibility_scan.yml
Original file line number Diff line number Diff line change
@@ -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)
74 changes: 74 additions & 0 deletions .github/workflows/scripts/check_for_changed_pages.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
81 changes: 81 additions & 0 deletions .github/workflows/scripts/run_accessibility_scan.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
15 changes: 15 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ Videos can be added using the `<Video />` component and referencing a path to th
<Video src="/path/to/video.mp4" />
```

## Accessibility testing

We run automated accessibility testing on pages affected by a pull request in both light and dark mode. Some common violations and their solutions are listed here:

```
Element has insufficient color contrast
```
[Text colors must have a contrast ratio of 4.5:1 against their background](https://www.w3.org/TR/WCAG22/#contrast-minimum). Avoid adding custom styling to text; our design system should ensure that accessible color combinations are in use.

```
Heading order invalid
```
Headings should be properly nested. This means, for example, an `<h3>` element must be nested beneath an `<h2>` element on the page, an `<h4>` must be nested beneath an `<h3>` and so on. Use [markdown syntax to create your headings](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#headings).


## Debug client-side code with browser developer tools

### Prerequisites
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -10807,6 +10840,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"
Expand Down Expand Up @@ -10937,6 +10975,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"
Expand Down

0 comments on commit db85581

Please sign in to comment.