アクセシビリティ対応が捗る自動チェックツール「aXe」って知ってる?

2017/05/01

Pavels Jelisejevs

154

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

アクセシビリティ対応は面倒だなと後回しにしているなら、自動チェックツールを試してみたら?

最近作成したWebサイトをデザインしたとき、特別なニーズや障害のある人にとってサイトがアクセシブルになるためにどのくらいの時間と労力を使いましたか。たぶん、多くの答えは「なにもしていない」だと思います。しかし、インターネットユーザーの中には、色を区別したり、テキストを読んだり、マウスを使ったり、あるいは、単にWebサイト構造が複雑でナビゲートするのに問題があるために、サイトのアクセスに苦労する人がたくさんいることを否定する人はいないと思います。

アクセシビリティへの配慮は、チェックしたり、解決策を実装したりするのに労力がかかるので、無視されがちです。Web開発者は根本となる基準を知っておくだけでなく、その基準が満たされているか常にチェックしなければなりません。自動的に基準をチェックして、もっと簡単にアクセシブルなWebサイトをつくれないでしょうか。

この記事では、作成するサイトやアプリケーションでアクセシビリティの問題を自動的にチェックしたり、レポートしたりするaXeライブラリーと関連ツールの使い方を紹介します。

aXeの紹介

aXeは、Web開発の現場で、普段からアクセシビリティテストができることを目的とした、アクセシビリティの自動テストライブラリーです。axe-coreライブラリーはオープンソースで、多くのテストフレームワーク、ツール、環境で使えるように設計されています。たとえば、機能テスト、ブラウザープラグイン、あるいは直接アプリケーションの開発版で実行できます。いまのところ、アクセシビリティのいろいろな面からWebサイトをチェックする約55個のルールをサポートしています。

ライブラリーがどう動くか簡単なデモをするために、シンプルなコンポーネントを作ってテストしてみます。ページ全体を作るわけではありません、ヘッダーだけです。

次のようなすばらしい考え方でデザインしたヘッダーを作成しました。

  1. 背景をライトグレーに、リンクをダークグレー。この組み合わせはおしゃれでスタイリッシュ
  2. 検索ボタンにかっこいい拡大鏡
  3. 検索入力のtab indexを1に設定。ユーザーがページを開いたとき、タブを押してただちに検索語を入力できる

よくできていると思いますね。それでは、アクセシビリティの観点ではどうでしょうか。次のスクリプトを使うと、CDNからaXeを実行して、エラーをすべてブラウザーのコンソールに出力できます。

axe.run(function (err, results) {
  if (results.violations.length) {
    console.warn(results.violations);
  }
});

例にスクリプトを適用してコンソールを開いてみると、違反オブジェクトが6つ表示され、問題が列挙されています。オブジェクトはそれぞれ違反したルール、問題となったhtml要素への参照、問題の解決方法のヘルプ情報を示しています。

JSONとして示された違反オブジェクトの例です。

[  
   {  
      "id":"button-name",
      "impact":"critical",
      "tags":[  
         "wcag2a",
         "wcag412",
         "section508",
         "section508.22.a"
      ],
      "description":"Ensures buttons have discernible text",
      "help":"Buttons must have discernible text",
      "helpUrl":"https://dequeuniversity.com/rules/axe/2.1/button-name?application=axeAPI",
      "nodes":[  
         {  
            "any":[  
               {  
                  "id":"non-empty-if-present",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"critical",
                  "message":"Element has a value attribute and the value attribute is empty"
               },
               {  
                  "id":"non-empty-value",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"critical",
                  "message":"Element has no value attribute or the value attribute is empty"
               },
               {  
                  "id":"button-has-visible-text",
                  "data":"",
                  "relatedNodes":[  

                  ],
                  "impact":"critical",
                  "message":"Element does not have inner text that is visible to screen readers"
               },
               {  
                  "id":"aria-label",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"critical",
                  "message":"aria-label attribute does not exist or is empty"
               },
               {  
                  "id":"aria-labelledby",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"critical",
                  "message":"aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible"
               },
               {  
                  "id":"role-presentation",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"moderate",
                  "message":"Element's default semantics were not overridden with role=\"presentation\""
               },
               {  
                  "id":"role-none",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"moderate",
                  "message":"Element's default semantics were not overridden with role=\"none\""
               }
            ],
            "all":[  

            ],
            "none":[  
               {  
                  "id":"focusable-no-name",
                  "data":null,
                  "relatedNodes":[  

                  ],
                  "impact":"serious",
                  "message":"Element is in tab order and does not have accessible text"
               }
            ],
            "impact":"critical",
            "html":"<button>\n      <i class=\"fa fa-search\"></i>\n    </button>",
            "target":[  
               "body > header > div > button"
            ],
            "failureSummary":"Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element has a value attribute and the value attribute is empty\n  Element has no value attribute or the value attribute is empty\n  Element does not have inner text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
         }
      ]
   },
]

