@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orientation Check — WCAG 1.3.4 (Orientation)
|
|
3
|
+
*
|
|
4
|
+
* Renders the page in portrait (375x667) and landscape (667x375), detecting
|
|
5
|
+
* "rotate device" messages/overlays and whether the main content is hidden in
|
|
6
|
+
* either orientation. Reports if the page restricts content to a specific
|
|
7
|
+
* orientation.
|
|
8
|
+
*
|
|
9
|
+
* This function OWNS navigation: it sets each viewport then navigates to the
|
|
10
|
+
* target URL for that orientation (two full navigations, not a reload), so the
|
|
11
|
+
* page lays out fresh for each orientation.
|
|
12
|
+
*
|
|
13
|
+
* Limitations:
|
|
14
|
+
* - Heuristics may miss CSS-only orientation restrictions
|
|
15
|
+
* - Cannot detect JavaScript-based orientation detection without visual indicators
|
|
16
|
+
* - Manual verification needed for exceptions (e.g., camera apps)
|
|
17
|
+
*/
|
|
18
|
+
import type { Page } from '@playwright/test';
|
|
19
|
+
import type { OrientationCheckResult } from '../types.js';
|
|
20
|
+
import { type OutputLocationOptions } from '../utils/test-harness.js';
|
|
21
|
+
export interface RunOrientationCheckOptions extends OutputLocationOptions {
|
|
22
|
+
/**
|
|
23
|
+
* A page to drive. This check navigates the page itself (once per
|
|
24
|
+
* orientation), so the page does not need to be pre-navigated.
|
|
25
|
+
*/
|
|
26
|
+
page: Page;
|
|
27
|
+
/** Target URL to audit. Falls back to the `TEST_PAGE` env var. */
|
|
28
|
+
targetUrl?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Whether to capture portrait/landscape screenshots next to the result file
|
|
31
|
+
* (default: false).
|
|
32
|
+
*/
|
|
33
|
+
screenshot?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run the orientation check, navigating the page in both portrait and landscape,
|
|
37
|
+
* write the result JSON (and optionally screenshots), and return the parsed
|
|
38
|
+
* result.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runOrientationCheck(options: RunOrientationCheckOptions): Promise<OrientationCheckResult>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orientation Check — WCAG 1.3.4 (Orientation)
|
|
3
|
+
*
|
|
4
|
+
* Renders the page in portrait (375x667) and landscape (667x375), detecting
|
|
5
|
+
* "rotate device" messages/overlays and whether the main content is hidden in
|
|
6
|
+
* either orientation. Reports if the page restricts content to a specific
|
|
7
|
+
* orientation.
|
|
8
|
+
*
|
|
9
|
+
* This function OWNS navigation: it sets each viewport then navigates to the
|
|
10
|
+
* target URL for that orientation (two full navigations, not a reload), so the
|
|
11
|
+
* page lays out fresh for each orientation.
|
|
12
|
+
*
|
|
13
|
+
* Limitations:
|
|
14
|
+
* - Heuristics may miss CSS-only orientation restrictions
|
|
15
|
+
* - Cannot detect JavaScript-based orientation detection without visual indicators
|
|
16
|
+
* - Manual verification needed for exceptions (e.g., camera apps)
|
|
17
|
+
*/
|
|
18
|
+
import { ORIENTATION_VIEWPORTS, ORIENTATION_LOCK_KEYWORDS, MAIN_CONTENT_SELECTORS, DEFAULT_ORIENTATION_RESULT_FILE, DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE, DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE, } from '../constants.js';
|
|
19
|
+
import { buildAuditResult, normalizeOrientationCheck, } from '../utils/axe-format.js';
|
|
20
|
+
import { saveAuditResult, resolveOutputPath, takeAuditScreenshot, resolveScreenshotPath, requireTargetUrl, logAuditHeader, logOutputPaths, } from '../utils/test-harness.js';
|
|
21
|
+
/** Capture orientation state in browser context. */
|
|
22
|
+
function captureOrientationState(args) {
|
|
23
|
+
const { lockKeywords, mainContentSelectors } = args;
|
|
24
|
+
const bodyWidth = document.body.scrollWidth;
|
|
25
|
+
const bodyHeight = document.body.scrollHeight;
|
|
26
|
+
const visibleText = document.body.innerText || '';
|
|
27
|
+
const visibleTextLength = visibleText.length;
|
|
28
|
+
const lowerText = visibleText.toLowerCase();
|
|
29
|
+
// Search for lock message keywords
|
|
30
|
+
let lockMessageFound = false;
|
|
31
|
+
let lockMessageText = null;
|
|
32
|
+
for (const keyword of lockKeywords) {
|
|
33
|
+
if (lowerText.includes(keyword.toLowerCase())) {
|
|
34
|
+
lockMessageFound = true;
|
|
35
|
+
const allElements = document.querySelectorAll('*');
|
|
36
|
+
for (const el of allElements) {
|
|
37
|
+
const text = el.textContent?.toLowerCase() || '';
|
|
38
|
+
if (text.includes(keyword.toLowerCase()) && text.length < 200) {
|
|
39
|
+
lockMessageText = el.textContent?.trim().slice(0, 100) || null;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Check if main content is hidden
|
|
47
|
+
let mainContentHidden = false;
|
|
48
|
+
for (const selector of mainContentSelectors) {
|
|
49
|
+
const mainEl = document.querySelector(selector);
|
|
50
|
+
if (mainEl) {
|
|
51
|
+
const style = window.getComputedStyle(mainEl);
|
|
52
|
+
const isHidden = style.display === 'none' ||
|
|
53
|
+
style.visibility === 'hidden' ||
|
|
54
|
+
parseFloat(style.opacity) === 0;
|
|
55
|
+
if (isHidden) {
|
|
56
|
+
mainContentHidden = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const rect = mainEl.getBoundingClientRect();
|
|
60
|
+
if (rect.width < 50 || rect.height < 50) {
|
|
61
|
+
mainContentHidden = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
lockMessageFound,
|
|
68
|
+
lockMessageText,
|
|
69
|
+
mainContentHidden,
|
|
70
|
+
bodyWidth,
|
|
71
|
+
bodyHeight,
|
|
72
|
+
visibleTextLength,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Determine where orientation lock was detected. */
|
|
76
|
+
function determineLockLocation(portraitHasLock, landscapeHasLock) {
|
|
77
|
+
if (portraitHasLock && landscapeHasLock) {
|
|
78
|
+
return 'both';
|
|
79
|
+
}
|
|
80
|
+
if (portraitHasLock) {
|
|
81
|
+
return 'portrait';
|
|
82
|
+
}
|
|
83
|
+
if (landscapeHasLock) {
|
|
84
|
+
return 'landscape';
|
|
85
|
+
}
|
|
86
|
+
return 'none';
|
|
87
|
+
}
|
|
88
|
+
/** Log orientation state for a specific orientation. */
|
|
89
|
+
function logOrientationState(label, viewport, state) {
|
|
90
|
+
console.log(`\n${label} (${viewport.width}x${viewport.height}):`);
|
|
91
|
+
console.log(` Lock message found: ${state.lockMessageFound ? 'YES' : 'No'}`);
|
|
92
|
+
if (state.lockMessageText) {
|
|
93
|
+
console.log(` Message: "${state.lockMessageText}"`);
|
|
94
|
+
}
|
|
95
|
+
console.log(` Main content hidden: ${state.mainContentHidden ? 'YES' : 'No'}`);
|
|
96
|
+
console.log(` Body size: ${state.bodyWidth}x${state.bodyHeight}`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the orientation check, navigating the page in both portrait and landscape,
|
|
100
|
+
* write the result JSON (and optionally screenshots), and return the parsed
|
|
101
|
+
* result.
|
|
102
|
+
*/
|
|
103
|
+
export async function runOrientationCheck(options) {
|
|
104
|
+
const { page, targetUrl, screenshot = false, ...location } = options;
|
|
105
|
+
const url = requireTargetUrl(targetUrl);
|
|
106
|
+
const checkArgs = {
|
|
107
|
+
lockKeywords: [...ORIENTATION_LOCK_KEYWORDS],
|
|
108
|
+
mainContentSelectors: [...MAIN_CONTENT_SELECTORS],
|
|
109
|
+
};
|
|
110
|
+
// Resolve where the result will be written up front so screenshots can be
|
|
111
|
+
// placed next to it (mirrors saveAuditResult's resolution).
|
|
112
|
+
const resolvedPath = resolveOutputPath({
|
|
113
|
+
...location,
|
|
114
|
+
defaultFile: DEFAULT_ORIENTATION_RESULT_FILE,
|
|
115
|
+
});
|
|
116
|
+
// Test portrait orientation
|
|
117
|
+
await page.setViewportSize(ORIENTATION_VIEWPORTS.portrait);
|
|
118
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
119
|
+
const portraitState = await page.evaluate(captureOrientationState, checkArgs);
|
|
120
|
+
let portraitScreenshotPath;
|
|
121
|
+
if (screenshot) {
|
|
122
|
+
portraitScreenshotPath = await takeAuditScreenshot(page, {
|
|
123
|
+
path: resolveScreenshotPath(resolvedPath, DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Test landscape orientation
|
|
127
|
+
await page.setViewportSize(ORIENTATION_VIEWPORTS.landscape);
|
|
128
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
129
|
+
const landscapeState = await page.evaluate(captureOrientationState, checkArgs);
|
|
130
|
+
let landscapeScreenshotPath;
|
|
131
|
+
if (screenshot) {
|
|
132
|
+
landscapeScreenshotPath = await takeAuditScreenshot(page, {
|
|
133
|
+
path: resolveScreenshotPath(resolvedPath, DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Determine lock status
|
|
137
|
+
const portraitHasLock = portraitState.lockMessageFound || portraitState.mainContentHidden;
|
|
138
|
+
const landscapeHasLock = landscapeState.lockMessageFound || landscapeState.mainContentHidden;
|
|
139
|
+
const hasOrientationLock = portraitHasLock || landscapeHasLock;
|
|
140
|
+
const lockDetectedIn = determineLockLocation(portraitHasLock, landscapeHasLock);
|
|
141
|
+
const details = {
|
|
142
|
+
portrait: portraitState,
|
|
143
|
+
landscape: landscapeState,
|
|
144
|
+
hasOrientationLock,
|
|
145
|
+
lockDetectedIn,
|
|
146
|
+
};
|
|
147
|
+
const result = buildAuditResult({
|
|
148
|
+
source: 'orientation-check',
|
|
149
|
+
url: page.url(),
|
|
150
|
+
details,
|
|
151
|
+
buckets: normalizeOrientationCheck(details),
|
|
152
|
+
});
|
|
153
|
+
// Output results
|
|
154
|
+
logAuditHeader('Orientation Check Results', 'WCAG 1.3.4', result.url);
|
|
155
|
+
logOrientationState('Portrait', ORIENTATION_VIEWPORTS.portrait, details.portrait);
|
|
156
|
+
logOrientationState('Landscape', ORIENTATION_VIEWPORTS.landscape, details.landscape);
|
|
157
|
+
console.log(`\nOrientation lock detected: ${details.hasOrientationLock ? 'YES' : 'No'}`);
|
|
158
|
+
if (details.hasOrientationLock) {
|
|
159
|
+
console.log(`Lock detected in: ${details.lockDetectedIn}`);
|
|
160
|
+
}
|
|
161
|
+
const writtenPath = saveAuditResult(result, {
|
|
162
|
+
...location,
|
|
163
|
+
defaultFile: DEFAULT_ORIENTATION_RESULT_FILE,
|
|
164
|
+
});
|
|
165
|
+
const screenshotPaths = [portraitScreenshotPath, landscapeScreenshotPath]
|
|
166
|
+
.filter((p) => p !== undefined)
|
|
167
|
+
.join(', ');
|
|
168
|
+
logOutputPaths(writtenPath, screenshotPaths || undefined);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - Cannot distinguish acceptable horizontal scroll (e.g., data tables)
|
|
14
14
|
* - Does not verify functional reflow for complex widgets
|
|
15
15
|
*/
|
|
16
|
-
import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, } from '../constants.js';
|
|
16
|
+
import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
17
17
|
import { createLayoutChecker } from '../utils/layout.js';
|
|
18
|
+
import { buildAuditResult, normalizeReflowCheck } from '../utils/axe-format.js';
|
|
18
19
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
19
20
|
/**
|
|
20
21
|
* Run the reflow check against the current page, write the result JSON
|
|
@@ -28,27 +29,33 @@ export async function runReflowCheck(options) {
|
|
|
28
29
|
overflowTolerance,
|
|
29
30
|
checkSelector: REFLOW_CHECK_SELECTOR,
|
|
30
31
|
allowedOverflowSelectors: [...REFLOW_ALLOWED_OVERFLOW_SELECTORS],
|
|
32
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
31
33
|
});
|
|
32
|
-
const
|
|
33
|
-
url: page.url(),
|
|
34
|
+
const details = {
|
|
34
35
|
viewport: { width: viewport.width, height: viewport.height },
|
|
35
36
|
...layoutResult,
|
|
36
37
|
};
|
|
38
|
+
const result = buildAuditResult({
|
|
39
|
+
source: 'reflow-check',
|
|
40
|
+
url: page.url(),
|
|
41
|
+
details,
|
|
42
|
+
buckets: normalizeReflowCheck(details),
|
|
43
|
+
});
|
|
37
44
|
// Output results
|
|
38
45
|
logAuditHeader('Reflow Check Results', 'WCAG 1.4.10', result.url);
|
|
39
46
|
logSummary({
|
|
40
|
-
Viewport: `${
|
|
41
|
-
'Document scroll width': `${
|
|
42
|
-
'Document client width': `${
|
|
43
|
-
'Horizontal scroll':
|
|
44
|
-
'Overflowing elements':
|
|
45
|
-
'Clipped text elements':
|
|
47
|
+
Viewport: `${details.viewport.width}x${details.viewport.height}`,
|
|
48
|
+
'Document scroll width': `${details.documentScrollWidth}px`,
|
|
49
|
+
'Document client width': `${details.documentClientWidth}px`,
|
|
50
|
+
'Horizontal scroll': details.hasHorizontalScroll,
|
|
51
|
+
'Overflowing elements': details.overflowingElements.length,
|
|
52
|
+
'Clipped text elements': details.clippedTextElements.length,
|
|
46
53
|
});
|
|
47
|
-
logIssueList('Overflowing Elements',
|
|
54
|
+
logIssueList('Overflowing Elements', details.overflowingElements, (el, i) => [
|
|
48
55
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
49
56
|
` rect.right: ${el.rect.right}px (viewport: ${el.viewportWidth}px)`,
|
|
50
57
|
]);
|
|
51
|
-
logIssueList('Clipped Text Elements',
|
|
58
|
+
logIssueList('Clipped Text Elements', details.clippedTextElements, (el, i) => [
|
|
52
59
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
53
60
|
` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
|
|
54
61
|
` overflow: ${el.overflow}, overflowX: ${el.overflowX}`,
|
|
@@ -15,13 +15,31 @@
|
|
|
15
15
|
* - Cannot detect all redundant target cases
|
|
16
16
|
* - CSS transform may affect measurements
|
|
17
17
|
*/
|
|
18
|
-
import { INTERACTIVE_SELECTOR, TARGET_SIZE_AA, TARGET_SIZE_AAA, INLINE_CONTEXT_TAGS, UA_CONTROLLED_INPUT_TYPES, INLINE_CONTEXT_MIN_TEXT, DEFAULT_TARGET_SIZE_RESULT_FILE, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE, } from '../constants.js';
|
|
18
|
+
import { INTERACTIVE_SELECTOR, TARGET_SIZE_AA, TARGET_SIZE_AAA, INLINE_CONTEXT_TAGS, UA_CONTROLLED_INPUT_TYPES, INLINE_CONTEXT_MIN_TEXT, DEFAULT_TARGET_SIZE_RESULT_FILE, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
19
|
+
import { buildAuditResult, normalizeTargetSizeCheck, } from '../utils/axe-format.js';
|
|
19
20
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
20
21
|
import { addPageAnnotations } from '../utils/annotations.js';
|
|
21
22
|
/**
|
|
22
23
|
* Collect basic target information from DOM (runs in browser context).
|
|
23
24
|
*/
|
|
24
|
-
function collectBasicTargetInfo(
|
|
25
|
+
function collectBasicTargetInfo(args) {
|
|
26
|
+
const { interactiveSelector, htmlSnippetMaxLength } = args;
|
|
27
|
+
function getHtmlSnippet(element) {
|
|
28
|
+
let html = '';
|
|
29
|
+
try {
|
|
30
|
+
html = element.outerHTML || '';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
html = '';
|
|
34
|
+
}
|
|
35
|
+
if (!html) {
|
|
36
|
+
return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
37
|
+
}
|
|
38
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
39
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
40
|
+
}
|
|
41
|
+
return { html, htmlTruncated: false };
|
|
42
|
+
}
|
|
25
43
|
function getUniqueSelector(element, elementIndex) {
|
|
26
44
|
if (element.id) {
|
|
27
45
|
return `#${CSS.escape(element.id)}`;
|
|
@@ -72,6 +90,7 @@ function collectBasicTargetInfo(interactiveSelector) {
|
|
|
72
90
|
targets.push({
|
|
73
91
|
selector: getUniqueSelector(element, index),
|
|
74
92
|
tagName,
|
|
93
|
+
...getHtmlSnippet(element),
|
|
75
94
|
role,
|
|
76
95
|
width: Math.round(rect.width * 100) / 100,
|
|
77
96
|
height: Math.round(rect.height * 100) / 100,
|
|
@@ -247,6 +266,8 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
247
266
|
const issue = {
|
|
248
267
|
selector: target.selector,
|
|
249
268
|
tagName: target.tagName,
|
|
269
|
+
html: target.html,
|
|
270
|
+
htmlTruncated: target.htmlTruncated,
|
|
250
271
|
role: target.role,
|
|
251
272
|
accessibleName: target.accessibleName,
|
|
252
273
|
width: target.width,
|
|
@@ -255,6 +276,9 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
255
276
|
level,
|
|
256
277
|
exception,
|
|
257
278
|
exceptionDetails,
|
|
279
|
+
// the heuristics can detect exceptions but can never rule out the
|
|
280
|
+
// essential exception, so findings are at best 'possible'/'not-assessed'.
|
|
281
|
+
exceptionAssessment: exception ? 'possible' : 'not-assessed',
|
|
258
282
|
href: target.href,
|
|
259
283
|
};
|
|
260
284
|
if (exception) {
|
|
@@ -276,7 +300,10 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
276
300
|
export async function runTargetSizeCheck(options) {
|
|
277
301
|
const { page, aaThreshold = TARGET_SIZE_AA, aaaThreshold = TARGET_SIZE_AAA, screenshot = false, ...location } = options;
|
|
278
302
|
// Collect basic target info from DOM
|
|
279
|
-
const basicTargets = await page.evaluate(collectBasicTargetInfo,
|
|
303
|
+
const basicTargets = await page.evaluate(collectBasicTargetInfo, {
|
|
304
|
+
interactiveSelector: INTERACTIVE_SELECTOR,
|
|
305
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
306
|
+
});
|
|
280
307
|
// Enhance with accessible names via ariaSnapshot()
|
|
281
308
|
const targets = [];
|
|
282
309
|
for (const basicTarget of basicTargets) {
|
|
@@ -296,8 +323,7 @@ export async function runTargetSizeCheck(options) {
|
|
|
296
323
|
}
|
|
297
324
|
// Analyze targets
|
|
298
325
|
const { failAA, failAAAOnly, passCount, excepted } = analyzeTargets(targets, aaThreshold, aaaThreshold);
|
|
299
|
-
const
|
|
300
|
-
url: page.url(),
|
|
326
|
+
const details = {
|
|
301
327
|
totalTargetsChecked: targets.length,
|
|
302
328
|
failAA,
|
|
303
329
|
failAAAOnly,
|
|
@@ -310,16 +336,22 @@ export async function runTargetSizeCheck(options) {
|
|
|
310
336
|
exceptedCount: excepted.length,
|
|
311
337
|
},
|
|
312
338
|
};
|
|
339
|
+
const result = buildAuditResult({
|
|
340
|
+
source: 'target-size-check',
|
|
341
|
+
url: page.url(),
|
|
342
|
+
details,
|
|
343
|
+
buckets: normalizeTargetSizeCheck(details),
|
|
344
|
+
});
|
|
313
345
|
// Output results
|
|
314
346
|
logAuditHeader('Target Size Check Results', 'WCAG 2.5.5 / 2.5.8', result.url);
|
|
315
347
|
logSummary({
|
|
316
|
-
'Total targets checked':
|
|
348
|
+
'Total targets checked': details.totalTargetsChecked,
|
|
317
349
|
});
|
|
318
350
|
console.log('\nSummary:');
|
|
319
|
-
console.log(` Pass (>= ${aaaThreshold}px): ${
|
|
320
|
-
console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${
|
|
321
|
-
console.log(` Fail AA (< ${aaThreshold}px): ${
|
|
322
|
-
console.log(` Possible exceptions: ${
|
|
351
|
+
console.log(` Pass (>= ${aaaThreshold}px): ${details.summary.passCount}`);
|
|
352
|
+
console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${details.summary.failAAAOnlyCount}`);
|
|
353
|
+
console.log(` Fail AA (< ${aaThreshold}px): ${details.summary.failAACount}`);
|
|
354
|
+
console.log(` Possible exceptions: ${details.summary.exceptedCount}`);
|
|
323
355
|
logIssueList(`Fail AA (< ${aaThreshold}px) - Requires Fix`, failAA, (el, i) => {
|
|
324
356
|
const lines = [
|
|
325
357
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Spacing Check — WCAG 1.4.12 (Text Spacing)
|
|
3
|
+
*
|
|
4
|
+
* Captures baseline metrics, injects the WCAG 1.4.12 spacing overrides, and
|
|
5
|
+
* reports elements whose text becomes clipped (overflow:hidden) under the
|
|
6
|
+
* increased spacing.
|
|
7
|
+
*
|
|
8
|
+
* The caller is responsible for navigating the page before calling this.
|
|
9
|
+
*/
|
|
10
|
+
import type { Page } from '@playwright/test';
|
|
11
|
+
import type { TextSpacingCheckResult } from '../types.js';
|
|
12
|
+
import { type OutputLocationOptions } from '../utils/test-harness.js';
|
|
13
|
+
export interface RunTextSpacingCheckOptions extends OutputLocationOptions {
|
|
14
|
+
/** A page already navigated to the target URL. */
|
|
15
|
+
page: Page;
|
|
16
|
+
/** Tolerance in pixels for clip detection (default: 2). */
|
|
17
|
+
tolerance?: number;
|
|
18
|
+
/** Whether to capture a screenshot next to the result file (default: false). */
|
|
19
|
+
screenshot?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run the text spacing check against the current page, write the result JSON
|
|
23
|
+
* (and optionally a screenshot), and return the parsed result.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runTextSpacingCheck(options: RunTextSpacingCheckOptions): Promise<TextSpacingCheckResult>;
|