JUnitレポート

@surea11y/core/junitは、スキャン結果をJUnit XMLとして出力します。CIダッシュボードがネイティブに読めるテストレポートの形式で、GitLabのマージリクエストのテストウィジェット、Azure DevOpsのTestsタブ、Jenkins、CircleCIが対応しています。

const { renderJunitReport } = require('@surea11y/core/junit');
const { runDomRulesInPage } = require('@surea11y/core');
 
const result = runDomRulesInPage(url, null, { profile: 'wcag22-aa' }, null);
require('fs').writeFileSync('surea11y.junit.xml', renderJunitReport(result));

renderJunitReport(result, options)は純粋関数で、XMLを文字列として返し、ファイルシステムには触れません。@surea11y/cliのsurea11y scan <target> --jsonの出力のように、以前にJSONとして保存した結果にも同じように使えます。コマンドラインからは呼び出す必要はありません。surea11y scan <target> --junit a11y.junit.xml(CLI 1.1.0以降)がデフォルトのオプションと--baselineファイルのbaselineEntriesでレポートを書き出します。GitLabとAzure DevOpsのレシピはCI連携を参照してください。

構造

WCAGの達成基準ごとに1つの<testsuite>、そこに対応するルールごとに1つの<testcase>です。上の呼び出しで次のページをスキャンすると:

<!doctype html>
<html lang="en">
<head><title>Home</title></head>
<body><main><h1>Welcome</h1><img src="a.png"></main></body>
</html>

結果は次のとおりです。