違反として、次のように書いてあります。

Ensures buttons have discernible text
Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds
Ensures every HTML document has a lang attribute
Ensures <img> elements have alternate text or a role of none or presentation
Ensures every form element has a label
Ensures tabindex attribute values are not greater than 0

(違反コード概要:ボタンは識別可能なテキストであること/前景色と背景色のコントラストがWCAG 2レベルAAを満たすコントラスト比であること/HTMLドキュメントはすべてlang属性をもつこと/<img>要素は、代替テキストを持つか、そのrole属性をnoneあるいはpresentationとすること/すべてのform要素はラベルを持つこと/tabindex属性の値は0より大きい値を持たないこと)

結局、おしゃれ、スタイリッシュ、かっこいい、便利と思ったヘッダーは、あまり素敵ではなかったことが分かります。

  1. 2つの灰色のシェードはコントラストが十分でなく、視覚障害者には読みづらいおそれがある
  2. 検索ボタンの拡大鏡アイコンは、スクリーンリーダーを使っている人にとって、ボタンがなにを意図しているのかまったく分からない
  3. 検索入力のtab indexは、スクリーンリーダーやキーボードを使う人にとって、ナビゲーションの邪魔になり、メニューリンクにアクセスするのを難しくしている

デザインするときに考えなかった事項もいくつか指摘されています。aXeは、いろいろな基準ガイドラインやベストプラクティスに基づき、全部で55項目についてチェックしています。

エラーのリストを見るには、ページそのものにスクリプトを挿入しなければなりませんでした。やればできますが、あまり便利ではありません。スクリプトを挿入したりせずに、どのページでもチェックができたら良いと思います。Selenium WebDriverやMochaといったよく知られたテストランナーを使うと実現できます。

Selenium WebDriverでaXeを実行

Seleniumを使ってaXeを実行するには、axe-webdriverjsライブラリーを使います。 このライブラリーはWebDriverで使えるaXeのAPIを提供します。

セットアップするのに別のプロジェクトを作り、npm initコマンドを使ってnpmプロジェクトを初期化します。初期化中になにか尋ねられても、すべてデフォルト値のままで大丈夫です。記事ではPhantomJSでテストするので、Seleniumを使うにはselenium-webdriverをインストールする必要があります。SeleniumはNodeのバージョン6.9以降が必要なので、インストールされているか確かめてください。

パッケージをインストールするには次のようにします。

npm install phantomjs-prebuilt selenium-webdriver --save-dev

次にaxe-coreaxe-webdriverjsをインストールします。

npm install axe-core axe-webdriverjs --save-dev

準備ができたので、sitepint.comのテストをするスクリプトを作成します(別にSitePointに恨みがあるわけではありませんよ)。プロジェクトフォルダーにaxe.jsファイルを作って、次のコードを加えます。

const axeBuilder = require('axe-webdriverjs');
const webDriver = require('selenium-webdriver');

// create a PhantomJS WebDriver instance
const driver = new webDriver.Builder()
  .forBrowser('phantomjs')
  .build();

// run the tests and output the results in the console
driver
  .get('http://sitepoint.com')
  .then(() => {
    axeBuilder(driver)
      .analyze((results) => {
        console.log(results);
      });
  });

テストをするにはnode axe.jsを実行します。このプロジェクトではPhantomJSをローカルにインストールしたので、コンソールから実行できません。npmスクリプトとして実行する必要があります。そのためには、package.jsonファイルを開いて、デフォルトのテストスクリプトのエントリを変更します。

"scripts": {
    "test": "node axe.js"
},

npm testを実行します。何秒か待つとaXeが見つけた違反のリストが表示されます。なにも表示されなかったら、この記事を読んでSitePointがすべて修復したのかもしれません。

この方法は、ページをテストのために修正する必要はなく、CLIで簡単に実行できるので最初の方法よりずっと便利です。しかし、テストのために、別のスクリプトを実行しなければならないのが欠点です。すべてのテストが同時に実行できたほうが便利です。次に、Mochaを使う方法を説明します。

Mochaを使ってaXeを実行する

いまあるテストランナーで一番人気のMochaは、aXeを試すのに良い候補です。しかし、いつも自分が使っている作業環境にaXeを統合できるはずです。Seleniumのプロジェクト例をさらに進めます。

