@d-zero/a11y-check-core 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +904 -1
  2. package/package.json +7 -7
package/README.md CHANGED
@@ -1 +1,904 @@
1
- # `@d-zero/a11y-check-core`
1
+ # @d-zero/a11y-check-core
2
+
3
+ Webサイトのアクセシビリティをチェックするためのコアモジュールです。Puppeteerを使用してページをスキャンし、WCAG準拠のアクセシビリティ違反を検出します。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install @d-zero/a11y-check-core
9
+ ```
10
+
11
+ または
12
+
13
+ ```bash
14
+ yarn add @d-zero/a11y-check-core
15
+ ```
16
+
17
+ ## 概要
18
+
19
+ `@d-zero/a11y-check-core` は、Webページのアクセシビリティをプログラムで検証するための柔軟なフレームワークを提供します。シナリオベースのアーキテクチャを採用しており、カスタムチェックを簡単に作成・拡張できます。
20
+
21
+ ### 主な機能
22
+
23
+ - **シナリオベースのチェック**: カスタムアクセシビリティチェックを定義可能
24
+ - **複数デバイスサイズ対応**: デスクトップとモバイルビューポートでのテスト
25
+ - **色のコントラスト比チェック**: WCAG AA/AAAレベルの自動判定
26
+ - **並列実行**: 複数のURLを効率的にチェック
27
+ - **キャッシング**: チェック結果のキャッシュによる高速化
28
+ - **詳細な結果分類**: Passed(合格)、Violation(違反)、NeedAnalysis(要分析)の3種類
29
+
30
+ ## 主要なAPI
31
+
32
+ ### `scenarioRunner`
33
+
34
+ 複数のURLに対してアクセシビリティチェックを実行するメイン関数です。
35
+
36
+ ```typescript
37
+ async function scenarioRunner<O>(
38
+ urlList: readonly (string | { id: string | null; url: string })[],
39
+ scenarios: readonly Scenario[],
40
+ options?: O & CoreOptions & ScenarioRunnerOptions & DealOptions,
41
+ ): Promise<Result>;
42
+ ```
43
+
44
+ #### パラメータ
45
+
46
+ - `urlList`: チェック対象のURL配列。文字列または `{ id, url }` オブジェクトで指定
47
+ - `scenarios`: 実行するシナリオの配列
48
+ - `options`: オプション設定
49
+ - `screenshot`: スクリーンショットを撮影するかどうか(boolean)
50
+ - `cache`: キャッシュを使用するかどうか(boolean)
51
+ - `cacheDir`: キャッシュディレクトリのパス(string、デフォルト: `.a11y-check-core`)
52
+ - `locale`: ロケール設定(string)
53
+ - `hooks`: ページスキャン時のフック関数(PageHook[])
54
+ - その他の `DealOptions`(並列実行の設定など)
55
+
56
+ #### 戻り値
57
+
58
+ ```typescript
59
+ {
60
+ needAnalysis: readonly NeedAnalysis[];
61
+ passed: readonly Passed[];
62
+ violations: readonly Violation[];
63
+ }
64
+ ```
65
+
66
+ #### 使用例
67
+
68
+ ```typescript
69
+ import { scenarioRunner, createScenario } from '@d-zero/a11y-check-core';
70
+
71
+ const myScenario = createScenario(() => ({
72
+ modulePath: '/path/to/scenario.js',
73
+ moduleParams: '{}',
74
+ id: 'my-scenario',
75
+ exec: async (page, sizeName, log) => {
76
+ log('Checking accessibility...');
77
+
78
+ const violations = [];
79
+ // カスタムチェックロジック
80
+
81
+ return { violations };
82
+ },
83
+ }));
84
+
85
+ const results = await scenarioRunner(['https://example.com'], [myScenario()], {
86
+ cache: true,
87
+ cacheDir: '.cache',
88
+ });
89
+
90
+ console.log(`Violations found: ${results.violations.length}`);
91
+ ```
92
+
93
+ ### `createScenario`
94
+
95
+ 型安全なシナリオを作成するためのヘルパー関数です。
96
+
97
+ ```typescript
98
+ function createScenario<O>(creator: ScenarioCreator<O>): ScenarioCreator<O>;
99
+ ```
100
+
101
+ #### パラメータ
102
+
103
+ - `creator`: シナリオクリエーター関数
104
+ - `options`: シナリオのオプション(任意の型)
105
+ - 戻り値: `Scenario` オブジェクト
106
+
107
+ #### 使用例
108
+
109
+ ```typescript
110
+ import { createScenario } from '@d-zero/a11y-check-core';
111
+ import type { Page } from 'puppeteer';
112
+
113
+ type MyScenarioOptions = {
114
+ checkImages: boolean;
115
+ checkForms: boolean;
116
+ };
117
+
118
+ const myScenario = createScenario<MyScenarioOptions>((options) => ({
119
+ modulePath: import.meta.filename,
120
+ moduleParams: JSON.stringify(options ?? {}),
121
+ id: 'my-custom-scenario',
122
+
123
+ exec: async (page: Page, sizeName: string, log: (msg: string) => void) => {
124
+ log('Starting custom accessibility check');
125
+
126
+ const violations = [];
127
+
128
+ if (options?.checkImages) {
129
+ // 画像のaltテキストチェック
130
+ const imagesWithoutAlt = await page.$$eval('img:not([alt])', (imgs) =>
131
+ imgs.map((img) => ({ src: img.src })),
132
+ );
133
+
134
+ for (const img of imagesWithoutAlt) {
135
+ violations.push({
136
+ id: 'img-alt-missing',
137
+ url: page.url(),
138
+ tool: 'my-scenario',
139
+ timestamp: new Date(),
140
+ component: null,
141
+ environment: sizeName,
142
+ targetNode: { value: `<img src="${img.src}">` },
143
+ asIs: { value: 'alt属性が存在しない' },
144
+ toBe: { value: '画像には代替テキスト(alt属性)が必要' },
145
+ explanation: { value: 'スクリーンリーダーで画像の内容が伝わらない' },
146
+ wcagVersion: 'WCAG 2.1',
147
+ scNumber: '1.1.1',
148
+ level: 'A',
149
+ severity: 'high',
150
+ screenshot: null,
151
+ });
152
+ }
153
+ }
154
+
155
+ if (options?.checkForms) {
156
+ // フォームラベルのチェック
157
+ // ...
158
+ }
159
+
160
+ return { violations };
161
+ },
162
+
163
+ analyze: async (needAnalysisResults, log) => {
164
+ // 収集したデータの分析処理(オプション)
165
+ log('Analyzing collected data...');
166
+
167
+ const violations = [];
168
+ // 分析ロジック
169
+
170
+ return { violations };
171
+ },
172
+ }));
173
+
174
+ // 使用
175
+ const scenario = myScenario({
176
+ checkImages: true,
177
+ checkForms: true,
178
+ });
179
+ ```
180
+
181
+ ### `colorContrastCheck`
182
+
183
+ 要素のスタイルから色のコントラスト比をチェックする関数です。
184
+
185
+ ```typescript
186
+ function colorContrastCheck(style: Style): ColorContrastError | ColorContrast;
187
+ ```
188
+
189
+ #### パラメータ
190
+
191
+ - `style`: スタイルオブジェクト
192
+ - `color`: 前景色(文字色)
193
+ - `backgroundColor`: 背景色
194
+ - `backgroundImage`: 背景画像
195
+ - `closestBackgroundColor`: 最も近い親要素の背景色
196
+ - `closestBackgroundImage`: 最も近い親要素の背景画像
197
+
198
+ #### 戻り値
199
+
200
+ 成功時は `ColorContrast` オブジェクト、エラー時は `ColorContrastError` 列挙値を返します。
201
+
202
+ **ColorContrast オブジェクト:**
203
+
204
+ ```typescript
205
+ {
206
+ foreground: Color; // 前景色の詳細
207
+ background: Color; // 背景色の詳細
208
+ ratio: number; // コントラスト比(例: 4.52)
209
+ ratioText: `${number}:1`; // コントラスト比のテキスト表現(例: "4.52:1")
210
+ AA: boolean; // WCAG AAレベルに合格するか
211
+ AAA: boolean; // WCAG AAAレベルに合格するか
212
+ }
213
+ ```
214
+
215
+ **ColorContrastError 列挙値:**
216
+
217
+ - `DOES_NOT_DETERMINE_FOREGROUND`: 前景色を判定できない
218
+ - `DOES_NOT_DETERMINE_BACKGROUND`: 背景色を判定できない
219
+ - `FOREGROUND_COLOR_HAS_ALPHA`: 前景色に透明度がある
220
+ - `BACKGROUND_COLOR_HAS_ALPHA`: 背景色に透明度がある
221
+
222
+ #### 使用例
223
+
224
+ ```typescript
225
+ import { colorContrastCheck, ColorContrastError } from '@d-zero/a11y-check-core';
226
+
227
+ const style = await page.evaluate((selector) => {
228
+ const element = document.querySelector(selector);
229
+ const computed = window.getComputedStyle(element);
230
+
231
+ let closestBackgroundColor = null;
232
+ let parent = element.parentElement;
233
+ while (parent) {
234
+ const bg = window.getComputedStyle(parent).backgroundColor;
235
+ if (bg && bg !== 'rgba(0, 0, 0, 0)') {
236
+ closestBackgroundColor = bg;
237
+ break;
238
+ }
239
+ parent = parent.parentElement;
240
+ }
241
+
242
+ return {
243
+ color: computed.color,
244
+ backgroundColor: computed.backgroundColor,
245
+ backgroundImage: computed.backgroundImage,
246
+ closestBackgroundColor,
247
+ closestBackgroundImage: null,
248
+ };
249
+ }, 'button');
250
+
251
+ const result = colorContrastCheck(style);
252
+
253
+ if (typeof result === 'number') {
254
+ // エラー処理
255
+ switch (result) {
256
+ case ColorContrastError.DOES_NOT_DETERMINE_FOREGROUND:
257
+ console.error('前景色を判定できません');
258
+ break;
259
+ case ColorContrastError.DOES_NOT_DETERMINE_BACKGROUND:
260
+ console.error('背景色を判定できません');
261
+ break;
262
+ case ColorContrastError.FOREGROUND_COLOR_HAS_ALPHA:
263
+ console.error('前景色に透明度があります');
264
+ break;
265
+ case ColorContrastError.BACKGROUND_COLOR_HAS_ALPHA:
266
+ console.error('背景色に透明度があります');
267
+ break;
268
+ }
269
+ } else {
270
+ // 成功
271
+ console.log(`コントラスト比: ${result.ratioText}`);
272
+ console.log(`WCAG AA: ${result.AA ? '合格' : '不合格'}`);
273
+ console.log(`WCAG AAA: ${result.AAA ? '合格' : '不合格'}`);
274
+ }
275
+ ```
276
+
277
+ ### `colorFnToHex`
278
+
279
+ CSS color関数(`rgb()`、`rgba()`)を16進数カラーコードに変換する関数です。
280
+
281
+ ```typescript
282
+ function colorFnToHex(colorFn: string | null): Color | null;
283
+ ```
284
+
285
+ #### パラメータ
286
+
287
+ - `colorFn`: CSS color関数の文字列(例: `"rgb(255, 0, 0)"` または `"rgba(255, 0, 0, 0.5)"`)
288
+
289
+ #### 戻り値
290
+
291
+ ```typescript
292
+ {
293
+ r: number; // 赤成分 (0-255)
294
+ g: number; // 緑成分 (0-255)
295
+ b: number; // 青成分 (0-255)
296
+ a: number; // アルファ値 (0-1)
297
+ hex: string; // 16進数表現 (例: "#FF0000")
298
+ hexA: string; // アルファを含む16進数表現 (例: "#FF000080")
299
+ }
300
+ ```
301
+
302
+ 完全に透明な色(`a = 0`)の場合は `null` を返します。
303
+
304
+ #### 使用例
305
+
306
+ ```typescript
307
+ import { colorFnToHex } from '@d-zero/a11y-check-core';
308
+
309
+ const color1 = colorFnToHex('rgb(255, 0, 0)');
310
+ console.log(color1);
311
+ // { r: 255, g: 0, b: 0, a: 1, hex: '#FF0000', hexA: '#FF0000FF' }
312
+
313
+ const color2 = colorFnToHex('rgba(128, 128, 128, 0.5)');
314
+ console.log(color2);
315
+ // { r: 128, g: 128, b: 128, a: 0.5, hex: '#808080', hexA: '#80808080' }
316
+
317
+ const transparent = colorFnToHex('rgba(0, 0, 0, 0)');
318
+ console.log(transparent);
319
+ // null
320
+ ```
321
+
322
+ ### `scNumberComparator`
323
+
324
+ WCAG達成基準番号(SC番号)をソートするための比較関数です。
325
+
326
+ ```typescript
327
+ function scNumberComparator(a: string | null, b: string | null): number;
328
+ ```
329
+
330
+ #### パラメータ
331
+
332
+ - `a`, `b`: WCAG SC番号(例: `"1.1.1"`, `"2.4.7"`)または `null`
333
+
334
+ #### 戻り値
335
+
336
+ - `< 0`: `a` が `b` より前
337
+ - `0`: `a` と `b` が同じ
338
+ - `> 0`: `a` が `b` より後
339
+
340
+ `null` は常に最後になります。
341
+
342
+ #### 使用例
343
+
344
+ ```typescript
345
+ import { scNumberComparator } from '@d-zero/a11y-check-core';
346
+
347
+ const violations = [
348
+ { scNumber: '2.4.7' },
349
+ { scNumber: '1.1.1' },
350
+ { scNumber: '1.3.1' },
351
+ { scNumber: null },
352
+ { scNumber: '2.4.1' },
353
+ ];
354
+
355
+ violations.sort((a, b) => scNumberComparator(a.scNumber, b.scNumber));
356
+
357
+ console.log(violations.map((v) => v.scNumber));
358
+ // ['1.1.1', '1.3.1', '2.4.1', '2.4.7', null]
359
+ ```
360
+
361
+ ### `importScenarios`
362
+
363
+ シナリオモジュールを動的にインポートする関数です。
364
+
365
+ ```typescript
366
+ async function importScenarios(scenarios: readonly Scenario[]): Promise<Scenario[]>;
367
+ ```
368
+
369
+ #### パラメータ
370
+
371
+ - `scenarios`: シナリオオブジェクトの配列
372
+
373
+ #### 戻り値
374
+
375
+ インポートされたシナリオの配列(Promise)
376
+
377
+ 通常はフレームワーク内部で使用されますが、カスタム実行環境を構築する場合に利用できます。
378
+
379
+ ## 型定義
380
+
381
+ ### `Scenario`
382
+
383
+ シナリオオブジェクトの型定義です。
384
+
385
+ ```typescript
386
+ type Scenario = {
387
+ readonly modulePath: string; // シナリオモジュールのファイルパス
388
+ readonly moduleParams: string; // シナリオのパラメータ(JSON文字列)
389
+ readonly id: string; // シナリオの一意なID
390
+ readonly exec: ScenarioExecutor; // 実行関数
391
+ readonly analyze?: ScenarioAnalyzer; // 分析関数(オプション)
392
+ };
393
+ ```
394
+
395
+ ### `ScenarioExecutor`
396
+
397
+ シナリオの実行関数の型定義です。
398
+
399
+ ```typescript
400
+ type ScenarioExecutor = (
401
+ page: Page, // Puppeteerのページオブジェクト
402
+ sizeName: string, // デバイスサイズ名("desktop" または "mobile")
403
+ log: (log: string) => void, // ログ出力関数
404
+ ) => Promise<Partial<Result>>;
405
+ ```
406
+
407
+ 各シナリオは、ページをチェックして `Result` の一部を返します。
408
+
409
+ ### `ScenarioAnalyzer`
410
+
411
+ 収集したデータを分析する関数の型定義です。
412
+
413
+ ```typescript
414
+ type ScenarioAnalyzer = (
415
+ results: NeedAnalysis[], // 要分析データの配列
416
+ log: (log: string) => void, // ログ出力関数
417
+ ) => Promise<void | Partial<Result>> | void | Partial<Result>;
418
+ ```
419
+
420
+ `NeedAnalysis` として収集されたデータをまとめて分析し、最終的な判定を下すことができます。
421
+
422
+ ### `Result`
423
+
424
+ チェック結果を表す型です。
425
+
426
+ ```typescript
427
+ type Result = {
428
+ readonly needAnalysis: readonly NeedAnalysis[]; // 要分析データ
429
+ readonly passed: readonly Passed[]; // 合格項目
430
+ readonly violations: readonly Violation[]; // 違反項目
431
+ };
432
+ ```
433
+
434
+ ### `Passed`(合格)
435
+
436
+ アクセシビリティチェックに合格した項目を表します。
437
+
438
+ ```typescript
439
+ type Passed = {
440
+ readonly id: string; // チェック項目のID
441
+ readonly url: string; // チェック対象のURL
442
+ readonly tool: string | null; // 使用したツール名
443
+ readonly timestamp: Date; // チェック実行日時
444
+ readonly component: string | null; // コンポーネント名(任意)
445
+ readonly environment: string; // 実行環境(デバイスサイズなど)
446
+ };
447
+ ```
448
+
449
+ #### 使用例
450
+
451
+ ```typescript
452
+ const passed: Passed = {
453
+ id: 'color-contrast-check',
454
+ url: 'https://example.com',
455
+ tool: 'color-contrast-checker',
456
+ timestamp: new Date(),
457
+ component: 'Button',
458
+ environment: 'desktop',
459
+ };
460
+ ```
461
+
462
+ ### `Violation`(違反)
463
+
464
+ アクセシビリティ違反を表します。
465
+
466
+ ```typescript
467
+ type Violation = {
468
+ readonly id: string; // 違反のID
469
+ readonly url: string; // 違反が見つかったURL
470
+ readonly tool: string | null; // 使用したツール名
471
+ readonly timestamp: Date; // 検出日時
472
+ readonly component: string | null; // コンポーネント名
473
+ readonly environment: string; // 実行環境
474
+
475
+ // 違反の詳細
476
+ readonly targetNode: Details; // 対象要素
477
+ readonly asIs: Details; // 現状
478
+ readonly toBe: Details; // あるべき姿
479
+ readonly explanation: Details; // 説明
480
+
481
+ // WCAG情報
482
+ readonly wcagVersion: string | null; // WCAGバージョン(例: "WCAG 2.1")
483
+ readonly scNumber: string | null; // 達成基準番号(例: "1.4.3")
484
+ readonly level: 'A' | 'AA' | 'AAA' | null; // 適合レベル
485
+ readonly severity: 'high' | 'medium' | 'low' | null; // 深刻度
486
+
487
+ readonly screenshot: string | null; // スクリーンショットのパス
488
+ };
489
+
490
+ type Details = {
491
+ readonly value: string; // 値
492
+ readonly note?: string; // 補足説明(任意)
493
+ };
494
+ ```
495
+
496
+ #### 使用例
497
+
498
+ ```typescript
499
+ const violation: Violation = {
500
+ id: 'low-contrast-text',
501
+ url: 'https://example.com/page',
502
+ tool: 'color-contrast-checker',
503
+ timestamp: new Date(),
504
+ component: 'MainButton',
505
+ environment: 'mobile',
506
+
507
+ targetNode: {
508
+ value: '<button class="primary">送信</button>',
509
+ note: 'ページ下部の送信ボタン',
510
+ },
511
+
512
+ asIs: {
513
+ value: 'コントラスト比 2.8:1',
514
+ note: '前景色 #767676、背景色 #FFFFFF',
515
+ },
516
+
517
+ toBe: {
518
+ value: 'コントラスト比 4.5:1 以上',
519
+ note: 'WCAG AA レベルでは通常テキストに4.5:1以上が必要',
520
+ },
521
+
522
+ explanation: {
523
+ value:
524
+ 'テキストと背景のコントラストが不十分なため、ロービジョンのユーザーが読みにくい',
525
+ note: 'より濃い色を使用するか、背景色を変更してください',
526
+ },
527
+
528
+ wcagVersion: 'WCAG 2.1',
529
+ scNumber: '1.4.3',
530
+ level: 'AA',
531
+ severity: 'high',
532
+ screenshot: '/screenshots/page-violation-001.png',
533
+ };
534
+ ```
535
+
536
+ ### `NeedAnalysis`(要分析)
537
+
538
+ 自動判定できず、人間による分析が必要なデータを表します。
539
+
540
+ ```typescript
541
+ type NeedAnalysis = {
542
+ readonly id: string; // データのID
543
+ readonly url: string; // データが収集されたURL
544
+ readonly tool: string | null; // 使用したツール名
545
+ readonly timestamp: Date; // 収集日時
546
+ readonly component: string | null; // コンポーネント名
547
+ readonly environment: string; // 実行環境
548
+
549
+ readonly scenarioId: string; // 収集したシナリオのID
550
+ readonly subKey?: string; // サブキー(同じシナリオで複数のデータ種別を扱う場合)
551
+ readonly data: string; // 収集したデータ(JSON文字列など)
552
+ };
553
+ ```
554
+
555
+ `NeedAnalysis` は、自動的に判定できない情報(例: 画像の代替テキストが適切か、リンクテキストが文脈に依存するかなど)を収集し、後で `ScenarioAnalyzer` や外部ツールで分析するために使用します。
556
+
557
+ #### 使用例
558
+
559
+ ```typescript
560
+ // シナリオ実行時にデータを収集
561
+ const exec: ScenarioExecutor = async (page, sizeName, log) => {
562
+ const needAnalysis: NeedAnalysis[] = [];
563
+
564
+ const images = await page.$$eval('img[alt]', (imgs) =>
565
+ imgs.map((img) => ({
566
+ src: img.getAttribute('src'),
567
+ alt: img.getAttribute('alt'),
568
+ })),
569
+ );
570
+
571
+ for (const img of images) {
572
+ needAnalysis.push({
573
+ id: `img-alt-analysis-${img.src}`,
574
+ url: page.url(),
575
+ tool: 'alt-text-analyzer',
576
+ timestamp: new Date(),
577
+ component: null,
578
+ environment: sizeName,
579
+ scenarioId: 'alt-text-quality',
580
+ data: JSON.stringify(img),
581
+ });
582
+ }
583
+
584
+ return { needAnalysis };
585
+ };
586
+
587
+ // 分析関数でまとめて判定
588
+ const analyze: ScenarioAnalyzer = async (results, log) => {
589
+ const violations: Violation[] = [];
590
+
591
+ for (const result of results) {
592
+ const { src, alt } = JSON.parse(result.data);
593
+
594
+ // AIや辞書を使った代替テキストの品質チェック
595
+ if (alt.length < 3 || alt === 'image' || alt === 'photo') {
596
+ violations.push({
597
+ id: `insufficient-alt-${src}`,
598
+ url: result.url,
599
+ tool: result.tool,
600
+ timestamp: result.timestamp,
601
+ component: result.component,
602
+ environment: result.environment,
603
+ targetNode: { value: `<img src="${src}" alt="${alt}">` },
604
+ asIs: { value: `代替テキスト: "${alt}"` },
605
+ toBe: { value: '画像の内容を具体的に説明する代替テキスト' },
606
+ explanation: { value: '代替テキストが不十分または汎用的すぎます' },
607
+ wcagVersion: 'WCAG 2.1',
608
+ scNumber: '1.1.1',
609
+ level: 'A',
610
+ severity: 'high',
611
+ screenshot: null,
612
+ });
613
+ }
614
+ }
615
+
616
+ return { violations };
617
+ };
618
+ ```
619
+
620
+ ### その他の型
621
+
622
+ #### `Style`
623
+
624
+ 要素のスタイル情報を表します。
625
+
626
+ ```typescript
627
+ type Style = {
628
+ readonly color: string; // 前景色
629
+ readonly backgroundColor: string; // 背景色
630
+ readonly backgroundImage: string; // 背景画像
631
+ readonly closestBackgroundColor: string | null; // 最も近い親要素の背景色
632
+ readonly closestBackgroundImage: string | null; // 最も近い親要素の背景画像
633
+ };
634
+ ```
635
+
636
+ #### `Color`
637
+
638
+ 色の詳細情報を表します。
639
+
640
+ ```typescript
641
+ type Color = {
642
+ readonly r: number; // 赤成分 (0-255)
643
+ readonly g: number; // 緑成分 (0-255)
644
+ readonly b: number; // 青成分 (0-255)
645
+ readonly a: number; // アルファ値 (0-1)
646
+ readonly hex: string; // 16進数表現(例: "#FF0000")
647
+ readonly hexA: string; // アルファを含む16進数表現(例: "#FF0000FF")
648
+ };
649
+ ```
650
+
651
+ #### `ColorContrast`
652
+
653
+ 色のコントラスト比の情報を表します。
654
+
655
+ ```typescript
656
+ type ColorContrast = {
657
+ readonly foreground: Color; // 前景色
658
+ readonly background: Color; // 背景色
659
+ readonly ratio: number; // コントラスト比(例: 4.52)
660
+ readonly ratioText: `${number}:1`; // コントラスト比のテキスト(例: "4.52:1")
661
+ readonly AA: boolean; // WCAG AAレベルに合格するか
662
+ readonly AAA: boolean; // WCAG AAAレベルに合格するか
663
+ };
664
+ ```
665
+
666
+ #### `CoreOptions`
667
+
668
+ コアオプションの型定義です。
669
+
670
+ ```typescript
671
+ type CoreOptions = {
672
+ readonly screenshot?: boolean; // スクリーンショットを撮影するか
673
+ readonly cache?: boolean; // キャッシュを使用するか
674
+ readonly cacheDir?: string; // キャッシュディレクトリ
675
+ };
676
+ ```
677
+
678
+ #### `ScenarioRunnerOptions`
679
+
680
+ シナリオランナーのオプションの型定義です。
681
+
682
+ ```typescript
683
+ type ScenarioRunnerOptions = DealOptions & {
684
+ readonly locale?: string; // ロケール設定
685
+ readonly hooks?: readonly PageHook[]; // ページスキャン時のフック
686
+ };
687
+ ```
688
+
689
+ ## 完全な使用例
690
+
691
+ 以下は、カスタムシナリオを作成してアクセシビリティチェックを実行する完全な例です。
692
+
693
+ ```typescript
694
+ import {
695
+ scenarioRunner,
696
+ createScenario,
697
+ colorContrastCheck,
698
+ ColorContrastError,
699
+ } from '@d-zero/a11y-check-core';
700
+ import type {
701
+ ScenarioExecutor,
702
+ ScenarioAnalyzer,
703
+ Violation,
704
+ NeedAnalysis,
705
+ } from '@d-zero/a11y-check-core';
706
+ import type { Page } from 'puppeteer';
707
+
708
+ // カスタムシナリオのオプション型
709
+ type MyScenarioOptions = {
710
+ checkContrast: boolean;
711
+ checkHeadings: boolean;
712
+ };
713
+
714
+ // カスタムシナリオの作成
715
+ const myAccessibilityScenario = createScenario<MyScenarioOptions>((options) => ({
716
+ modulePath: import.meta.filename,
717
+ moduleParams: JSON.stringify(options ?? {}),
718
+ id: 'my-a11y-scenario',
719
+
720
+ // 実行関数: ページをチェックしてデータを収集
721
+ exec: async (page: Page, sizeName: string, log: (msg: string) => void) => {
722
+ const violations: Violation[] = [];
723
+ const needAnalysis: NeedAnalysis[] = [];
724
+
725
+ log('アクセシビリティチェックを開始');
726
+
727
+ // 1. 色のコントラストチェック
728
+ if (options?.checkContrast) {
729
+ log('色のコントラスト比をチェック中...');
730
+
731
+ const textElements = await page.$$('p, span, a, button, h1, h2, h3, h4, h5, h6');
732
+
733
+ for (const element of textElements) {
734
+ const style = await page.evaluate((el) => {
735
+ const computed = window.getComputedStyle(el);
736
+ let closestBg = null;
737
+ let parent = el.parentElement;
738
+
739
+ while (parent) {
740
+ const bg = window.getComputedStyle(parent).backgroundColor;
741
+ if (bg && bg !== 'rgba(0, 0, 0, 0)') {
742
+ closestBg = bg;
743
+ break;
744
+ }
745
+ parent = parent.parentElement;
746
+ }
747
+
748
+ return {
749
+ color: computed.color,
750
+ backgroundColor: computed.backgroundColor,
751
+ backgroundImage: computed.backgroundImage,
752
+ closestBackgroundColor: closestBg,
753
+ closestBackgroundImage: null,
754
+ };
755
+ }, element);
756
+
757
+ const contrastResult = colorContrastCheck(style);
758
+
759
+ if (typeof contrastResult !== 'number' && !contrastResult.AA) {
760
+ const html = await page.evaluate((el) => el.outerHTML, element);
761
+
762
+ violations.push({
763
+ id: `low-contrast-${Date.now()}`,
764
+ url: page.url(),
765
+ tool: 'color-contrast-checker',
766
+ timestamp: new Date(),
767
+ component: null,
768
+ environment: sizeName,
769
+ targetNode: { value: html },
770
+ asIs: {
771
+ value: `コントラスト比 ${contrastResult.ratioText}`,
772
+ note: `前景色: ${contrastResult.foreground.hex}, 背景色: ${contrastResult.background.hex}`,
773
+ },
774
+ toBe: {
775
+ value: 'コントラスト比 4.5:1 以上',
776
+ note: 'WCAG AAレベル準拠',
777
+ },
778
+ explanation: {
779
+ value: 'テキストと背景のコントラストが不十分です',
780
+ },
781
+ wcagVersion: 'WCAG 2.1',
782
+ scNumber: '1.4.3',
783
+ level: 'AA',
784
+ severity: 'high',
785
+ screenshot: null,
786
+ });
787
+ }
788
+ }
789
+ }
790
+
791
+ // 2. 見出し構造のチェック
792
+ if (options?.checkHeadings) {
793
+ log('見出し構造をチェック中...');
794
+
795
+ const headings = await page.$$eval('h1, h2, h3, h4, h5, h6', (elements) =>
796
+ elements.map((el) => ({
797
+ tag: el.tagName.toLowerCase(),
798
+ text: el.textContent?.trim() ?? '',
799
+ html: el.outerHTML,
800
+ })),
801
+ );
802
+
803
+ // 見出しレベルのスキップをチェック
804
+ for (let i = 1; i < headings.length; i++) {
805
+ const prevLevel = parseInt(headings[i - 1].tag.slice(1));
806
+ const currLevel = parseInt(headings[i].tag.slice(1));
807
+
808
+ if (currLevel - prevLevel > 1) {
809
+ violations.push({
810
+ id: `heading-skip-${i}`,
811
+ url: page.url(),
812
+ tool: 'heading-structure-checker',
813
+ timestamp: new Date(),
814
+ component: null,
815
+ environment: sizeName,
816
+ targetNode: { value: headings[i].html },
817
+ asIs: {
818
+ value: `${headings[i - 1].tag} の次に ${headings[i].tag} が使用されています`,
819
+ },
820
+ toBe: {
821
+ value: '見出しレベルは順番に使用する必要があります',
822
+ },
823
+ explanation: {
824
+ value:
825
+ '見出しレベルをスキップすると、スクリーンリーダーユーザーが文書構造を理解しにくくなります',
826
+ },
827
+ wcagVersion: 'WCAG 2.1',
828
+ scNumber: '1.3.1',
829
+ level: 'A',
830
+ severity: 'medium',
831
+ screenshot: null,
832
+ });
833
+ }
834
+ }
835
+ }
836
+
837
+ log(`チェック完了: ${violations.length}件の違反を検出`);
838
+
839
+ return { violations, needAnalysis };
840
+ },
841
+
842
+ // 分析関数(オプション): 収集したデータを後で分析
843
+ analyze: async (results: NeedAnalysis[], log: (msg: string) => void) => {
844
+ log(`${results.length}件のデータを分析中...`);
845
+
846
+ // ここで収集したデータをまとめて分析
847
+ const violations: Violation[] = [];
848
+
849
+ // 例: 複数ページのデータを比較して判定するなど
850
+
851
+ return { violations };
852
+ },
853
+ }));
854
+
855
+ // メイン処理
856
+ async function main() {
857
+ const urls = [
858
+ 'https://example.com',
859
+ 'https://example.com/about',
860
+ 'https://example.com/contact',
861
+ ];
862
+
863
+ const scenario = myAccessibilityScenario({
864
+ checkContrast: true,
865
+ checkHeadings: true,
866
+ });
867
+
868
+ const results = await scenarioRunner(urls, [scenario], {
869
+ cache: true,
870
+ cacheDir: '.a11y-cache',
871
+ locale: 'ja-JP',
872
+ });
873
+
874
+ console.log('\n=== アクセシビリティチェック結果 ===');
875
+ console.log(`合格: ${results.passed.length}件`);
876
+ console.log(`違反: ${results.violations.length}件`);
877
+ console.log(`要分析: ${results.needAnalysis.length}件`);
878
+
879
+ // 違反の詳細を出力
880
+ if (results.violations.length > 0) {
881
+ console.log('\n=== 違反の詳細 ===');
882
+ for (const violation of results.violations) {
883
+ console.log(`\n[${violation.severity}] ${violation.id}`);
884
+ console.log(`URL: ${violation.url}`);
885
+ console.log(
886
+ `WCAG: ${violation.wcagVersion} ${violation.scNumber} (レベル ${violation.level})`,
887
+ );
888
+ console.log(`現状: ${violation.asIs.value}`);
889
+ console.log(`改善: ${violation.toBe.value}`);
890
+ console.log(`説明: ${violation.explanation.value}`);
891
+ }
892
+ }
893
+ }
894
+
895
+ main().catch(console.error);
896
+ ```
897
+
898
+ ## ライセンス
899
+
900
+ MIT
901
+
902
+ ## 作者
903
+
904
+ D-ZERO
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d-zero/a11y-check-core",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Accessibility Checker (Core Module)",
5
5
  "author": "D-ZERO",
6
6
  "license": "MIT",
@@ -23,15 +23,15 @@
23
23
  "clean": "tsc --build --clean"
24
24
  },
25
25
  "dependencies": {
26
- "@d-zero/puppeteer-dealer": "0.6.5",
27
- "@d-zero/shared": "0.17.1",
26
+ "@d-zero/puppeteer-dealer": "0.7.0",
27
+ "@d-zero/shared": "0.18.0",
28
28
  "ansi-colors": "4.1.3",
29
29
  "color-contrast-checker": "2.1.0",
30
- "puppeteer": "24.36.0"
30
+ "puppeteer": "24.37.2"
31
31
  },
32
32
  "devDependencies": {
33
- "@d-zero/dealer": "1.5.3",
34
- "@d-zero/puppeteer-page-scan": "4.4.0"
33
+ "@d-zero/dealer": "1.6.0",
34
+ "@d-zero/puppeteer-page-scan": "4.4.2"
35
35
  },
36
- "gitHead": "e2189e6878674b8fef5fa3c121ad109c448040fe"
36
+ "gitHead": "a6d7b36c485bbc0782375c6e1ad0d0606f423e97"
37
37
  }