@detiq/crawler 1.1.13 → 1.1.15
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/Dockerfile +1 -1
- package/dist/accessibility-tree-crawler.js +156 -0
- package/dist/accessibility-tree-crawler.js.map +7 -0
- package/dist/advanced-ui-probes.js +310 -0
- package/dist/advanced-ui-probes.js.map +7 -0
- package/dist/content-chunker.js +2 -2
- package/dist/content-chunker.js.map +2 -2
- package/dist/crawl-session-manager.js +145 -0
- package/dist/crawl-session-manager.js.map +7 -0
- package/dist/crawler.js +1142 -263
- package/dist/crawler.js.map +2 -2
- package/dist/crud-interaction.js +264 -0
- package/dist/crud-interaction.js.map +7 -0
- package/dist/dropdown-enumerator.js +230 -0
- package/dist/dropdown-enumerator.js.map +7 -0
- package/dist/filter-pagination-crawler.js +299 -0
- package/dist/filter-pagination-crawler.js.map +7 -0
- package/dist/frontier.js +5 -3
- package/dist/frontier.js.map +2 -2
- package/dist/gap-pass.js +47 -14
- package/dist/gap-pass.js.map +2 -2
- package/dist/http-engine.js +28 -15
- package/dist/http-engine.js.map +2 -2
- package/dist/index.js +15 -1
- package/dist/index.js.map +2 -2
- package/dist/job-queue.js +12 -5
- package/dist/job-queue.js.map +2 -2
- package/dist/locator-extractor.js +23 -0
- package/dist/locator-extractor.js.map +2 -2
- package/dist/multi-context-handler.js +182 -0
- package/dist/multi-context-handler.js.map +7 -0
- package/dist/nested-window-crawler.js +172 -0
- package/dist/nested-window-crawler.js.map +7 -0
- package/dist/network-action-correlator.js +209 -0
- package/dist/network-action-correlator.js.map +7 -0
- package/dist/performance-metrics-collector.js +155 -0
- package/dist/performance-metrics-collector.js.map +7 -0
- package/dist/performance-metrics.js.map +2 -2
- package/dist/policies/crawl-safety.js +5 -1
- package/dist/policies/crawl-safety.js.map +2 -2
- package/dist/profiles/domain-profile.js +21 -1
- package/dist/profiles/domain-profile.js.map +2 -2
- package/dist/rate-limiter.js +2 -2
- package/dist/rate-limiter.js.map +2 -2
- package/dist/screen-reader.js +1 -0
- package/dist/screen-reader.js.map +2 -2
- package/dist/service-worker-interceptor.js +136 -0
- package/dist/service-worker-interceptor.js.map +7 -0
- package/dist/session-capture.js +4 -2
- package/dist/session-capture.js.map +2 -2
- package/dist/shadow-dom-crawler.js +132 -0
- package/dist/shadow-dom-crawler.js.map +7 -0
- package/dist/sitemap.js +16 -10
- package/dist/sitemap.js.map +2 -2
- package/dist/state-graph.js +193 -0
- package/dist/state-graph.js.map +7 -0
- package/dist/stealth/human-behavior.js +36 -3
- package/dist/stealth/human-behavior.js.map +2 -2
- package/dist/url-pattern-completer.js +48 -14
- package/dist/url-pattern-completer.js.map +2 -2
- package/dist/visual-diff-verdict.js +3 -1
- package/dist/visual-diff-verdict.js.map +2 -2
- package/package.json +2 -1
- package/src/accessibility-tree-crawler.ts +246 -0
- package/src/advanced-ui-probes.ts +470 -0
- package/src/crawl-session-manager.ts +231 -0
- package/src/crawler.ts +1071 -142
- package/src/crud-interaction.ts +386 -0
- package/src/dropdown-enumerator.ts +312 -0
- package/src/filter-pagination-crawler.ts +500 -0
- package/src/gap-pass.ts +58 -16
- package/src/http-engine.ts +29 -15
- package/src/index.ts +14 -1
- package/src/job-queue.ts +3 -1
- package/src/locator-extractor.ts +45 -0
- package/src/multi-context-handler.ts +296 -0
- package/src/nested-window-crawler.ts +310 -0
- package/src/network-action-correlator.ts +320 -0
- package/src/performance-metrics-collector.ts +243 -0
- package/src/performance-metrics.ts +1 -1
- package/src/screen-reader.ts +4 -1
- package/src/service-worker-interceptor.ts +185 -0
- package/src/session-capture.ts +3 -0
- package/src/shadow-dom-crawler.ts +198 -0
- package/src/sitemap.ts +17 -10
- package/src/state-graph.ts +280 -0
- package/src/stealth/human-behavior.ts +38 -4
- package/src/url-pattern-completer.ts +61 -15
- package/tsconfig.json +1 -1
- package/types/accessibility-tree-crawler.d.ts +44 -0
- package/types/advanced-ui-probes.d.ts +47 -0
- package/types/crawl-session-manager.d.ts +44 -0
- package/types/crawler.d.ts +3 -1
- package/types/crud-interaction.d.ts +31 -0
- package/types/dropdown-enumerator.d.ts +20 -0
- package/types/filter-pagination-crawler.d.ts +52 -0
- package/types/gap-pass.d.ts +1 -0
- package/types/index.d.ts +14 -1
- package/types/job-queue.d.ts +18 -9
- package/types/locator-extractor.d.ts +7 -0
- package/types/multi-context-handler.d.ts +55 -0
- package/types/nested-window-crawler.d.ts +41 -0
- package/types/network-action-correlator.d.ts +47 -0
- package/types/performance-metrics-collector.d.ts +50 -0
- package/types/performance-metrics.d.ts +1 -2
- package/types/screen-reader.d.ts +1 -2
- package/types/service-worker-interceptor.d.ts +36 -0
- package/types/session-capture.d.ts +1 -1
- package/types/shadow-dom-crawler.d.ts +35 -0
- package/types/sitemap.d.ts +2 -2
- package/types/state-graph.d.ts +59 -0
- package/types/stealth/human-behavior.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@detiq/crawler",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"axe-core": "^4.12.1",
|
|
26
26
|
"camoufox": "^0.1.19",
|
|
27
27
|
"fast-xml-parser": "^5.10.1",
|
|
28
|
+
"minimatch": "^10.0.0",
|
|
28
29
|
"imapflow": "^1.4.3",
|
|
29
30
|
"lighthouse": "^13.4.0",
|
|
30
31
|
"linkedom": "^0.18.12",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility tree crawler — extract landmark, heading, and ARIA metadata from crawled pages.
|
|
3
|
+
*
|
|
4
|
+
* Captures: landmarks with interactive counts, heading hierarchy with violation detection,
|
|
5
|
+
* interactive elements missing accessible names, live regions, and skip-link presence.
|
|
6
|
+
*
|
|
7
|
+
* This data feeds ZeTa's accessibility test generator to surface WCAG 2.1 Level A/AA
|
|
8
|
+
* failures (missing names, heading skips, absent skip links, unlabeled live regions).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Page } from 'playwright';
|
|
12
|
+
|
|
13
|
+
export interface LandmarkInfo {
|
|
14
|
+
role: string;
|
|
15
|
+
label: string;
|
|
16
|
+
elementId: string;
|
|
17
|
+
interactiveCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HeadingInfo {
|
|
21
|
+
level: number;
|
|
22
|
+
text: string;
|
|
23
|
+
elementId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MissingNameEl {
|
|
27
|
+
tagName: string;
|
|
28
|
+
role: string;
|
|
29
|
+
elementId: string;
|
|
30
|
+
htmlSnippet: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LiveRegionInfo {
|
|
34
|
+
role: string;
|
|
35
|
+
ariaLive: string;
|
|
36
|
+
ariaAtomic: string;
|
|
37
|
+
currentText: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AccessibilityTreeResult {
|
|
41
|
+
landmarkCount: number;
|
|
42
|
+
landmarks: LandmarkInfo[];
|
|
43
|
+
headings: HeadingInfo[];
|
|
44
|
+
headingHierarchyViolations: string[];
|
|
45
|
+
missingNameElements: MissingNameEl[];
|
|
46
|
+
missingNameCount: number;
|
|
47
|
+
liveRegions: LiveRegionInfo[];
|
|
48
|
+
totalInteractiveCount: number;
|
|
49
|
+
hasSkipLink: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function extractLandmarks(page: any): Promise<LandmarkInfo[]> {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await page.evaluate(() => {
|
|
55
|
+
const LANDMARK_SELECTORS = [
|
|
56
|
+
{ role: 'banner', selector: 'header,[role="banner"]' },
|
|
57
|
+
{ role: 'navigation', selector: 'nav,[role="navigation"]' },
|
|
58
|
+
{ role: 'main', selector: 'main,[role="main"]' },
|
|
59
|
+
{ role: 'complementary', selector: 'aside,[role="complementary"]' },
|
|
60
|
+
{ role: 'contentinfo', selector: 'footer,[role="contentinfo"]' },
|
|
61
|
+
{ role: 'search', selector: '[role="search"]' },
|
|
62
|
+
{ role: 'form', selector: 'form[aria-label],[role="form"]' },
|
|
63
|
+
{ role: 'region', selector: '[role="region"][aria-label]' },
|
|
64
|
+
];
|
|
65
|
+
return LANDMARK_SELECTORS.flatMap(({ role, selector }) =>
|
|
66
|
+
[...document.querySelectorAll(selector)].slice(0, 10).map(el => ({
|
|
67
|
+
role,
|
|
68
|
+
label: el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || '',
|
|
69
|
+
elementId: el.id || '',
|
|
70
|
+
interactiveCount: el.querySelectorAll(
|
|
71
|
+
'button,a[href],input,select,textarea,[role="button"],[tabindex="0"]'
|
|
72
|
+
).length,
|
|
73
|
+
}))
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
return raw.slice(0, 50);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function extractHeadings(page: any): Promise<HeadingInfo[]> {
|
|
83
|
+
try {
|
|
84
|
+
const raw = await page.evaluate(() => {
|
|
85
|
+
const headingEls = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]')];
|
|
86
|
+
return headingEls
|
|
87
|
+
.map(el => ({
|
|
88
|
+
level:
|
|
89
|
+
parseInt(el.tagName.slice(1)) ||
|
|
90
|
+
parseInt(el.getAttribute('aria-level') || '0') ||
|
|
91
|
+
0,
|
|
92
|
+
text: el.textContent?.trim().slice(0, 100) || '',
|
|
93
|
+
elementId: el.id || '',
|
|
94
|
+
}))
|
|
95
|
+
.filter(h => h.level >= 1 && h.level <= 6);
|
|
96
|
+
});
|
|
97
|
+
return raw.slice(0, 100);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectHeadingViolations(headings: HeadingInfo[]): string[] {
|
|
104
|
+
const violations: string[] = [];
|
|
105
|
+
if (headings.length === 0) return violations;
|
|
106
|
+
|
|
107
|
+
const h1s = headings.filter(h => h.level === 1);
|
|
108
|
+
if (h1s.length === 0) {
|
|
109
|
+
violations.push('Missing h1 landmark');
|
|
110
|
+
} else if (h1s.length > 1) {
|
|
111
|
+
violations.push('Multiple h1 elements found');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let prevLevel = 0;
|
|
115
|
+
for (const heading of headings) {
|
|
116
|
+
if (prevLevel > 0 && heading.level > prevLevel + 1) {
|
|
117
|
+
violations.push(
|
|
118
|
+
`Heading level skip: h${prevLevel} to h${heading.level}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
prevLevel = heading.level;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return violations;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function findMissingNames(page: any): Promise<MissingNameEl[]> {
|
|
128
|
+
try {
|
|
129
|
+
return await page.evaluate(() => {
|
|
130
|
+
const INTERACTIVE_SELECTOR =
|
|
131
|
+
'button,a[href],input:not([type="hidden"]),select,textarea,' +
|
|
132
|
+
'[role="button"],[role="link"],[role="checkbox"],[role="radio"],' +
|
|
133
|
+
'[role="switch"],[role="menuitem"],[role="tab"]';
|
|
134
|
+
return [...document.querySelectorAll(INTERACTIVE_SELECTOR)]
|
|
135
|
+
.filter(el => {
|
|
136
|
+
const labelledById = el.getAttribute('aria-labelledby') || '';
|
|
137
|
+
const name =
|
|
138
|
+
(el.getAttribute('aria-label') || '').trim() ||
|
|
139
|
+
(labelledById
|
|
140
|
+
? document.getElementById(labelledById)?.textContent?.trim() ?? ''
|
|
141
|
+
: '') ||
|
|
142
|
+
(el.textContent || '').trim() ||
|
|
143
|
+
((el as HTMLInputElement).placeholder || '').trim() ||
|
|
144
|
+
((el as HTMLElement).title || '').trim() ||
|
|
145
|
+
((el as HTMLImageElement).alt || '').trim();
|
|
146
|
+
return !name || name.length === 0;
|
|
147
|
+
})
|
|
148
|
+
.slice(0, 30)
|
|
149
|
+
.map(el => ({
|
|
150
|
+
tagName: el.tagName.toLowerCase(),
|
|
151
|
+
role: el.getAttribute('role') || '',
|
|
152
|
+
elementId: el.id || '',
|
|
153
|
+
htmlSnippet: el.outerHTML.slice(0, 120),
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function extractLiveRegions(page: any): Promise<LiveRegionInfo[]> {
|
|
162
|
+
try {
|
|
163
|
+
return await page.evaluate(() => {
|
|
164
|
+
const els = [
|
|
165
|
+
...document.querySelectorAll(
|
|
166
|
+
'[aria-live],[role="alert"],[role="status"],[role="log"]'
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
return els.slice(0, 20).map(el => ({
|
|
170
|
+
role: el.getAttribute('role') || '',
|
|
171
|
+
ariaLive: el.getAttribute('aria-live') || '',
|
|
172
|
+
ariaAtomic: el.getAttribute('aria-atomic') || '',
|
|
173
|
+
currentText: (el.textContent || '').trim().slice(0, 200),
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function checkSkipLink(page: any): Promise<boolean> {
|
|
182
|
+
try {
|
|
183
|
+
return await page.evaluate(() => {
|
|
184
|
+
const firstFocusable = document.querySelector('a[href^="#"],a:first-of-type');
|
|
185
|
+
return !!(
|
|
186
|
+
firstFocusable &&
|
|
187
|
+
/skip/i.test(
|
|
188
|
+
(firstFocusable.textContent || '') + (firstFocusable.getAttribute('href') || '')
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function crawlAccessibilityTree(page: any): Promise<AccessibilityTreeResult> {
|
|
198
|
+
const [
|
|
199
|
+
landmarksResult,
|
|
200
|
+
headingsResult,
|
|
201
|
+
missingNamesResult,
|
|
202
|
+
liveRegionsResult,
|
|
203
|
+
skipLinkResult,
|
|
204
|
+
interactiveCountResult,
|
|
205
|
+
] = await Promise.allSettled([
|
|
206
|
+
extractLandmarks(page),
|
|
207
|
+
extractHeadings(page),
|
|
208
|
+
findMissingNames(page),
|
|
209
|
+
extractLiveRegions(page),
|
|
210
|
+
checkSkipLink(page),
|
|
211
|
+
page.evaluate(() => {
|
|
212
|
+
const INTERACTIVE_SELECTOR =
|
|
213
|
+
'button,a[href],input:not([type="hidden"]),select,textarea,' +
|
|
214
|
+
'[role="button"],[role="link"],[role="checkbox"],[role="radio"],' +
|
|
215
|
+
'[role="switch"],[role="menuitem"],[role="tab"]';
|
|
216
|
+
return document.querySelectorAll(INTERACTIVE_SELECTOR).length;
|
|
217
|
+
}).catch(() => 0),
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const landmarks =
|
|
221
|
+
landmarksResult.status === 'fulfilled' ? landmarksResult.value : [];
|
|
222
|
+
const headings =
|
|
223
|
+
headingsResult.status === 'fulfilled' ? headingsResult.value : [];
|
|
224
|
+
const missingNameElements =
|
|
225
|
+
missingNamesResult.status === 'fulfilled' ? missingNamesResult.value : [];
|
|
226
|
+
const liveRegions =
|
|
227
|
+
liveRegionsResult.status === 'fulfilled' ? liveRegionsResult.value : [];
|
|
228
|
+
const hasSkipLink =
|
|
229
|
+
skipLinkResult.status === 'fulfilled' ? skipLinkResult.value : false;
|
|
230
|
+
const totalInteractiveCount =
|
|
231
|
+
interactiveCountResult.status === 'fulfilled' ? interactiveCountResult.value : 0;
|
|
232
|
+
|
|
233
|
+
const headingHierarchyViolations = detectHeadingViolations(headings);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
landmarkCount: landmarks.length,
|
|
237
|
+
landmarks,
|
|
238
|
+
headings,
|
|
239
|
+
headingHierarchyViolations,
|
|
240
|
+
missingNameElements,
|
|
241
|
+
missingNameCount: missingNameElements.length,
|
|
242
|
+
liveRegions,
|
|
243
|
+
totalInteractiveCount,
|
|
244
|
+
hasSkipLink,
|
|
245
|
+
};
|
|
246
|
+
}
|