Skip to content

Commit

Permalink
Merge pull request #880 from YasushiKobayashi/feature/axe
Browse files Browse the repository at this point in the history
add 「axeを活用して、アクセシビリティをちゃんと理解しなくても、アクセシビリティを担保したhtmlを書く」
  • Loading branch information
YasushiKobayashi committed Jun 5, 2023
2 parents 46b5b98 + a11dc53 commit a58adbf
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
## hygen/Jest/Storybookでテストカバレッジが自然と上がっていく開発環境作り
- https://zenn.dev/ptpadan/articles/hygen-storybook-jest

## axeを活用して、アクセシビリティをちゃんと理解しなくても、アクセシビリティを担保したhtmlを書く
- https://zenn.dev/ptpadan/articles/axe-accessibility

# setup

```bash
Expand Down
113 changes: 113 additions & 0 deletions articles/axe-accessibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: axeを活用して、アクセシビリティをちゃんと理解しなくても、アクセシビリティを担保したhtmlを書く
emoji: "📚"
type: "tech" # tech: 技術記事 / idea: アイデア
topics: ['Jest', 'hygen', 'Playwgiht', 'axe', 'Accessibility']
published: true
---

アクセシビリティにはあまり詳しくないので、アクセシビリティ上問題があるコードを書いてしまっても気づくことがこれまでできませんでしたが、axe を使用することでアクセシビリティをある程度担保したコードを書けているかテストをできるようになりました。

`jest-axe``@axe-core/playwright` を使用したテストを CI で回すことで、アクセシビリティを継続的に担保していくことができます。

## jest axeを活用してコンポーネントのアクセシビリティ担保

`jest-axe`を使用することでコンポーネント単位でアクセシビリティテストをできます。

コンポーネント単位でテストができることで、1 つ1つアクセシビリティ的に正しいコンポーネントを作ることができ、小さい単位でアクセシビリティの改善をしていくことができるので既存プロジェクトにも導入がしやすいです。


## jest-axe のセットアップ

まず `jest.setup.js` に axe のセットアップをします。

```typescript
import { toHaveNoViolations } from 'jest-axe'
import 'jest-axe/extend-expect'

expect.extend(toHaveNoViolations)
```

次に axe を実行するための、ラッパーの関数を用意して実行するようにしています。
自分はこちらのように axe のラッパーを用意してこのサンプルリポジトリでは、特に除外の設定をしていないですが、導入しているプロジェクトでは担保することが難しいルールを基本除外にしています。

https://github.com/YasushiKobayashi/samples/blob/master/src/next-sample/src/testUtils/axeRunner.ts

```typescript
import { configureAxe } from 'jest-axe'

export const axeRunner = configureAxe({
rules: {
'image-alt': { enabled: false },
'link-name': { enabled: false },
},
})
```


https://github.com/YasushiKobayashi/samples/blob/master/src/next-sample/src/templates/Top/Top.spec.tsx#L22

## コンポーネントベースで不要なものは都度除外

コンポーネント単位でテストをする場合どうしても、input タグだけのコンポーネントなのに label がないとエラーになってもどうしようもないということがあるので、このコンポーネントではこのルールを除外したいということがあった場合は、都度除外することでテストを通すことができます。

また、すべてのルールにちゃんとドキュメントがあり、今守りたいルールなのか考えながら導入していくことが可能です。

下記のように基本的に日本語に翻訳されているルールも多いです。

https://dequeuniversity.com/rules/axe/4.4/label?lang=ja

## hygenでアクセシビリティテストをコンポーネント作成フローに組込む

下記の記事でも書いたように、最近はコンポーネントを作成する時は hygen を使用してテンプレートを元にテストファイルまで作成しているので、テンプレートで必ず axe のテストをするように仕組みづくりができています。

https://zenn.dev/ptpadan/articles/hygen-storybook-jest

## Playwright axeでE2Eテストでもアクセシビリティテストする

`@axe-core/playwright` を使うことで、E2E テストの中でもアクセシビリティテストができます。

全てに対応しようとすると難しいものも多いので対応できてないルールも多いのですが、1 つのコンポーネントをテストするだけでは検出できるできないエラーを発見でき、例えばこのように h1 タグに関するエラーを出してくれます。

```typescript
{
id: 'page-has-heading-one',
impact: 'moderate',
tags: [ 'cat.semantics', 'best-practice' ],
description: 'Ensure that the page, or at least one of its frames contains a level-one heading',
help: 'Page should contain a level-one heading',
helpUrl: 'https://dequeuniversity.com/rules/axe/4.4/page-has-heading-one?application=playwright',
nodes: [ [Object] ]
},
```

### Playwright axeのセットアップ

Playwright axe でも同様に axe を実行するためのラッパーを用意し、都度ページの状態が変わるたびにテストを実行するようにしています。

完全な運用は難しそうなので、エラーが見つかった際にテストを落とす運用はまだしていないです。

```typescript
import AxeBuilder from '@axe-core/playwright'