<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="surea11y" tests="14" failures="1" errors="0" skipped="4" time="0">
<testsuite name="WCAG 1.1.1" tests="1" failures="1" errors="0" skipped="0" time="0">
<properties>
<property name="wcagCriterion" value="1.1.1"/>
<property name="title" value="Non-text content: text alternatives"/>
<property name="wcagLevel" value="A"/>
<property name="criterionOutcome" value="fail"/>
<property name="engine" value="a11ycore"/>
<property name="schemaVersion" value="1.0.0"/>
<property name="wcagVersion" value="2.2"/>
<property name="profile" value="wcag22-aa"/>
<property name="layout" value="false"/>
<property name="locale" value="en"/>
<property name="url" value="https://example.test/"/>
</properties>
<testcase classname="wcag-1.1.1" name="img-alt-present" time="0">
<failure type="fail" message="1 failing occurrence: This &lt;img&gt; has no alt attribute.">- This &lt;img&gt; has no alt attribute. Add an alt attribute that says what the image shows, or alt=&quot;&quot; if the image is purely decorative.
selector: html &gt; body &gt; main &gt; img
html: &lt;img src=&quot;a.png&quot;&gt;
help: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html</failure>
</testcase>
</testsuite>
<!-- 10 more suites trimmed: 13 tests, 4 of them skipped (manual review) -->
</testsuites>
  • 達成基準の名前を持つ緑のスイートは、その達成基準について実行されたルールが失敗を報告しなかったことを意味します。達成基準が完全にチェックされた、あるいは満たされたという意味ではありません。ほとんどの達成基準には、どのルールも扱わない部分があります(WCAG適合性との対応を参照)。criterionOutcomeプロパティがそれを示します。ルールが合格しても達成基準の一部しかチェックしない場合はcantTellで、現時点ではすべての達成基準がそうです。
  • スイートは達成基準に従います。人が追跡するのは達成基準だからです。テストケースは検出箇所ではなくルールに従います。そのため、ページ内で40回繰り返される不具合も、本文に40件すべてを列挙した1つの失敗したテストになり、テスト数は実行ごとに安定します。
  • 名前はどのロケールでも同じです。スイートの名前はWCAG <criterion>(WCAG 1.4.3)、どの達成基準にも対応しないルールのスイートはOther checks、テストケースの名前はルールのid、そのclassnameはwcag-<criterion>またはotherで、スキャンがどのengineOptions.localeで実行されても変わりません。CIのテスト履歴(GitLab、Jenkins、GitHubのテストレポーター)はこれらの名前をキーにしているため、フランス語で読んだスキャンも英語で読んだスキャンと同じテストになります。翻訳される達成基準のタイトルは、スイートのtitleプロパティに入ります(Contrast: minimum、Contraste (minimum))。検出結果自身のテキスト(各検出箇所のメッセージとヒント)はスキャンのロケールで、レポートがその周りに加える語句はどのロケールでも英語です。messageの先頭の件数(1 failing occurrence: Cet élément <img> n’a pas d’attribut alt.)、selector:、html:、help:のラベル、<skipped>のメッセージがそれに当たります。core 1.12より前は、スイートの名前に達成基準のタイトルが含まれていました(WCAG 1.1.1 Non-text content: text alternatives)。そのため、古い名前で追跡していたダッシュボードでは、アップグレード後の最初のレポートですべてのテストが一度だけ新規として表示され、履歴はそこから始まり直します。
  • スイートは複合ルールではなく各ルール自身のWCAGとの対応から作られるため、複合ルールを除外していても、実行されたすべてのルールが報告されます。複合ルールが実行された場合は、スイートのtitleプロパティとcriterionOutcomeプロパティを提供します。このプロパティは、その達成基準に対応するカスタムルールを組み込みルールと同じように数えます。複数の複合ルールが検査する達成基準(4.1.2:アクセシブルな名前とARIAの妥当性)は、それらの判定結果のうち最も悪いもの(一部がfailなら達成基準もfailになるため)と、それらのタイトルに共通するタイトル(Name, role, value)を取ります。2つの達成基準に対応するルールは両方のスイートに現れます。どの達成基準にも対応しないルールは、classname="other"を持つ最後のOther checksスイートに入ります。
  • スイートはスキャンのWCAGのレベルとバージョンに従います。それらによってスキャンがrulesResultsから外した達成基準にはスイートがありません。engineOptions.wcagLevel: 'AA'(engine.wcagLevel)のもとでのAAAの達成基準や、WCAG 2.2(engine.wcagVersion)でincludeRuleIds: ['duplicate-id']を実行したときの、2.2で削除された4.1.1(構文解析)です。スキャンが報告する達成基準は残ります。レベルより上でもIDで指定された集約、一部だけチェックされた達成基準、4.1.1を選んだ2.2のスキャンでの4.1.1などです。外された達成基準だけに対応するルールはOther checksに入り、その失敗は<skipped message="Not counted: WCAG 1.4.6 is outside the scan's target, WCAG 2.2 AA; 1 failing occurrence">になり、検出箇所は<system-out>に入ります。ルールは実行されて失敗しており、レポートはそれを示しますが、AAのビルドがAAAの達成基準で失敗することはありません。完了しなかったルールは引き続き<error>です。選択が示すレベル(runOnly.wcag.level、またはレベルのタグ)もengine.wcagLevelとして記録され、同じようにスイートを絞り込みます。古いエンジンの結果のように、engine.wcagLevelもengine.wcagVersionも記録していない結果では、すべての達成基準が残ります。
  • 実行のengine、schemaVersion、wcagVersion、profile、optInRules、engine.environmentにあるレンダリングの条件(layoutと、ブラウザーでは1280x720の形のviewport、devicePixelRatio、colorScheme、forcedColors、fonts)、locale、urlは、結果にある場合だけ、各スイートのプロパティとして繰り返されます。ビューポートの幅によって出たり消えたりする失敗は、実行が使った幅までたどれます。上の例はjsdomでのスキャンなので、レイアウトがありません。
  • 複数のビューポート幅でスキャンしたページは、どの幅でも同じスイート名とテスト名になるため、CIのテスト表示でまとめられてしまうことがあります。幅ごとに1つのファイルを書き出し、CIのレポート設定でファイルを幅ごとに名前付けするかグループ化してください。viewportプロパティには、そのファイルがどの幅を表しているかが記録されます。
  • スイートは達成基準の番号順(1.4.10より1.4.3が先)、テストケースはルールのid順に並びます。
  • en301549プロパティは、その達成基準を再掲するEN 301 549の条項を示します。条項があり、スキャンがEN 301 549の条項を要求した場合だけです(EN 301 549を参照)。結果のstandardsに列挙されたほかの各規格は、そのキーを名前とするプロパティを、スイートのルールが対応するその規格の要件ごとに1つ追加します。ルールをまとめるパックのチェックリスト項目は、そのidで列挙されます(<property name="acme" value="acme-images"/>)。カスタムルールだけが指定する規格は、プロパティを追加しません。

コンパクトな結果(engineOptions.output.detail: 'findings')は、それが表す完全な結果として読まれるので、同じように報告されます。

フレーム横断の結果

