Selenium＋axeでWebアクセシビリティの世界に足を踏み入れる

この記事は[ベリサーブ Advent Calendar 2020 - Qiita](https://qiita.com/advent-calendar/2020/veriserve)の５日めです。昨日は[Remote TestKitの基本機能を使ってみた - Qiita](https://qiita.com/vZke/items/de7ecb7944c2fd5456cc)でした。

---

普段Seleniumなどで機能テストの自動化を行っているのですが、Seleniumと組み合わせてアクセシビリティのテストができるaxeというものがあると聞き、試しに使ってみることにしました。

本家が[dequelabs/axe-core: Accessibility engine for automated Web UI testing](https://github.com/dequelabs/axe-core)で、今回はPython向けのライブラリ[axe-selenium-python · PyPI](https://pypi.org/project/axe-selenium-python/)を使います。

「ほげほげを使ってみた／触ってみた」だとそれほど意味がないじゃないか、と思う派なのですが、上記のPython向けライブラリのページには「解析結果はどのような形で出てくるのか」や「どんなルールに沿ってテストしてくれるのか」があんまり書いてないので、そのあたりを本記事で補えればと思います。

また、アクセシビリティのテストってよくわからん、やったことないや、という方もaxeの出力するレポートを通じて色々と学ぶところがありそうです。（私がそうでした）

## インストール

基本はPyPIに書いてあるとおりに進みます。pipでインストール。

``` zsh
% pip install axe-selenium-python
Collecting axe-selenium-python
  Downloading axe_selenium_python-2.1.6-py2.py3-none-any.whl (86 kB)
     |████████████████████████████████| 86 kB 4.5 MB/s 

中略

Installing collected packages: axe-selenium-python
Successfully installed axe-selenium-python-2.1.6
```
すぐ終わります。

## サンプルコードを動かす

こちらもPyPIにあるサンプルをまずは動かしてみましょう。
Chromeを使いたいので、サンプルを一部書き換えて、[webdriver-manager · PyPI](https://pypi.org/project/webdriver-manager/)を使うようにしています。

``` python
from selenium import webdriver
from axe_selenium_python import Axe
from webdriver_manager.chrome import ChromeDriverManager


def test_google():
    driver = webdriver.Chrome(ChromeDriverManager().install())
    driver.get("http://www.google.com")
    axe = Axe(driver)
    # Inject axe-core javascript into page.
    axe.inject()
    # Run axe accessibility checks.
    results = axe.run()
    # Write results to file
    axe.write_results(results, 'a11y.json')
    driver.close()
    # Assert no violations are found
    assert len(results["violations"]) == 0, axe.report(results["violations"])
```

これをpytestで動かします。

``` zsh
% pytest test_axe.py
=================================================================================== test session starts ====================================================================================
platform darwin -- Python 3.8.2, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yoshikiito/Documents/Temp
collected 1 item                                                                                                                                                                           

test_axe.py F                                                                                                                                                                        [100%]

========================================================================================= FAILURES =========================================================================================
_______________________________________________________________________________________ test_google ________________________________________________________________________________________

    def test_google():
        driver = webdriver.Chrome(ChromeDriverManager().install())
        driver.get("http://www.google.com")
        axe = Axe(driver)
        # Inject axe-core javascript into page.
        axe.inject()
        # Run axe accessibility checks.
        results = axe.run()
        # Write results to file
        axe.write_results(results, 'a11y.json')
        driver.close()
        # Assert no violations are found
>       assert len(results["violations"]) == 0, axe.report(results["violations"])
E       AssertionError: Found 8 accessibility violations:
E         
E         
E         Rule Violated:
E         aria-allowed-role - Ensures role attribute has an appropriate value for the element
E               URL: https://dequeuniversity.com/rules/axe/3.1/aria-allowed-role?application=axeAPI
E               Impact Level: minor
E               Tags: cat.aria best-practice
E               Elements Affected:
E               1) Target: .gLFyf
E                       role combobox  is not allowed for given element
E         


（中略）

E         
E       assert 8 == 0
E        +  where 8 = len([{'description': 'Ensures role attribute has an appropriate value for the element', 'help': 'ARIA role must be appropr...'https://dequeuniversity.com/rules/axe/3.1/landmark-one-main?application=axeAPI', 'id': 'landmark-one-main', ...}, ...])

test_axe.py:18: AssertionError
================================================================================= short test summary info ==================================================================================
FAILED test_axe.py::test_google - AssertionError: Found 8 accessibility violations:
==================================================================================== 1 failed in 10.10s ====================================================================================
```

Googleの検索ページですがずらずらとエラーが出てきました。８つのアクセシビリティ違反があるようです。

## 結果を見て理解を試みる

たとえば上記の`role combobox  is not allowed for given element`について。

まずそもそもエラー中に出てくるariaってなんぞや、からなのですが、以下が参考になります。
[ARIA - アクセシビリティ | MDN](https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA)

> Accessible Rich Internet Applications (ARIA) は Web コンテンツや Web アプリケーション (特に Ajax や JavaScript や Bootstrap のようなより最新のウェブ技術を伴って開発するもの) を、ハンディキャップを持つ人々にとってよりアクセシブルにする方法を定義します。例えば、ARIA はナビゲーションの目印、JavaScript ウィジェット、フォームのヒントやエラーメッセージ、動的なコンテンツ更新などをアクセシブルにします。

`role combobox  is not allowed for given element`では、ARIAにおけるrole、今回のエラー箇所でいうと`combobox`というロールは、該当の要素（今回はinput）に対して許可されていませんよ、ということのようです。

（書いてる時点でドラフトですが）W3Cの該当箇所[ARIA in HTML](https://www.w3.org/TR/html-aria/)を見てみると以下のような記載がありました。

![ScreenShot 2020-12-04 15.04.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/fde4e37e-6225-8d65-5f2e-662a98c51c58.png)

今回違反といわれている箇所のタグが以下です。

``` html
<input class="gLFyf gsfi" maxlength="2048" name="q" type="text" jsaction="paste:puy29d" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" autofocus="" role="combobox" spellcheck="false" title="検索" value="" aria-label="検索" data-ved="0ahUKEwiH-6_10LPtAhUT7WEKHZFYCC4Q39UDCAY">
```

> input type=text or with a missing or invalid type, with no list attribute

に該当する＝comboboxはroleとして適切、な気がするのですが・・・もう少し勉強が必要なようです。

## 別のサイトでも試してみよう

Googleのトップページでアクセシビリティを見てもあまり・・・だと思ったので、試しに自分のブログ（[テストウフ](https://yoshikiito.net/)）でやってみました。

結果は以下のとおり。

``` zsh
（中略）

E         
E         
E         Rule Violated:
E         color-contrast - Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds
E               URL: https://dequeuniversity.com/rules/axe/3.1/color-contrast?application=axeAPI
E               Impact Level: serious
E               Tags: cat.color wcag2aa wcag143
E               Elements Affected:
E               1) Target: #nav > li:nth-child(1) > a
E                       Element has insufficient color contrast of 2.47 (foreground color: #42b983, background color: #ffffff, font size: 12.0pt, font weight: bold). Expected contrast ratio of 4.5:1
E               2) Target: #nav > li:nth-child(2) > a
E                       Element has insufficient color contrast of 2.47 (foreground color: #42b983, background color: #ffffff, font size: 12.0pt, font weight: bold). Expected contrast ratio of 4.5:1


（中略）


E         
E         
E         Rule Violated:
E         frame-title - Ensures <iframe> and <frame> elements contain a non-empty title attribute
E               URL: https://dequeuniversity.com/rules/axe/3.1/frame-title?application=axeAPI
E               Impact Level: serious
E               Tags: cat.text-alternatives wcag2a wcag241 wcag412 section508 section508.22.i
E               Elements Affected:
E               1) Target: div[data-reactroot=""] > iframe
E                       aria-label attribute does not exist or is empty
E                       aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
E                       Element has no title attribute or the title attribute is empty
E                       Element's default semantics were not overridden with role="presentation"
E                       Element's default semantics were not overridden with role="none"
E         
E         
E         
E         
E         
E         Rule Violated:
E         html-has-lang - Ensures every HTML document has a lang attribute
E               URL: https://dequeuniversity.com/rules/axe/3.1/html-has-lang?application=axeAPI
E               Impact Level: serious
E               Tags: cat.language wcag2a wcag311
E               Elements Affected:
E               1) Target: html
E                       The <html> element does not have a lang attribute
E         
E         
E         
E         
E         
E         Rule Violated:
E         image-alt - Ensures <img> elements have alternate text or a role of none or presentation
E               URL: https://dequeuniversity.com/rules/axe/3.1/image-alt?application=axeAPI
E               Impact Level: critical
E               Tags: cat.text-alternatives wcag2a wcag111 section508 section508.22.a
E               Elements Affected:
E               1) Target: img
E                       Element does not have an alt attribute
E                       aria-label attribute does not exist or is empty
E                       aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
E                       Element has no title attribute or the title attribute is empty
E                       Element's default semantics were not overridden with role="presentation"
E                       Element's default semantics were not overridden with role="none"
E         
E         
E         
E         
E         
E         Rule Violated:
E         link-name - Ensures links have discernible text
E               URL: https://dequeuniversity.com/rules/axe/3.1/link-name?application=axeAPI
E               Impact Level: serious
E               Tags: cat.name-role-value wcag2a wcag412 wcag244 section508 section508.22.a
E               Elements Affected:
E               1) Target: .c6.mcol:nth-child(1) > .li > a
E                       Element does not have text that is visible to screen readers
E                       aria-label attribute does not exist or is empty
E                       aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
E                       Element's default semantics were not overridden with role="presentation"
E                       Element's default semantics were not overridden with role="none"
E                       Element is in tab order and does not have accessible text
E               2) Target: .c6.mcol:nth-child(1) > .li > .inner > .facts > li > a:nth-child(1)
E                       Element does not have text that is visible to screen readers
E                       aria-label attribute does not exist or is empty
E                       aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
E                       Element's default semantics were not overridden with role="presentation"
E                       Element's default semantics were not overridden with role="none"
E                       Element is in tab order and does not have accessible text


（中略）


E         
E         
E         
E         Rule Violated:
E         list - Ensures that lists are structured correctly
E               URL: https://dequeuniversity.com/rules/axe/3.1/list?application=axeAPI
E               Impact Level: serious
E               Tags: cat.structure wcag2a wcag131
E               Elements Affected:
E               1) Target: .c6.mcol:nth-child(1) > .li > .inner > .facts
E                       List element has direct children that are not allowed inside <li> elements
E               2) Target: .c6.mcol:nth-child(2) > .li > .inner > .facts
E                       List element has direct children that are not allowed inside <li> elements


（中略）

E         
E         
E         Rule Violated:
E         listitem - Ensures <li> elements are used semantically
E               URL: https://dequeuniversity.com/rules/axe/3.1/listitem?application=axeAPI
E               Impact Level: serious
E               Tags: cat.structure wcag2a wcag131
E               Elements Affected:
E               1) Target: .c6.mcol:nth-child(1) > .li > .inner > .facts > a > li
E                       List item does not have a <ul>, <ol> or role="list" parent element
E               2) Target: .c6.mcol:nth-child(2) > .li > .inner > .facts > a > li
E                       List item does not have a <ul>, <ol> or role="list" parent element

（中略）

E         
E         Rule Violated:
E         page-has-heading-one - Ensure that the page, or at least one of its frames contains a level-one heading
E               URL: https://dequeuniversity.com/rules/axe/3.1/page-has-heading-one?application=axeAPI
E               Impact Level: moderate
E               Tags: cat.semantics best-practice
E               Elements Affected:
E               1) Target: html
E                       Page must have a level-one heading
E         
E         
E         
E       assert 8 == 0
E        +  where 8 = len([{'description': 'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresho...te> elements', 'helpUrl': 'https://dequeuniversity.com/rules/axe/3.1/list?application=axeAPI', 'id': 'list', ...}, ...])

test_axe.py:18: AssertionError

================================================================================= short test summary info ==================================================================================
FAILED test_axe.py::test_google - AssertionError: Found 8 accessibility violations:
==================================================================================== 1 failed in 42.56s ====================================================================================
```

Googleのトップのときと同じく８件の指摘があったのですが、これは８種類という意味で、それぞれの指摘の中では細かく「こことここと」というのを示してくれています。

自分のブログでやったときのほうが指摘事項がだいぶわかりやすい印象でした。

- color-contrast - Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds
- frame-title - Ensures <iframe> and![ScreenShot 2020-12-04 15.19.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/ac0e0805-2a88-9a01-c29f-548fdd0c16a8.png)
 <frame> elements contain a non-empty title attribute
- html-has-lang - Ensures every HTML document has a lang attribute
- image-alt - Ensures <img> elements have alternate text or a role of none or presentation
- link-name - Ensures links have discernible text
- list - Ensures that lists are structured correctly
- listitem - Ensures <li> elements are used semantically
- page-has-heading-one - Ensure that the page, or at least one of its frames contains a level-one heading

こうやってサイトを静的にチェックしてくれて詳細な指摘を出してくれるのはとても便利そうです。

テスターもしくはテストエンジニアが直接axeでアクセシビリティを見る、というよりは、開発者がCIの中に組み込んで指摘事項を確認し、必要あれば対応する、といった使い方ですかね。

## どんなルールでチェックしてくれるのか

[axe-core/rule-descriptions.md at develop · dequelabs/axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md)

ここにルールの一覧があります。

## Chrome拡張もあります

[axe - Web Accessibility Testing - Chrome ウェブストア](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=ja)

![ScreenShot 2020-12-04 15.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/93617237-a997-75a5-cf0d-98cab24f9c17.png)

こっちのほうが見やすい・・・