もちろんMocha自体とアサーションライブラリーが必要です。Chaiはどうでしょうか。これらを全部次のコマンドでインストールします。

npm install mocha chai --save-dev

次に、Mochaのテストケースで書いたSeleniumのコードをラップする必要があります。次のコードでtest/axe.spec.jsファイルを作ります。

const assert = require('chai').assert;
const axeBuilder = require('axe-webdriverjs');
const webDriver = require('selenium-webdriver');

const driver = new webDriver.Builder()
  .forBrowser('phantomjs')
  .build();

describe('aXe test', () => {
  it('should check the main page of SitePoint', () => {
    // a Mocha test case can be treated as asynchronous 
    // by returning a promise
    return driver.get('http://sitepoint.com/')
      .then(() => {
        return new Promise((resolve) => {
          axeBuilder(driver).analyze((results) => {
            assert.equal(results.violations.length, 0);

            resolve()
          });
        });
      })
      .then(() => driver.quit())
  })
  // The test might take some 5-10 seconds to execute, 
  // so we'll disable the timeout
  .timeout(0);
});

このテストは、results.violations配列の長さが0かどうかをチェックして、基本的なアサートを実行します。テストを実行するには、テストスクリプトがMochaを呼ぶように変更します。

"scripts": {
  "test": "mocha"
},

次のステップは、テストに通らなかったときさらに詳細なエラーレポートを表示することが考えられます。自分の気に入ったCI環境と統合して、ページの結果を適切に表示するのも便利です。これらは両方とも読者の練習問題としておきます。

記事は、ほかの便利なaXeのコンフィグレーションオプションの話題に移ります。

高度なコンフィグレーション

初期設定ではaXeはページ全体にデフォルトチェックをすべて実行します。しかし、テストするWebサイトの領域やチェック範囲を制限したほうが良い場合もあります。

Webサイトの一部だけをチェックする

includeexcludeメソッド使ってWebサイトのどの部分をチェックするか、あるいはスキップするかを指定できます。

axeBuilder(driver)
  // check only the main element
  .include('main')
  // skip ad banners
  .exclude('.banner')
  .analyze((results) => {
    // ...
  });

いま取りかかっているWebサイトの一部だけをチェックしたいときや、自分では修正できない、あるいは管理できない部分を除外するのに役に立ちます。

ルールを選ぶ

aXeのそれぞれのルールは1つまたは複数のタグでマークされ、ひとまとめにされます。withRulesまたはwithTagsメソッドを使って、指定したルールを無効にしたり、有効にしたりできます。

  • withRulesはIDを使って、特定のルールを使用可能にして構成する。次の例では、色のコントラストやリンクの名前のみをチェックする
    axeBuilder(driver)
    .withRules(['color-contrast', 'link-name'])
    .analyze((results) => {
      // ...
    });
    
  • withTagsは特定のタグでマークされたルールを使用できるようにする(例ではwcag2a)
    axeBuilder(driver)
      .withTags(['wcag2a'])
      .analyze((results) => {
        // ...
      });
    

終わりに

aXeはアクセシビリティテストの労力を部分的に機械に任せ、あいた時間をプロジェクト全体のデザインや構成など、ほかのことに使えるようにしてくれます。自分の開発環境やCI環境にきれいな形で統合する作業が必要ですが、手動でテストするよりはましです。1回やってしまえば、次のプロジェクトからは統合するのがたやすくなります。

aXeに基づくほかのツールはたくさんあります。 aXe Chrome pluginはブラウザーのどのようなページでも簡単に検査してくれます。Gulpを使っているなら、Gulp aXe pluginもあります。Reactを用いるプロジェクトには『UI開発の流れが変わる!React Storybookでデザイナーも開発者も幸せになれる』で説明したReact StoryBook用のプラグインがあり、Reactコンポーネントのアクセシビリティをテストできます。これらのツールのほうが、使いやすいかもしれません。

この記事でより多くの人たちが、自分のプロジェクトでアクセシビリティに関して考えるきっかけとなれば幸いです。

※本記事はMallory van AchterbergDominic MyersRalph MasonJoan Yinが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Automated Accessibility Checking with aXe

[翻訳:関 宏也/編集:Livit

Copyright © 2017, Pavels Jelisejevs All Rights Reserved.

Pavels Jelisejevs

Pavels Jelisejevs

ラトビア、リガ出身のソフトウェア開発者で、Web全般に強い関心を持っています。興味の対象は、分析やオートメーションをはじめ、バックエンド・フロントエンド開発もしています。FacebookまたはLinkedInでいつでも相談に応じています。

Loading...