フレーム横断の結果(出力スキーマを参照)では、まずページのスイートが、続いて各フレームのスイートが出力されます。フレームのスイートはフレームのパスで名前が付けられ(Frame #checkout: WCAG 1.1.1。どのロケールでも同じです)、パスを保持するframeプロパティを持ち、そのテストケースのclassnameはパスで始まります(frame #checkout wcag-1.1.1)。そのため、スイート名、またはclassnameとnameをキーにするCIでも、フレーム内の失敗がページの失敗と混ざることはありません。応答しなかったフレームは、スキップされたテストケースNot scanned: <error>を1つ持つスイートFrame <path>になります。そのフレームは検査されておらず、それは合格ではないからです。<testsuites>の合計はすべてのフレームを数えます。

判定結果

ルールの判定結果JUnit理由
fail<failure type="fail">。不合格の検出箇所ごとに1行(メッセージ、ヒント、セレクター、マークアップ)。続いて、ルールがhttp(s)のヘルプリンク(meta.helpUrl)を持つ場合はhelp: <url>、持たない場合は最初のWCAG達成基準のUnderstanding文書決定論的で、ゲートを止めるケースです。
cantTell手動レビューが必要な検出箇所の数を示す<skipped>JUnitには「判断できない」がありません。スキップにすればビルドを赤にせずに表示でき、SARIFがwarningで引いているのと同じ線引きになります。
pass中身のない<testcase>
notApplicable省略1ページに数百あり、どれも何も伝えません。includeNotApplicable: trueを指定すると<skipped message="Not applicable">として追加します。
完了しなかったルール(検出箇所がなくerrorを持つcantTell。例外を投げたもの)エラーを含む<error type="ruleError">。errorsに数えられます実行できなかったテストを表すJUnitの要素です。ページはそのルールで検査されておらず、それは合格でもレビューすべきものでもありません。

cantTellの検出箇所も持つfailのルールは、不合格を<failure>に、未判断のものをダッシュボードがテスト出力を表示する<system-out>に報告します。スキップされたcantTellのルールも同様です。

シャドウツリー内の要素のselector:行は、シャドウホストを先に、続いて要素を示し、 >>> でつなぎます:html > body > main > x-card >>> button。SARIFとHTMLレポートが示すのと同じ位置です。

オプション

オプションデフォルト効果
cantTellAs'skipped''failure'を指定すると、cantTellのルールを<failure type="cantTell">として報告します。未判断のものが残っている間は合格させてはいけないパイプライン向けです。値は大文字小文字を区別せずに読みます。それ以外の値はTypeErrorを投げます。綴りを誤った値は、黙ってゲートしなくなるためです。
leanFailAs'skipped''failure'を指定すると、未判定の検出結果に不適合に傾くもの(そのlean)を含むルールを<failure type="leanFail">として報告し、それらの検出結果を列挙します。それぞれに、不適合の可能性が高い理由(likely fails:)、傾きの注意事項(ある場合)、人が確かめる方法(how to check:、検出結果のhowToCheckの手順、続いてfails when:)が付きます。手順を持たない結果には、レポートがNodeで追加します。そのルールのほかの未判定の検出結果はcantTellAsの指定どおりになります。すべての問いではなく、不適合の可能性が高いものでゲートするパイプライン向けです。ベストプラクティスのルールも対象です。そうしたルールはどのWCAG達成基準にも対応せず、failになることはありませんが、示す事実は無条件で不適合に傾くため、見出しレベルの飛び(heading-orderで<h1>の次に<h3>)はOther checksの<failure type="leanFail">になります。対応するすべての達成基準がスキャンの目標の外にあるルールは、不合格の場合と同じくゲートしません。値は大文字小文字を区別せずに読みます。それ以外の値はTypeErrorを投げます。@surea11y/coreのleaningFailures(result, { wcagOnly })は、これがゲートする検出結果を、ベースラインを適用する前の状態で列挙します。
leanFailWcagOnlyfalseleanFailAs: 'failure'と組み合わせてtrueにすると、スキャンの目標内のWCAG達成基準に対応するルールの検出結果だけでゲートし、ベストプラクティスのルールの傾きは除きます。trueとfalse以外の値はTypeErrorを投げます。
includeNotApplicablefalsenotApplicableのルールをスキップされたテストとして含めます。
baselineEntriesなしベースラインのbuildBaselineEntries()で作るエントリーです。そこに記録された不合格の検出箇所は、SARIFとまったく同じように除外されます。保存したままのベースラインファイル(それをJSON.parseしたもの)か、そのentriesリストを受け取ります。不合格がすべて既知のルールは、合格ではなく<skipped message="N known failures recorded in the baseline">になります。合格したわけではないからです。1.13が書いたエントリーのうち、1.14で変わった理由コードのものも、新しいコードの検出結果に一致します。leanFailAs: 'failure'のもとでは、不適合に傾き、lean: 'fail'と示したエントリー(buildBaselineEntries(result, { leanFail: true }))がある検出結果もゲートせず、<skipped>のメッセージはそれをknown likely failures recorded in the baselineに数えます。
packsなしスキャンが実行したパック(engineOptions.packs)です。leanFailAs: 'failure'のもとでは、HTMLレポートと同じく、パックのルールの不適合の可能性が高いものに、そのルール自身のmeta.reviewの手順が付きます。
name'surea11y'ルートの<testsuites>のnameです。1つのダッシュボードで複数ページのレポートを見分けるために使います。

決定性

エンジンは時計を持たず、このレポートも時刻を作り出しません。timeはすべて"0"で、timestamp属性は結果が持っている場合(engineOptions.timestamp)だけ各スイートに付きます。同じスキャンからは常にバイト単位で同一のXMLが生成されるため、レポートをコミットしたり差分を取ったりできます。

テキストはXML用にエスケープし、エスケープしてもXML 1.0が許可しない文字(ほとんどの制御文字、対になっていないサロゲート)は削除します。ページから取得したマークアップにはそうした文字が含まれることがあるためです。