export const axeRunner = async (page: Page, disableRules: string[] = []) => {
const results = await new AxeBuilder({ page })
.disableRules(['image-alt', 'color-contrast', 'meta-viewport', 'link-name', ...disableRules])
.analyze()

if (results.violations.length > 0) {
const title = await page.title()
const url = page.url()
console.error(title, url, results.violations)
// test.fail()
}
}
```

Playwright のセットアップやテストの書き方については、こちらを参考にしてください Playwright が実行できていればすぐに上記のような方法でテスト可能です。。

https://zenn.dev/ptpadan/articles/playwright-e2e

今回サンプルコードにした内容や、動作確認で使用したコードは全てこちらの PR で作成しており、すべて動作確認可能です。

https://github.com/YasushiKobayashi/samples/pull/880
8 changes: 6 additions & 2 deletions src/playwright-sample/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@

import { expect, test } from '@playwright/test'

import { baseDir, baseUrl } from './testUtils'
import { baseDir, baseUrl, testPage } from './testUtils'

test.describe.parallel('test', () => {
test('success', async ({ page }, testInfo) => {
let i = 1
const basePath = `${baseDir}/${testInfo.title}`
await page.goto(baseUrl)
await page.screenshot({ path: `${basePath}/${i++}.png`, fullPage: true })
await testPage(page, basePath)

await page.getByLabel('First Name').fill('hoge')
await expect(await page.getByLabel('First Name').inputValue()).toBe('hoge')

await page.screenshot({ path: `${basePath}/${i++}.png`, fullPage: true })
await testPage(page, basePath)

await page.getByLabel('Last Name').fill('huga')
await expect(await page.getByLabel('Last Name').inputValue()).toBe('huga')
await page.screenshot({ path: `${basePath}/${i++}.png`, fullPage: true })
await testPage(page, basePath)

await page.getByRole('button', { name: 'submit' }).click()
await page.screenshot({ path: `${basePath}/${i++}.png`, fullPage: true })
await testPage(page, basePath)

await expect(await page.getByLabel('First Name').inputValue()).toBe('')
await expect(await page.getByLabel('Last Name').inputValue()).toBe('')
Expand Down
23 changes: 23 additions & 0 deletions src/playwright-sample/src/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
import AxeBuilder from '@axe-core/playwright'
import { Page } from '@playwright/test'

export const baseUrl = 'http://localhost:3000/'
export const baseDir = 'test-results/screenshots'

const axeRunner = async (page: Page, disableRules: string[] = []) => {
const results = await new AxeBuilder({ page })
.disableRules(['image-alt', 'color-contrast', 'meta-viewport', 'link-name', ...disableRules])
.analyze()

if (results.violations.length > 0) {
const title = await page.title()
const url = page.url()
console.error(title, url, results.violations)
// test.fail()
}
}

let i = 1
export const testPage = async (page: Page, basePath: string, disableRules: string[] = []) => {
await axeRunner(page, disableRules)
// eslint-disable-next-line no-plusplus
await page.screenshot({ path: `${basePath}/${i++}.png`, fullPage: true })
}

0 comments on commit a58adbf

Please sign in to comment.