Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add 「axeを活用して、アクセシビリティをちゃんと理解しなくても、アクセシビリティを担保したhtmlを書く」 #880

Merged
merged 3 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このパッチは、Jest-axeと@axe-core/playwrightを使用してアクセシビリティテストを実施するための設定や関数が含まれています。jest.setup.jsファイルにAxeのセットアップを行い、単位コンポーネントごとのテストを可能にします。さらにhygenを使用してテンプレート内でテストを作成し、Playwgiht axeを使ってE2Eテストでもアクセシビリティテストを実行します。また、除外設定を含めて各種ルールについても説明されています。提供されたサンプルリポジトリは、すべてのコードと動作確認の PR が含まれているため、参考にしやすいです。

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 })
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このパッチコードは、PlaywrightとAxeCoreを使用して、ページのアクセシビリティ問題を検出し、テスト結果をスクリーンショットとして保存するためのものです。コードに重大なバグのリスクは見当たりませんが、baseDiriなどの変数名はより具体的でわかりやすい名前が望まれます。また、axeRunner()関数内のエラーハンドリングが十分ではなく、ログを出力しただけでtest.fail()がコメントアウトされているため、障害発生時の処理を改善する必要があります。