@a11y-skills/audit 0.1.0 → 0.3.0
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.
- package/CHANGELOG.md +65 -0
- package/README.ja.md +76 -6
- package/README.md +78 -6
- package/dist/constants.d.ts +84 -0
- package/dist/constants.js +228 -0
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.js +1 -0
- package/dist/detectors/pause-control.d.ts +18 -0
- package/dist/detectors/pause-control.js +206 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/index.d.ts +7 -1
- package/dist/playwright/index.js +7 -1
- package/dist/playwright/runAutoPlayDetection.d.ts +36 -0
- package/dist/playwright/runAutoPlayDetection.js +143 -0
- package/dist/playwright/runAutocompleteAudit.d.ts +27 -0
- package/dist/playwright/runAutocompleteAudit.js +227 -0
- package/dist/playwright/runAxeAudit.d.ts +4 -0
- package/dist/playwright/runAxeAudit.js +26 -30
- package/dist/playwright/runFocusIndicatorCheck.js +55 -12
- package/dist/playwright/runOrientationCheck.d.ts +40 -0
- package/dist/playwright/runOrientationCheck.js +170 -0
- package/dist/playwright/runReflowCheck.js +18 -11
- package/dist/playwright/runTargetSizeCheck.js +42 -10
- package/dist/playwright/runTextSpacingCheck.d.ts +25 -0
- package/dist/playwright/runTextSpacingCheck.js +285 -0
- package/dist/playwright/runTimeLimitDetector.d.ts +31 -0
- package/dist/playwright/runTimeLimitDetector.js +219 -0
- package/dist/playwright/runZoomCheck.d.ts +42 -0
- package/dist/playwright/runZoomCheck.js +174 -0
- package/dist/schemas/index.d.ts +20 -1
- package/dist/schemas/index.js +404 -186
- package/dist/test-entries/auto-play-detection.d.ts +7 -0
- package/dist/test-entries/auto-play-detection.js +13 -0
- package/dist/test-entries/autocomplete-audit.d.ts +5 -0
- package/dist/test-entries/autocomplete-audit.js +11 -0
- package/dist/test-entries/orientation-check.d.ts +8 -0
- package/dist/test-entries/orientation-check.js +12 -0
- package/dist/test-entries/text-spacing-check.d.ts +5 -0
- package/dist/test-entries/text-spacing-check.js +11 -0
- package/dist/test-entries/time-limit-detector.d.ts +8 -0
- package/dist/test-entries/time-limit-detector.js +12 -0
- package/dist/test-entries/zoom-200-check.d.ts +5 -0
- package/dist/test-entries/zoom-200-check.js +11 -0
- package/dist/types.d.ts +275 -40
- package/dist/types.js +9 -0
- package/dist/utils/axe-format.d.ts +88 -0
- package/dist/utils/axe-format.js +361 -0
- package/dist/utils/image-compare.d.ts +24 -0
- package/dist/utils/image-compare.js +49 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +20 -1
- package/dist/utils/recommendations.d.ts +18 -0
- package/dist/utils/recommendations.js +88 -0
- package/dist/utils/rule-registry.d.ts +216 -0
- package/dist/utils/rule-registry.js +220 -0
- package/dist/utils/test-harness.d.ts +8 -2
- package/dist/utils/test-harness.js +13 -6
- package/package.json +32 -2
package/dist/constants.js
CHANGED
|
@@ -21,6 +21,11 @@ Note: Automated testing detects only ~30-40% of WCAG issues.
|
|
|
21
21
|
Manual testing is required for complete accessibility evaluation.
|
|
22
22
|
`;
|
|
23
23
|
// =============================================================================
|
|
24
|
+
// Normalized result format
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/** Maximum length of `NormalizedNode.html` / detail `html` snippets. */
|
|
27
|
+
export const HTML_SNIPPET_MAX_LENGTH = 300;
|
|
28
|
+
// =============================================================================
|
|
24
29
|
// Axe Audit defaults (broad WCAG coverage)
|
|
25
30
|
// =============================================================================
|
|
26
31
|
/** Default axe-core tag set (WCAG 2.0/2.1/2.2 A & AA) */
|
|
@@ -182,3 +187,226 @@ export const UA_CONTROLLED_INPUT_TYPES = [
|
|
|
182
187
|
* Minimum text length around inline link to qualify for inline exception
|
|
183
188
|
*/
|
|
184
189
|
export const INLINE_CONTEXT_MIN_TEXT = 10;
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Text Spacing Check Constants (WCAG 1.4.12)
|
|
192
|
+
// =============================================================================
|
|
193
|
+
/**
|
|
194
|
+
* CSS overrides for text spacing test per WCAG 1.4.12
|
|
195
|
+
* - Line height: at least 1.5 times the font size
|
|
196
|
+
* - Letter spacing: at least 0.12 times the font size
|
|
197
|
+
* - Word spacing: at least 0.16 times the font size
|
|
198
|
+
* - Paragraph spacing: at least 2 times the font size
|
|
199
|
+
*/
|
|
200
|
+
export const TEXT_SPACING_CSS = `
|
|
201
|
+
* {
|
|
202
|
+
line-height: 1.5 !important;
|
|
203
|
+
letter-spacing: 0.12em !important;
|
|
204
|
+
word-spacing: 0.16em !important;
|
|
205
|
+
}
|
|
206
|
+
p, div, span, li, td, th, dd, dt, label, blockquote {
|
|
207
|
+
margin-bottom: 2em !important;
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
/** Tolerance for detecting clipping after text spacing changes */
|
|
211
|
+
export const TEXT_SPACING_CLIP_TOLERANCE = 2;
|
|
212
|
+
/** Selector for text-containing elements to check */
|
|
213
|
+
export const TEXT_SPACING_CHECK_SELECTOR = `
|
|
214
|
+
p, h1, h2, h3, h4, h5, h6, li, td, th, span, div,
|
|
215
|
+
a, button, label, dd, dt, blockquote, figcaption
|
|
216
|
+
`.trim();
|
|
217
|
+
export const DEFAULT_TEXT_SPACING_RESULT_FILE = 'text-spacing-result.json';
|
|
218
|
+
export const DEFAULT_TEXT_SPACING_SCREENSHOT_FILE = 'text-spacing-screenshot.png';
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// Zoom 200% Check Constants (WCAG 1.4.4)
|
|
221
|
+
// =============================================================================
|
|
222
|
+
/** Zoom factor for resize text test */
|
|
223
|
+
export const ZOOM_FACTOR = 2;
|
|
224
|
+
/** Base viewport size before zoom (standard desktop) */
|
|
225
|
+
export const ZOOM_BASE_VIEWPORT = { width: 1280, height: 720 };
|
|
226
|
+
/** Tolerance for detecting clipping at zoom */
|
|
227
|
+
export const ZOOM_CLIP_TOLERANCE = 5;
|
|
228
|
+
export const DEFAULT_ZOOM_RESULT_FILE = 'zoom-200-result.json';
|
|
229
|
+
export const DEFAULT_ZOOM_SCREENSHOT_FILE = 'zoom-200-screenshot.png';
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// Orientation Check Constants (WCAG 1.3.4)
|
|
232
|
+
// =============================================================================
|
|
233
|
+
/** Viewport sizes for orientation tests */
|
|
234
|
+
export const ORIENTATION_VIEWPORTS = {
|
|
235
|
+
portrait: { width: 375, height: 667 },
|
|
236
|
+
landscape: { width: 667, height: 375 },
|
|
237
|
+
};
|
|
238
|
+
/** Keywords indicating orientation lock messages (EN/JP) */
|
|
239
|
+
export const ORIENTATION_LOCK_KEYWORDS = [
|
|
240
|
+
// English
|
|
241
|
+
'rotate device',
|
|
242
|
+
'rotate your device',
|
|
243
|
+
'rotate your phone',
|
|
244
|
+
'turn your device',
|
|
245
|
+
'landscape only',
|
|
246
|
+
'portrait only',
|
|
247
|
+
'please rotate',
|
|
248
|
+
'best viewed in',
|
|
249
|
+
'for best experience',
|
|
250
|
+
// Japanese
|
|
251
|
+
'画面を回転',
|
|
252
|
+
'端末を回転',
|
|
253
|
+
'デバイスを回転',
|
|
254
|
+
'横向きにして',
|
|
255
|
+
'縦向きにして',
|
|
256
|
+
'横画面でご覧',
|
|
257
|
+
'縦画面でご覧',
|
|
258
|
+
'回転してください',
|
|
259
|
+
];
|
|
260
|
+
/** Main content selectors to check visibility */
|
|
261
|
+
export const MAIN_CONTENT_SELECTORS = [
|
|
262
|
+
'main',
|
|
263
|
+
'[role="main"]',
|
|
264
|
+
'#main',
|
|
265
|
+
'#content',
|
|
266
|
+
'.main-content',
|
|
267
|
+
'article',
|
|
268
|
+
];
|
|
269
|
+
export const DEFAULT_ORIENTATION_RESULT_FILE = 'orientation-result.json';
|
|
270
|
+
export const DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE = 'orientation-screenshot-portrait.png';
|
|
271
|
+
export const DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE = 'orientation-screenshot-landscape.png';
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// Autocomplete Audit Constants (WCAG 1.3.5)
|
|
274
|
+
// =============================================================================
|
|
275
|
+
/**
|
|
276
|
+
* Mapping of field patterns to expected autocomplete tokens
|
|
277
|
+
* Based on HTML autocomplete attribute values
|
|
278
|
+
*/
|
|
279
|
+
export const AUTOCOMPLETE_FIELD_PATTERNS = {
|
|
280
|
+
// Name fields
|
|
281
|
+
name: /^(full.?name|your.?name|氏名|お名前|名前)$/i,
|
|
282
|
+
'given-name': /^(first.?name|given.?name|名|ファーストネーム|名前)$/i,
|
|
283
|
+
'family-name': /^(last.?name|family.?name|surname|姓|ラストネーム|苗字)$/i,
|
|
284
|
+
'honorific-prefix': /^(prefix|title|敬称)$/i,
|
|
285
|
+
'honorific-suffix': /^(suffix|様|さん)$/i,
|
|
286
|
+
// Contact fields
|
|
287
|
+
email: /^(e.?mail|メール|メールアドレス)$/i,
|
|
288
|
+
tel: /^(phone|tel|telephone|電話|電話番号|携帯)$/i,
|
|
289
|
+
'tel-national': /^(phone.?national|国内電話)$/i,
|
|
290
|
+
// Address fields
|
|
291
|
+
'street-address': /^(address|street|住所|番地)$/i,
|
|
292
|
+
'address-line1': /^(address.?1|address.?line.?1|住所1)$/i,
|
|
293
|
+
'address-line2': /^(address.?2|address.?line.?2|住所2)$/i,
|
|
294
|
+
'postal-code': /^(zip|postal|郵便番号|〒)$/i,
|
|
295
|
+
country: /^(country|国|国名)$/i,
|
|
296
|
+
'country-name': /^(country.?name|国名)$/i,
|
|
297
|
+
// Organization fields
|
|
298
|
+
organization: /^(company|org|organization|会社|組織|会社名)$/i,
|
|
299
|
+
'organization-title': /^(title|position|役職|肩書き)$/i,
|
|
300
|
+
// Username/password
|
|
301
|
+
username: /^(user.?name|login|ユーザー名|ログインID)$/i,
|
|
302
|
+
'current-password': /^(password|pass|current.?password|パスワード|現在のパスワード)$/i,
|
|
303
|
+
'new-password': /^(new.?password|新しいパスワード)$/i,
|
|
304
|
+
// Payment
|
|
305
|
+
'cc-name': /^(card.?name|cc.?name|カード名義)$/i,
|
|
306
|
+
'cc-number': /^(card.?number|cc.?number|カード番号)$/i,
|
|
307
|
+
'cc-exp': /^(expir|cc.?exp|有効期限)$/i,
|
|
308
|
+
'cc-exp-month': /^(exp.?month|有効期限.?月)$/i,
|
|
309
|
+
'cc-exp-year': /^(exp.?year|有効期限.?年)$/i,
|
|
310
|
+
'cc-csc': /^(cvc|cvv|csc|security.?code|セキュリティコード)$/i,
|
|
311
|
+
// Dates
|
|
312
|
+
bday: /^(birth.?day|birthday|dob|生年月日|誕生日)$/i,
|
|
313
|
+
'bday-day': /^(birth.?day.?day|日)$/i,
|
|
314
|
+
'bday-month': /^(birth.?day.?month|月)$/i,
|
|
315
|
+
'bday-year': /^(birth.?day.?year|年)$/i,
|
|
316
|
+
sex: /^(sex|gender|性別)$/i,
|
|
317
|
+
// Other
|
|
318
|
+
url: /^(url|website|homepage|ウェブサイト|ホームページ)$/i,
|
|
319
|
+
photo: /^(photo|avatar|写真|アバター)$/i,
|
|
320
|
+
};
|
|
321
|
+
/** Valid autocomplete token values */
|
|
322
|
+
export const VALID_AUTOCOMPLETE_TOKENS = [
|
|
323
|
+
'off', 'on',
|
|
324
|
+
'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix', 'nickname',
|
|
325
|
+
'email', 'username', 'new-password', 'current-password', 'one-time-code',
|
|
326
|
+
'organization-title', 'organization',
|
|
327
|
+
'street-address', 'address-line1', 'address-line2', 'address-line3',
|
|
328
|
+
'address-level1', 'address-level2', 'address-level3', 'address-level4',
|
|
329
|
+
'country', 'country-name', 'postal-code',
|
|
330
|
+
'cc-name', 'cc-given-name', 'cc-additional-name', 'cc-family-name', 'cc-number', 'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type',
|
|
331
|
+
'transaction-currency', 'transaction-amount',
|
|
332
|
+
'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex',
|
|
333
|
+
'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-local-prefix', 'tel-local-suffix', 'tel-extension',
|
|
334
|
+
'impp', 'url', 'photo',
|
|
335
|
+
];
|
|
336
|
+
export const DEFAULT_AUTOCOMPLETE_RESULT_FILE = 'autocomplete-result.json';
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// Time Limit Detector Constants (WCAG 2.2.1)
|
|
339
|
+
// =============================================================================
|
|
340
|
+
/** Keywords indicating countdown or time limit in visible text (EN/JP) */
|
|
341
|
+
export const TIME_LIMIT_KEYWORDS = [
|
|
342
|
+
// English
|
|
343
|
+
'session expires',
|
|
344
|
+
'session timeout',
|
|
345
|
+
'time remaining',
|
|
346
|
+
'time left',
|
|
347
|
+
'countdown',
|
|
348
|
+
'expires in',
|
|
349
|
+
'will timeout',
|
|
350
|
+
'auto logout',
|
|
351
|
+
'automatic logout',
|
|
352
|
+
// Japanese
|
|
353
|
+
'セッション終了',
|
|
354
|
+
'セッションタイムアウト',
|
|
355
|
+
'残り時間',
|
|
356
|
+
'タイムアウト',
|
|
357
|
+
'自動ログアウト',
|
|
358
|
+
'有効期限',
|
|
359
|
+
'制限時間',
|
|
360
|
+
'カウントダウン',
|
|
361
|
+
];
|
|
362
|
+
/** Timer threshold for reporting (10 minutes in ms) */
|
|
363
|
+
export const TIME_LIMIT_THRESHOLD_MS = 600000;
|
|
364
|
+
/** Minimum timer to report (10 seconds in ms, to filter out UI animations) */
|
|
365
|
+
export const TIME_LIMIT_MIN_MS = 10000;
|
|
366
|
+
export const DEFAULT_TIME_LIMIT_RESULT_FILE = 'time-limit-result.json';
|
|
367
|
+
// =============================================================================
|
|
368
|
+
// Auto-play Detection Constants (WCAG 1.4.2 / 2.2.2)
|
|
369
|
+
// =============================================================================
|
|
370
|
+
/** Screenshot intervals in milliseconds (0s, 2s, 4s, 6s) */
|
|
371
|
+
export const SCREENSHOT_INTERVALS = [0, 2000, 4000, 6000];
|
|
372
|
+
/** Pixel change threshold (0.1% = significant change) */
|
|
373
|
+
export const CHANGE_THRESHOLD = 0.1;
|
|
374
|
+
/** Pixelmatch color difference threshold (0-1) */
|
|
375
|
+
export const PIXELMATCH_THRESHOLD = 0.1;
|
|
376
|
+
/** Wait time after clicking pause control (ms) */
|
|
377
|
+
export const PAUSE_CLICK_WAIT = 500;
|
|
378
|
+
/** Wait time between screenshots for comparison (ms) */
|
|
379
|
+
export const SCREENSHOT_COMPARISON_WAIT = 2000;
|
|
380
|
+
export const DEFAULT_AUTO_PLAY_OUTPUT_DIR = './auto-play-screenshots';
|
|
381
|
+
export const DETECTION_RESULT_FILENAME = 'detection-result.json';
|
|
382
|
+
// =============================================================================
|
|
383
|
+
// Pause Control Detection Patterns (WCAG 1.4.2 / 2.2.2)
|
|
384
|
+
// =============================================================================
|
|
385
|
+
/** Keywords for pause/stop controls (EN/JP) */
|
|
386
|
+
export const PAUSE_KEYWORDS = [
|
|
387
|
+
// English
|
|
388
|
+
'pause', 'stop', 'halt', 'freeze', 'play',
|
|
389
|
+
// Japanese
|
|
390
|
+
'一時停止', '停止', 'ポーズ', '止める', '再生',
|
|
391
|
+
];
|
|
392
|
+
/** Class name patterns indicating pause/play controls */
|
|
393
|
+
export const CONTROL_CLASS_PATTERNS = [
|
|
394
|
+
'pause', 'play', 'stop', 'toggle', 'switch',
|
|
395
|
+
'control', 'btn-pause', 'btn-play', 'btn-stop',
|
|
396
|
+
];
|
|
397
|
+
/** Carousel-related class patterns */
|
|
398
|
+
export const CAROUSEL_PATTERNS = [
|
|
399
|
+
'carousel', 'slider', 'slide', 'swiper', 'slick',
|
|
400
|
+
'hero', 'banner', 'gallery', 'rotator',
|
|
401
|
+
];
|
|
402
|
+
/** Navigation control keywords */
|
|
403
|
+
export const NAV_KEYWORDS = [
|
|
404
|
+
'prev', 'next', '前', '次', 'arrow', 'dot', 'indicator',
|
|
405
|
+
];
|
|
406
|
+
/** SVG metadata patterns to exclude from accessible names */
|
|
407
|
+
export const SVG_METADATA_PATTERNS = [
|
|
408
|
+
'created with', 'made with', 'generated by',
|
|
409
|
+
'svg', 'icon', 'symbol',
|
|
410
|
+
];
|
|
411
|
+
/** Maximum parent levels to check for carousel context */
|
|
412
|
+
export const MAX_PARENT_LEVELS = 5;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { detectPauseControls, verifyPauseControl, createSkippedVerification, } from './pause-control.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { detectPauseControls, verifyPauseControl, createSkippedVerification, } from './pause-control.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pause/Stop control detection for auto-playing content.
|
|
3
|
+
* WCAG 1.4.2 (Audio Control) / 2.2.2 (Pause, Stop, Hide)
|
|
4
|
+
*/
|
|
5
|
+
import type { Page } from '@playwright/test';
|
|
6
|
+
import type { PauseControlInfo, PauseVerificationResult } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Detect pause/stop controls in the page.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectPauseControls(page: Page): Promise<PauseControlInfo>;
|
|
11
|
+
/**
|
|
12
|
+
* Verify if clicking the pause control actually stops the auto-play.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyPauseControl(page: Page, pauseControls: PauseControlInfo, outputDir: string, changeThreshold: number): Promise<PauseVerificationResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Create a skipped verification result.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createSkippedVerification(reason: string): PauseVerificationResult;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pause/Stop control detection for auto-playing content.
|
|
3
|
+
* WCAG 1.4.2 (Audio Control) / 2.2.2 (Pause, Stop, Hide)
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { compareImages, formatDiffPercent, hasSignificantChange, } from '../utils/image-compare.js';
|
|
7
|
+
import { PAUSE_KEYWORDS, CONTROL_CLASS_PATTERNS, CAROUSEL_PATTERNS, NAV_KEYWORDS, SVG_METADATA_PATTERNS, MAX_PARENT_LEVELS, PAUSE_CLICK_WAIT, SCREENSHOT_COMPARISON_WAIT, } from '../constants.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect pause/stop controls in the page.
|
|
10
|
+
*/
|
|
11
|
+
export async function detectPauseControls(page) {
|
|
12
|
+
const config = {
|
|
13
|
+
pauseKeywords: [...PAUSE_KEYWORDS],
|
|
14
|
+
controlClassPatterns: [...CONTROL_CLASS_PATTERNS],
|
|
15
|
+
carouselPatterns: [...CAROUSEL_PATTERNS],
|
|
16
|
+
navKeywords: [...NAV_KEYWORDS],
|
|
17
|
+
svgMetadataPatterns: [...SVG_METADATA_PATTERNS],
|
|
18
|
+
maxParentLevels: MAX_PARENT_LEVELS,
|
|
19
|
+
};
|
|
20
|
+
return await page.evaluate((cfg) => {
|
|
21
|
+
const controls = [];
|
|
22
|
+
const carouselIndicators = [];
|
|
23
|
+
let hasAccessibleName = false;
|
|
24
|
+
const getSelector = (el) => {
|
|
25
|
+
if (el.id)
|
|
26
|
+
return `#${el.id}`;
|
|
27
|
+
if (el.className) {
|
|
28
|
+
const classes = el.className
|
|
29
|
+
.toString()
|
|
30
|
+
.split(' ')
|
|
31
|
+
.filter((c) => c)
|
|
32
|
+
.join('.');
|
|
33
|
+
if (classes)
|
|
34
|
+
return `${el.tagName.toLowerCase()}.${classes}`;
|
|
35
|
+
}
|
|
36
|
+
return el.tagName.toLowerCase();
|
|
37
|
+
};
|
|
38
|
+
const interactiveElements = document.querySelectorAll('button, [role="button"], input[type="button"], [tabindex="0"]');
|
|
39
|
+
interactiveElements.forEach((el) => {
|
|
40
|
+
const element = el;
|
|
41
|
+
const tagName = element.tagName.toLowerCase();
|
|
42
|
+
const ariaLabel = element.getAttribute('aria-label') || '';
|
|
43
|
+
const textContent = element.textContent?.trim() || '';
|
|
44
|
+
const className = element.className?.toString() || '';
|
|
45
|
+
let accessibleName = ariaLabel || textContent;
|
|
46
|
+
const isSvgMetadata = cfg.svgMetadataPatterns.some((pattern) => accessibleName.toLowerCase().includes(pattern));
|
|
47
|
+
if (isSvgMetadata) {
|
|
48
|
+
accessibleName = '';
|
|
49
|
+
}
|
|
50
|
+
const lowerName = accessibleName.toLowerCase();
|
|
51
|
+
const lowerClass = className.toLowerCase();
|
|
52
|
+
const nameMatch = cfg.pauseKeywords.some((kw) => lowerName.includes(kw.toLowerCase()));
|
|
53
|
+
const classMatch = cfg.controlClassPatterns.some((pattern) => lowerClass.includes(pattern));
|
|
54
|
+
const isNearCarousel = cfg.carouselPatterns.some((pattern) => {
|
|
55
|
+
if (lowerClass.includes(pattern))
|
|
56
|
+
return true;
|
|
57
|
+
let parent = element.parentElement;
|
|
58
|
+
for (let i = 0; i < cfg.maxParentLevels && parent; i++) {
|
|
59
|
+
if (parent.className?.toString().toLowerCase().includes(pattern)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
parent = parent.parentElement;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
const hasSvg = element.querySelector('svg');
|
|
67
|
+
let hasPauseIconPattern = false;
|
|
68
|
+
if (hasSvg) {
|
|
69
|
+
const rects = hasSvg.querySelectorAll('rect, path');
|
|
70
|
+
if (rects.length === 2) {
|
|
71
|
+
hasPauseIconPattern = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (nameMatch) {
|
|
75
|
+
controls.push({
|
|
76
|
+
element: tagName,
|
|
77
|
+
name: accessibleName || `[class: ${className.slice(0, 50)}]`,
|
|
78
|
+
matchedBy: 'accessible-name',
|
|
79
|
+
selector: getSelector(element),
|
|
80
|
+
});
|
|
81
|
+
hasAccessibleName = true;
|
|
82
|
+
}
|
|
83
|
+
else if (classMatch && isNearCarousel) {
|
|
84
|
+
controls.push({
|
|
85
|
+
element: tagName,
|
|
86
|
+
name: accessibleName || `[class: ${className.slice(0, 50)}]`,
|
|
87
|
+
matchedBy: 'class-name-near-carousel',
|
|
88
|
+
selector: getSelector(element),
|
|
89
|
+
});
|
|
90
|
+
if (accessibleName)
|
|
91
|
+
hasAccessibleName = true;
|
|
92
|
+
}
|
|
93
|
+
else if (hasPauseIconPattern && isNearCarousel) {
|
|
94
|
+
controls.push({
|
|
95
|
+
element: tagName,
|
|
96
|
+
name: accessibleName || `[class: ${className.slice(0, 50)}]`,
|
|
97
|
+
matchedBy: 'svg-icon-pattern',
|
|
98
|
+
selector: getSelector(element),
|
|
99
|
+
});
|
|
100
|
+
if (accessibleName)
|
|
101
|
+
hasAccessibleName = true;
|
|
102
|
+
}
|
|
103
|
+
const isNavControl = cfg.navKeywords.some((kw) => lowerName.includes(kw) || lowerClass.includes(kw));
|
|
104
|
+
if (isNavControl && isNearCarousel) {
|
|
105
|
+
carouselIndicators.push({
|
|
106
|
+
element: tagName,
|
|
107
|
+
name: accessibleName || `[class: ${className.slice(0, 50)}]`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
found: controls.length > 0,
|
|
113
|
+
controls,
|
|
114
|
+
carouselIndicators,
|
|
115
|
+
hasAccessibleName,
|
|
116
|
+
};
|
|
117
|
+
}, config);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Verify if clicking the pause control actually stops the auto-play.
|
|
121
|
+
*/
|
|
122
|
+
export async function verifyPauseControl(page, pauseControls, outputDir, changeThreshold) {
|
|
123
|
+
const control = pauseControls.controls[0];
|
|
124
|
+
if (!pauseControls.found || !control) {
|
|
125
|
+
return {
|
|
126
|
+
attempted: false,
|
|
127
|
+
controlClicked: null,
|
|
128
|
+
beforeClickDiffPercent: null,
|
|
129
|
+
afterClickDiffPercent: null,
|
|
130
|
+
pauseWorked: null,
|
|
131
|
+
error: 'No pause controls found to verify',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const selector = control.selector;
|
|
135
|
+
if (!selector) {
|
|
136
|
+
return {
|
|
137
|
+
attempted: false,
|
|
138
|
+
controlClicked: null,
|
|
139
|
+
beforeClickDiffPercent: null,
|
|
140
|
+
afterClickDiffPercent: null,
|
|
141
|
+
pauseWorked: null,
|
|
142
|
+
error: 'No selector available for pause control',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const beforePath1 = path.join(outputDir, 'verify-before-1.png');
|
|
147
|
+
const beforePath2 = path.join(outputDir, 'verify-before-2.png');
|
|
148
|
+
await page.screenshot({ path: beforePath1, fullPage: false });
|
|
149
|
+
await page.waitForTimeout(SCREENSHOT_COMPARISON_WAIT);
|
|
150
|
+
await page.screenshot({ path: beforePath2, fullPage: false });
|
|
151
|
+
const beforeDiff = compareImages(beforePath1, beforePath2, path.join(outputDir, 'verify-before-diff.png'));
|
|
152
|
+
const element = await page.$(selector);
|
|
153
|
+
if (!element) {
|
|
154
|
+
return {
|
|
155
|
+
attempted: true,
|
|
156
|
+
controlClicked: selector,
|
|
157
|
+
beforeClickDiffPercent: formatDiffPercent(beforeDiff.diffPercent),
|
|
158
|
+
afterClickDiffPercent: null,
|
|
159
|
+
pauseWorked: null,
|
|
160
|
+
error: `Could not find element with selector: ${selector}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
await element.click();
|
|
164
|
+
await page.waitForTimeout(PAUSE_CLICK_WAIT);
|
|
165
|
+
const afterPath1 = path.join(outputDir, 'verify-after-1.png');
|
|
166
|
+
const afterPath2 = path.join(outputDir, 'verify-after-2.png');
|
|
167
|
+
await page.screenshot({ path: afterPath1, fullPage: false });
|
|
168
|
+
await page.waitForTimeout(SCREENSHOT_COMPARISON_WAIT);
|
|
169
|
+
await page.screenshot({ path: afterPath2, fullPage: false });
|
|
170
|
+
const afterDiff = compareImages(afterPath1, afterPath2, path.join(outputDir, 'verify-after-diff.png'));
|
|
171
|
+
const hadChangeBefore = hasSignificantChange(beforeDiff.diffPercent, changeThreshold);
|
|
172
|
+
const hasChangeAfter = hasSignificantChange(afterDiff.diffPercent, changeThreshold);
|
|
173
|
+
const pauseWorked = hadChangeBefore && !hasChangeAfter;
|
|
174
|
+
return {
|
|
175
|
+
attempted: true,
|
|
176
|
+
controlClicked: selector,
|
|
177
|
+
beforeClickDiffPercent: formatDiffPercent(beforeDiff.diffPercent),
|
|
178
|
+
afterClickDiffPercent: formatDiffPercent(afterDiff.diffPercent),
|
|
179
|
+
pauseWorked,
|
|
180
|
+
error: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return {
|
|
185
|
+
attempted: true,
|
|
186
|
+
controlClicked: selector,
|
|
187
|
+
beforeClickDiffPercent: null,
|
|
188
|
+
afterClickDiffPercent: null,
|
|
189
|
+
pauseWorked: null,
|
|
190
|
+
error: `Error during verification: ${err instanceof Error ? err.message : String(err)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Create a skipped verification result.
|
|
196
|
+
*/
|
|
197
|
+
export function createSkippedVerification(reason) {
|
|
198
|
+
return {
|
|
199
|
+
attempted: false,
|
|
200
|
+
controlClicked: null,
|
|
201
|
+
beforeClickDiffPercent: null,
|
|
202
|
+
afterClickDiffPercent: null,
|
|
203
|
+
pauseWorked: null,
|
|
204
|
+
error: reason,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,4 +10,6 @@
|
|
|
10
10
|
* - `@a11y-skills/audit/schemas`: result types & JSON Schemas.
|
|
11
11
|
*/
|
|
12
12
|
export * from './types.js';
|
|
13
|
-
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
13
|
+
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, HTML_SNIPPET_MAX_LENGTH, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
14
|
+
export { RULES, getRule, type RuleKey, type RuleMeta } from './utils/rule-registry.js';
|
|
15
|
+
export { buildAuditResult, mergeNormalizedResults, normalizeAxeResults, normalizeAutocompleteAudit, normalizeAutoPlayDetection, normalizeFocusCheck, normalizeOrientationCheck, normalizeReflowCheck, normalizeTargetSizeCheck, normalizeTextSpacingCheck, normalizeTimeLimitDetector, normalizeZoomCheck, type MergedAuditResult, type NormalizedBuckets, type RawAxeResults, type RawAxeRule, } from './utils/axe-format.js';
|
package/dist/index.js
CHANGED
|
@@ -10,4 +10,6 @@
|
|
|
10
10
|
* - `@a11y-skills/audit/schemas`: result types & JSON Schemas.
|
|
11
11
|
*/
|
|
12
12
|
export * from './types.js';
|
|
13
|
-
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
13
|
+
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, HTML_SNIPPET_MAX_LENGTH, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
14
|
+
export { RULES, getRule } from './utils/rule-registry.js';
|
|
15
|
+
export { buildAuditResult, mergeNormalizedResults, normalizeAxeResults, normalizeAutocompleteAudit, normalizeAutoPlayDetection, normalizeFocusCheck, normalizeOrientationCheck, normalizeReflowCheck, normalizeTargetSizeCheck, normalizeTextSpacingCheck, normalizeTimeLimitDetector, normalizeZoomCheck, } from './utils/axe-format.js';
|
|
@@ -16,4 +16,10 @@ export { runAxeAudit, type RunAxeAuditOptions } from './runAxeAudit.js';
|
|
|
16
16
|
export { runFocusIndicatorCheck, type RunFocusIndicatorCheckOptions, } from './runFocusIndicatorCheck.js';
|
|
17
17
|
export { runReflowCheck, type RunReflowCheckOptions } from './runReflowCheck.js';
|
|
18
18
|
export { runTargetSizeCheck, type RunTargetSizeCheckOptions, } from './runTargetSizeCheck.js';
|
|
19
|
-
export {
|
|
19
|
+
export { runTextSpacingCheck, type RunTextSpacingCheckOptions, } from './runTextSpacingCheck.js';
|
|
20
|
+
export { runZoomCheck, type RunZoomCheckOptions } from './runZoomCheck.js';
|
|
21
|
+
export { runOrientationCheck, type RunOrientationCheckOptions, } from './runOrientationCheck.js';
|
|
22
|
+
export { runAutocompleteAudit, type RunAutocompleteAuditOptions, } from './runAutocompleteAudit.js';
|
|
23
|
+
export { runTimeLimitDetector, type RunTimeLimitDetectorOptions, } from './runTimeLimitDetector.js';
|
|
24
|
+
export { runAutoPlayDetection, type RunAutoPlayDetectionOptions, } from './runAutoPlayDetection.js';
|
|
25
|
+
export { resolveOutputPath, getTargetUrl, requireTargetUrl, type OutputLocationOptions, } from '../utils/test-harness.js';
|
package/dist/playwright/index.js
CHANGED
|
@@ -16,4 +16,10 @@ export { runAxeAudit } from './runAxeAudit.js';
|
|
|
16
16
|
export { runFocusIndicatorCheck, } from './runFocusIndicatorCheck.js';
|
|
17
17
|
export { runReflowCheck } from './runReflowCheck.js';
|
|
18
18
|
export { runTargetSizeCheck, } from './runTargetSizeCheck.js';
|
|
19
|
-
export {
|
|
19
|
+
export { runTextSpacingCheck, } from './runTextSpacingCheck.js';
|
|
20
|
+
export { runZoomCheck } from './runZoomCheck.js';
|
|
21
|
+
export { runOrientationCheck, } from './runOrientationCheck.js';
|
|
22
|
+
export { runAutocompleteAudit, } from './runAutocompleteAudit.js';
|
|
23
|
+
export { runTimeLimitDetector, } from './runTimeLimitDetector.js';
|
|
24
|
+
export { runAutoPlayDetection, } from './runAutoPlayDetection.js';
|
|
25
|
+
export { resolveOutputPath, getTargetUrl, requireTargetUrl, } from '../utils/test-harness.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-play Content Detection — WCAG 1.4.2 (Audio Control) / 2.2.2 (Pause, Stop, Hide)
|
|
3
|
+
*
|
|
4
|
+
* Takes screenshots at 2s intervals (0/2/4/6s), pixel-diffs consecutive frames,
|
|
5
|
+
* detects whether visual content continues past 5s, finds pause/stop controls,
|
|
6
|
+
* and verifies whether they work.
|
|
7
|
+
*
|
|
8
|
+
* The caller is responsible for navigating the page before calling this.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: this is the only check that needs `pixelmatch` + `pngjs` (declared
|
|
11
|
+
* as optionalDependencies). To keep them out of the package barrel, the modules
|
|
12
|
+
* that pull them (`utils/image-compare`, `detectors`) are imported LAZILY here,
|
|
13
|
+
* so importing `@a11y-skills/audit/playwright` never requires the optional deps
|
|
14
|
+
* unless `runAutoPlayDetection` is actually called.
|
|
15
|
+
*/
|
|
16
|
+
import type { Page } from '@playwright/test';
|
|
17
|
+
import type { AutoPlayDetectionResult } from '../types.js';
|
|
18
|
+
export interface RunAutoPlayDetectionOptions {
|
|
19
|
+
/** A page already navigated to the target URL. */
|
|
20
|
+
page: Page;
|
|
21
|
+
/**
|
|
22
|
+
* Directory for screenshots, diffs, and the result JSON. Defaults to
|
|
23
|
+
* `<A11Y_OUTPUT_DIR | cwd>/auto-play-screenshots`.
|
|
24
|
+
*/
|
|
25
|
+
outputDir?: string;
|
|
26
|
+
/** Significant-change threshold in percent (default: 0.1). */
|
|
27
|
+
changeThreshold?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run auto-play detection against the current page. Writes screenshots, diff
|
|
31
|
+
* images, and `detection-result.json` into the output directory; returns the
|
|
32
|
+
* result.
|
|
33
|
+
*
|
|
34
|
+
* @throws if the optional `pixelmatch` / `pngjs` deps are not installed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runAutoPlayDetection(options: RunAutoPlayDetectionOptions): Promise<AutoPlayDetectionResult>;
|