@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance metrics collector — captures Core Web Vitals and navigation timing
|
|
3
|
+
* for pages visited during crawl runs.
|
|
4
|
+
* Collects LCP, CLS, INP, TTFB, long tasks, and slow resources via PerformanceObserver
|
|
5
|
+
* and the Navigation/Resource Timing APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Page } from 'playwright';
|
|
9
|
+
|
|
10
|
+
export type CwvScore = 'good' | 'needs-improvement' | 'poor';
|
|
11
|
+
|
|
12
|
+
export interface LcpMetric {
|
|
13
|
+
value: number;
|
|
14
|
+
score: CwvScore;
|
|
15
|
+
elementTag: string | null;
|
|
16
|
+
url: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ClsMetric {
|
|
20
|
+
value: number;
|
|
21
|
+
score: CwvScore;
|
|
22
|
+
shiftCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface InpMetric {
|
|
26
|
+
p75: number;
|
|
27
|
+
score: CwvScore;
|
|
28
|
+
sampleCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NavTimingMetric {
|
|
32
|
+
ttfb: number;
|
|
33
|
+
ttfbScore: CwvScore;
|
|
34
|
+
domContentLoaded: number;
|
|
35
|
+
domComplete: number;
|
|
36
|
+
loadComplete: number;
|
|
37
|
+
transferSizeBytes: number;
|
|
38
|
+
protocol: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SlowResource {
|
|
42
|
+
path: string;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
transferSizeBytes: number;
|
|
45
|
+
initiatorType: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PerformanceMetricsResult {
|
|
49
|
+
lcp: LcpMetric | null;
|
|
50
|
+
cls: ClsMetric | null;
|
|
51
|
+
inp: InpMetric | null;
|
|
52
|
+
navTiming: NavTimingMetric | null;
|
|
53
|
+
slowResources: SlowResource[];
|
|
54
|
+
longTaskCount: number;
|
|
55
|
+
captureSuccess: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Observer injection — call before page navigation via addInitScript
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export async function injectPerformanceObservers(page: Page): Promise<void> {
|
|
63
|
+
await page.addInitScript(() => {
|
|
64
|
+
(window as any).__zetaPerf = { lcp: null, cls: 0, clsCount: 0, inp: [], longTaskCount: 0 };
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
new PerformanceObserver(list => {
|
|
68
|
+
const entries = list.getEntries();
|
|
69
|
+
const last = entries[entries.length - 1] as any;
|
|
70
|
+
if (last) {
|
|
71
|
+
(window as any).__zetaPerf.lcp = {
|
|
72
|
+
value: last.startTime,
|
|
73
|
+
elementTag: last.element?.tagName || null,
|
|
74
|
+
url: last.url || null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}).observe({ entryTypes: ['largest-contentful-paint'] });
|
|
78
|
+
} catch {}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
new PerformanceObserver(list => {
|
|
82
|
+
for (const entry of list.getEntries() as any[]) {
|
|
83
|
+
if (!entry.hadRecentInput) {
|
|
84
|
+
(window as any).__zetaPerf.cls += entry.value;
|
|
85
|
+
(window as any).__zetaPerf.clsCount++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}).observe({ entryTypes: ['layout-shift'] });
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
new PerformanceObserver(list => {
|
|
93
|
+
for (const entry of list.getEntries() as any[]) {
|
|
94
|
+
(window as any).__zetaPerf.inp.push(entry.duration);
|
|
95
|
+
}
|
|
96
|
+
}).observe({ entryTypes: ['event'], durationThreshold: 40 } as any);
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
new PerformanceObserver(list => {
|
|
101
|
+
(window as any).__zetaPerf.longTaskCount += list.getEntries().length;
|
|
102
|
+
}).observe({ entryTypes: ['longtask'] });
|
|
103
|
+
} catch {}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Scoring helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function scoreLcp(value: number): CwvScore {
|
|
112
|
+
if (value <= 2500) return 'good';
|
|
113
|
+
if (value <= 4000) return 'needs-improvement';
|
|
114
|
+
return 'poor';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scoreCls(value: number): CwvScore {
|
|
118
|
+
if (value <= 0.1) return 'good';
|
|
119
|
+
if (value <= 0.25) return 'needs-improvement';
|
|
120
|
+
return 'poor';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function scoreInp(p75: number): CwvScore {
|
|
124
|
+
if (p75 <= 200) return 'good';
|
|
125
|
+
if (p75 <= 500) return 'needs-improvement';
|
|
126
|
+
return 'poor';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scoreTtfb(value: number): CwvScore {
|
|
130
|
+
if (value <= 800) return 'good';
|
|
131
|
+
if (value <= 1800) return 'needs-improvement';
|
|
132
|
+
return 'poor';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Internal collector
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async function collectMetrics(page: Page): Promise<PerformanceMetricsResult> {
|
|
140
|
+
const rawMetrics = await page.evaluate(() => {
|
|
141
|
+
const p = (window as any).__zetaPerf || {};
|
|
142
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;
|
|
143
|
+
const resources = performance.getEntriesByType('resource')
|
|
144
|
+
.filter(r => r.duration > 500)
|
|
145
|
+
.sort((a, b) => b.duration - a.duration)
|
|
146
|
+
.slice(0, 10)
|
|
147
|
+
.map(r => ({
|
|
148
|
+
path: (() => { try { return new URL(r.name).pathname; } catch { return r.name.slice(0, 80); } })(),
|
|
149
|
+
durationMs: Math.round(r.duration),
|
|
150
|
+
transferSizeBytes: (r as any).transferSize || 0,
|
|
151
|
+
initiatorType: (r as any).initiatorType || '',
|
|
152
|
+
}));
|
|
153
|
+
const inpValues = p.inp || [];
|
|
154
|
+
const p75 = inpValues.length > 0
|
|
155
|
+
? inpValues.sort((a: number, b: number) => a - b)[Math.floor(inpValues.length * 0.75)]
|
|
156
|
+
: 0;
|
|
157
|
+
return {
|
|
158
|
+
lcp: p.lcp || null,
|
|
159
|
+
cls: p.cls ?? null,
|
|
160
|
+
clsCount: p.clsCount ?? 0,
|
|
161
|
+
inpP75: p75,
|
|
162
|
+
inpCount: inpValues.length,
|
|
163
|
+
longTaskCount: p.longTaskCount || 0,
|
|
164
|
+
nav: nav ? {
|
|
165
|
+
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
|
166
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
167
|
+
domComplete: Math.round(nav.domComplete - nav.startTime),
|
|
168
|
+
loadComplete: Math.round(nav.loadEventEnd - nav.startTime),
|
|
169
|
+
transferSizeBytes: (nav as any).transferSize || 0,
|
|
170
|
+
protocol: (nav as any).nextHopProtocol || '',
|
|
171
|
+
} : null,
|
|
172
|
+
resources,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const lcp: LcpMetric | null = rawMetrics.lcp
|
|
177
|
+
? {
|
|
178
|
+
value: rawMetrics.lcp.value,
|
|
179
|
+
score: scoreLcp(rawMetrics.lcp.value),
|
|
180
|
+
elementTag: rawMetrics.lcp.elementTag,
|
|
181
|
+
url: rawMetrics.lcp.url,
|
|
182
|
+
}
|
|
183
|
+
: null;
|
|
184
|
+
|
|
185
|
+
const cls: ClsMetric | null = rawMetrics.cls !== null
|
|
186
|
+
? {
|
|
187
|
+
value: rawMetrics.cls,
|
|
188
|
+
score: scoreCls(rawMetrics.cls),
|
|
189
|
+
shiftCount: rawMetrics.clsCount,
|
|
190
|
+
}
|
|
191
|
+
: null;
|
|
192
|
+
|
|
193
|
+
const inp: InpMetric | null = rawMetrics.inpCount > 0
|
|
194
|
+
? {
|
|
195
|
+
p75: rawMetrics.inpP75,
|
|
196
|
+
score: scoreInp(rawMetrics.inpP75),
|
|
197
|
+
sampleCount: rawMetrics.inpCount,
|
|
198
|
+
}
|
|
199
|
+
: null;
|
|
200
|
+
|
|
201
|
+
const navTiming: NavTimingMetric | null = rawMetrics.nav
|
|
202
|
+
? {
|
|
203
|
+
ttfb: rawMetrics.nav.ttfb,
|
|
204
|
+
ttfbScore: scoreTtfb(rawMetrics.nav.ttfb),
|
|
205
|
+
domContentLoaded: rawMetrics.nav.domContentLoaded,
|
|
206
|
+
domComplete: rawMetrics.nav.domComplete,
|
|
207
|
+
loadComplete: rawMetrics.nav.loadComplete,
|
|
208
|
+
transferSizeBytes: rawMetrics.nav.transferSizeBytes,
|
|
209
|
+
protocol: rawMetrics.nav.protocol,
|
|
210
|
+
}
|
|
211
|
+
: null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
lcp,
|
|
215
|
+
cls,
|
|
216
|
+
inp,
|
|
217
|
+
navTiming,
|
|
218
|
+
slowResources: rawMetrics.resources,
|
|
219
|
+
longTaskCount: rawMetrics.longTaskCount,
|
|
220
|
+
captureSuccess: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Public API
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const emptyResult: Omit<PerformanceMetricsResult, 'captureSuccess'> = {
|
|
229
|
+
lcp: null,
|
|
230
|
+
cls: null,
|
|
231
|
+
inp: null,
|
|
232
|
+
navTiming: null,
|
|
233
|
+
slowResources: [],
|
|
234
|
+
longTaskCount: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export async function collectPagePerformance(page: Page): Promise<PerformanceMetricsResult> {
|
|
238
|
+
try {
|
|
239
|
+
return await collectMetrics(page);
|
|
240
|
+
} catch {
|
|
241
|
+
return { ...emptyResult, captureSuccess: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -40,7 +40,7 @@ export interface PagePerfMetrics {
|
|
|
40
40
|
measuredAt: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export async function collectPerfMetrics(page:
|
|
43
|
+
export async function collectPerfMetrics(page: any, url: string): Promise<PagePerfMetrics> {
|
|
44
44
|
const measuredAt = new Date().toISOString();
|
|
45
45
|
|
|
46
46
|
// Inject PerformanceObserver for LCP + CLS, wait up to 3s
|
package/src/screen-reader.ts
CHANGED
|
@@ -15,8 +15,11 @@ export interface RawElement {
|
|
|
15
15
|
expectedData?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export async function readScreen(page:
|
|
18
|
+
export async function readScreen(page: any): Promise<RawElement[]> {
|
|
19
19
|
return page.evaluate(() => {
|
|
20
|
+
// esbuild injects __name() for named arrow-function constants (e.g. `const priority = ...`).
|
|
21
|
+
// Polyfill prevents ReferenceError when the serialized callback runs in the browser.
|
|
22
|
+
if (typeof (globalThis as any).__name === 'undefined') (globalThis as any).__name = (fn: any) => fn;
|
|
20
23
|
const out: { role: string; label: string; selectorHint: string; expectedData?: string }[] = [];
|
|
21
24
|
const seen = new Set<string>();
|
|
22
25
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Worker detection — inspects SW registrations, cache storage, background
|
|
3
|
+
* sync queues, and PWA installability for a crawled page.
|
|
4
|
+
* Surfaces offline-first contracts and push-capable surfaces that HTTP-only
|
|
5
|
+
* crawlers cannot observe.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Page } from 'playwright';
|
|
9
|
+
|
|
10
|
+
export interface SwRegistration {
|
|
11
|
+
scope: string;
|
|
12
|
+
scriptURL: string | null;
|
|
13
|
+
state: 'active' | 'installing' | 'waiting' | 'none';
|
|
14
|
+
updateViaCache: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SwCacheEntry {
|
|
18
|
+
cacheName: string;
|
|
19
|
+
entryCount: number;
|
|
20
|
+
sampleUrls: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BackgroundSyncInfo {
|
|
24
|
+
scope: string;
|
|
25
|
+
pendingTags: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PwaInfo {
|
|
29
|
+
isInstallable: boolean;
|
|
30
|
+
manifestUrl: string | null;
|
|
31
|
+
hasPushPermission: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ServiceWorkerResult {
|
|
35
|
+
supported: boolean;
|
|
36
|
+
active: boolean;
|
|
37
|
+
registrations: SwRegistration[];
|
|
38
|
+
cacheStorages: SwCacheEntry[];
|
|
39
|
+
backgroundSync: BackgroundSyncInfo[];
|
|
40
|
+
pwa: PwaInfo;
|
|
41
|
+
totalCachedEntries: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EMPTY_RESULT: ServiceWorkerResult = {
|
|
45
|
+
supported: false,
|
|
46
|
+
active: false,
|
|
47
|
+
registrations: [],
|
|
48
|
+
cacheStorages: [],
|
|
49
|
+
backgroundSync: [],
|
|
50
|
+
pwa: { isInstallable: false, manifestUrl: null, hasPushPermission: false },
|
|
51
|
+
totalCachedEntries: 0,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
async function detectServiceWorkers(
|
|
55
|
+
page: any,
|
|
56
|
+
): Promise<{ supported: boolean; active: boolean; registrations: SwRegistration[] }> {
|
|
57
|
+
const result = await page.evaluate(async () => {
|
|
58
|
+
if (!('serviceWorker' in navigator)) return { supported: false, active: false, registrations: [] };
|
|
59
|
+
try {
|
|
60
|
+
const controller = navigator.serviceWorker.controller;
|
|
61
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
62
|
+
return {
|
|
63
|
+
supported: true,
|
|
64
|
+
active: !!controller,
|
|
65
|
+
registrations: regs.map(r => ({
|
|
66
|
+
scope: r.scope,
|
|
67
|
+
scriptURL: r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL || null,
|
|
68
|
+
state: r.active ? 'active' : r.installing ? 'installing' : r.waiting ? 'waiting' : 'none',
|
|
69
|
+
updateViaCache: r.updateViaCache,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return { supported: true, active: false, registrations: [] };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return result as { supported: boolean; active: boolean; registrations: SwRegistration[] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function enumerateCacheStorage(page: any): Promise<SwCacheEntry[]> {
|
|
80
|
+
const caches = await page.evaluate(async () => {
|
|
81
|
+
if (!('caches' in window)) return [];
|
|
82
|
+
try {
|
|
83
|
+
const names = await window.caches.keys();
|
|
84
|
+
const result = [];
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
try {
|
|
87
|
+
const cache = await window.caches.open(name);
|
|
88
|
+
const keys = await cache.keys();
|
|
89
|
+
result.push({
|
|
90
|
+
cacheName: name,
|
|
91
|
+
entryCount: keys.length,
|
|
92
|
+
sampleUrls: keys.slice(0, 15).map(k => {
|
|
93
|
+
try { return new URL(k.url).pathname; } catch { return k.url.slice(0, 80); }
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
} catch { /* skip inaccessible cache */ }
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return caches as SwCacheEntry[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function detectBackgroundSync(page: any): Promise<BackgroundSyncInfo[]> {
|
|
107
|
+
const syncInfo = await page.evaluate(async () => {
|
|
108
|
+
if (!('serviceWorker' in navigator)) return [];
|
|
109
|
+
try {
|
|
110
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
111
|
+
const result = [];
|
|
112
|
+
for (const reg of regs) {
|
|
113
|
+
if ('sync' in reg) {
|
|
114
|
+
try {
|
|
115
|
+
const tags = await (reg as any).sync.getTags();
|
|
116
|
+
result.push({ scope: reg.scope, pendingTags: tags || [] });
|
|
117
|
+
} catch {
|
|
118
|
+
result.push({ scope: reg.scope, pendingTags: [] });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return syncInfo as BackgroundSyncInfo[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function detectPwa(page: any): Promise<PwaInfo> {
|
|
131
|
+
const pwa = await page.evaluate(() => {
|
|
132
|
+
const manifestEl = document.querySelector('link[rel="manifest"]');
|
|
133
|
+
const manifestUrl = manifestEl?.getAttribute('href') || null;
|
|
134
|
+
const hasSW = 'serviceWorker' in navigator && !!navigator.serviceWorker.controller;
|
|
135
|
+
let hasPushPermission = false;
|
|
136
|
+
try { hasPushPermission = Notification.permission === 'granted'; } catch { /* ignore */ }
|
|
137
|
+
return {
|
|
138
|
+
isInstallable: !!(manifestUrl && hasSW),
|
|
139
|
+
manifestUrl,
|
|
140
|
+
hasPushPermission,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
return pwa as PwaInfo;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function inspectServiceWorker(page: any): Promise<ServiceWorkerResult> {
|
|
147
|
+
try {
|
|
148
|
+
// Allow SW on HTTPS or localhost (http://localhost and http://127.0.0.1 support SW)
|
|
149
|
+
const url = page.url();
|
|
150
|
+
const isSecureContext = url.startsWith('https://') ||
|
|
151
|
+
/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?/.test(url);
|
|
152
|
+
if (!isSecureContext) return { ...EMPTY_RESULT };
|
|
153
|
+
|
|
154
|
+
const [swResult, cacheResult, syncResult, pwaResult] = await Promise.allSettled([
|
|
155
|
+
detectServiceWorkers(page),
|
|
156
|
+
enumerateCacheStorage(page),
|
|
157
|
+
detectBackgroundSync(page),
|
|
158
|
+
detectPwa(page),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const sw = swResult.status === 'fulfilled'
|
|
162
|
+
? swResult.value
|
|
163
|
+
: { supported: false, active: false, registrations: [] };
|
|
164
|
+
|
|
165
|
+
const cacheStorages = cacheResult.status === 'fulfilled' ? cacheResult.value : [];
|
|
166
|
+
const backgroundSync = syncResult.status === 'fulfilled' ? syncResult.value : [];
|
|
167
|
+
const pwa = pwaResult.status === 'fulfilled'
|
|
168
|
+
? pwaResult.value
|
|
169
|
+
: EMPTY_RESULT.pwa;
|
|
170
|
+
|
|
171
|
+
const totalCachedEntries = cacheStorages.reduce((sum, c) => sum + c.entryCount, 0);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
supported: sw.supported,
|
|
175
|
+
active: sw.active,
|
|
176
|
+
registrations: sw.registrations,
|
|
177
|
+
cacheStorages,
|
|
178
|
+
backgroundSync,
|
|
179
|
+
pwa,
|
|
180
|
+
totalCachedEntries,
|
|
181
|
+
};
|
|
182
|
+
} catch {
|
|
183
|
+
return { ...EMPTY_RESULT };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/session-capture.ts
CHANGED
|
@@ -207,6 +207,7 @@ export async function validateAndCaptureSession(
|
|
|
207
207
|
},
|
|
208
208
|
llmConfig?: { modelName: string; apiKey: string },
|
|
209
209
|
captchaOpts?: { apiKey?: string; provider?: '2captcha' | 'capsolver' },
|
|
210
|
+
loginInstructions?: string,
|
|
210
211
|
): Promise<{ storageState: string; cookies: number; localStorage: number; finalUrl?: string }> {
|
|
211
212
|
const browser = await chromium.launch({ headless: true });
|
|
212
213
|
try {
|
|
@@ -257,6 +258,8 @@ export async function validateAndCaptureSession(
|
|
|
257
258
|
{ username: creds.username, password: creds.password, getOneTimeCode },
|
|
258
259
|
appUrl,
|
|
259
260
|
captchaOpts,
|
|
261
|
+
undefined,
|
|
262
|
+
loginInstructions,
|
|
260
263
|
);
|
|
261
264
|
if (shLoggedIn) {
|
|
262
265
|
const shCtx = (sh as any).context ?? (sh as any).browserContext;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM crawler — enumerate shadow hosts and detect web component libraries.
|
|
3
|
+
*
|
|
4
|
+
* Walks the live DOM recursively (up to depth 8) to find all open shadow roots
|
|
5
|
+
* and custom elements with closed shadow roots, then classifies the component
|
|
6
|
+
* framework in use. Results feed ZeTa's test generator so it can account for
|
|
7
|
+
* elements that are invisible to standard Playwright locators.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Page } from 'playwright';
|
|
11
|
+
|
|
12
|
+
export interface ShadowInteractiveEl {
|
|
13
|
+
role: string;
|
|
14
|
+
label: string;
|
|
15
|
+
type: string;
|
|
16
|
+
tagName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ShadowHostInfo {
|
|
20
|
+
tagName: string;
|
|
21
|
+
hostId: string;
|
|
22
|
+
shadowMode: 'open' | 'closed';
|
|
23
|
+
depth: number;
|
|
24
|
+
interactiveCount: number;
|
|
25
|
+
interactiveElements: ShadowInteractiveEl[];
|
|
26
|
+
slotNames: string[];
|
|
27
|
+
childShadowHosts: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ShadowDomCrawlResult {
|
|
31
|
+
shadowHostCount: number;
|
|
32
|
+
openShadowCount: number;
|
|
33
|
+
closedShadowCount: number;
|
|
34
|
+
maxNestingDepth: number;
|
|
35
|
+
hosts: ShadowHostInfo[];
|
|
36
|
+
totalInteractiveInShadow: number;
|
|
37
|
+
detectedLibraries: string[];
|
|
38
|
+
blindSpots: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RawShadowHost {
|
|
42
|
+
tagName: string;
|
|
43
|
+
hostId: string;
|
|
44
|
+
shadowMode: 'open' | 'closed';
|
|
45
|
+
depth: number;
|
|
46
|
+
interactiveCount: number;
|
|
47
|
+
interactiveDetails: Array<{ role: string; label: string; type: string; tagName: string }>;
|
|
48
|
+
slotNames: string[];
|
|
49
|
+
childShadowHosts: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function enumerateShadowHosts(page: any): Promise<ShadowHostInfo[]> {
|
|
53
|
+
const raw: RawShadowHost[] = await page.evaluate(() => {
|
|
54
|
+
// esbuild injects __name() for named function declarations; polyfill prevents ReferenceError in browser.
|
|
55
|
+
if (typeof (globalThis as any).__name === 'undefined') (globalThis as any).__name = (fn: any) => fn;
|
|
56
|
+
function collectHosts(root: Document | ShadowRoot, depth: number): RawShadowHost[] {
|
|
57
|
+
if (depth > 8) return [];
|
|
58
|
+
const hosts: RawShadowHost[] = [];
|
|
59
|
+
const els = root.querySelectorAll('*');
|
|
60
|
+
for (const el of Array.from(els)) {
|
|
61
|
+
const htmlEl = el as HTMLElement & { shadowRoot: ShadowRoot | null };
|
|
62
|
+
if (htmlEl.shadowRoot) {
|
|
63
|
+
const interactive = htmlEl.shadowRoot.querySelectorAll(
|
|
64
|
+
'button,input,select,textarea,a[href],[role="button"],[role="link"],[role="checkbox"],[role="menuitem"],[role="tab"],[role="switch"]'
|
|
65
|
+
);
|
|
66
|
+
const slots = Array.from(htmlEl.shadowRoot.querySelectorAll('slot')).map(
|
|
67
|
+
(s) => (s as HTMLSlotElement).name || 'default'
|
|
68
|
+
);
|
|
69
|
+
const nestedHosts = Array.from(htmlEl.shadowRoot.querySelectorAll('*')).filter(
|
|
70
|
+
(c) => (c as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot
|
|
71
|
+
).length;
|
|
72
|
+
hosts.push({
|
|
73
|
+
tagName: htmlEl.tagName.toLowerCase(),
|
|
74
|
+
hostId:
|
|
75
|
+
htmlEl.id ||
|
|
76
|
+
(htmlEl.className?.toString()?.split(' ')[0]) ||
|
|
77
|
+
'',
|
|
78
|
+
shadowMode: 'open',
|
|
79
|
+
depth,
|
|
80
|
+
interactiveCount: interactive.length,
|
|
81
|
+
interactiveDetails: Array.from(interactive)
|
|
82
|
+
.slice(0, 8)
|
|
83
|
+
.map((i) => {
|
|
84
|
+
const iEl = i as HTMLElement;
|
|
85
|
+
return {
|
|
86
|
+
role: iEl.getAttribute('role') || iEl.tagName.toLowerCase(),
|
|
87
|
+
label: (
|
|
88
|
+
iEl.getAttribute('aria-label') ||
|
|
89
|
+
iEl.getAttribute('placeholder') ||
|
|
90
|
+
iEl.textContent?.trim() ||
|
|
91
|
+
''
|
|
92
|
+
).slice(0, 40),
|
|
93
|
+
type: iEl.getAttribute('type') || '',
|
|
94
|
+
tagName: iEl.tagName.toLowerCase(),
|
|
95
|
+
};
|
|
96
|
+
}),
|
|
97
|
+
slotNames: slots,
|
|
98
|
+
childShadowHosts: nestedHosts,
|
|
99
|
+
});
|
|
100
|
+
const nested = collectHosts(htmlEl.shadowRoot, depth + 1);
|
|
101
|
+
hosts.push(...nested);
|
|
102
|
+
} else if (el.tagName.includes('-') && !(el as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot) {
|
|
103
|
+
hosts.push({
|
|
104
|
+
tagName: el.tagName.toLowerCase(),
|
|
105
|
+
hostId: (el as HTMLElement).id || '',
|
|
106
|
+
shadowMode: 'closed',
|
|
107
|
+
depth,
|
|
108
|
+
interactiveCount: 0,
|
|
109
|
+
interactiveDetails: [],
|
|
110
|
+
slotNames: [],
|
|
111
|
+
childShadowHosts: 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return hosts;
|
|
116
|
+
}
|
|
117
|
+
return collectHosts(document, 0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return raw.slice(0, 100).map((h) => ({
|
|
121
|
+
tagName: h.tagName,
|
|
122
|
+
hostId: h.hostId,
|
|
123
|
+
shadowMode: h.shadowMode,
|
|
124
|
+
depth: h.depth,
|
|
125
|
+
interactiveCount: h.interactiveCount,
|
|
126
|
+
interactiveElements: h.interactiveDetails,
|
|
127
|
+
slotNames: h.slotNames,
|
|
128
|
+
childShadowHosts: h.childShadowHosts,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function detectWebComponentLibrary(page: any): Promise<string[]> {
|
|
133
|
+
return page.evaluate(() => {
|
|
134
|
+
const checks: [string, string][] = [
|
|
135
|
+
['sl-button', 'Shoelace'],
|
|
136
|
+
['fast-button', 'Microsoft FAST'],
|
|
137
|
+
['mwc-button', 'Material Web Components'],
|
|
138
|
+
['ion-button', 'Ionic'],
|
|
139
|
+
['vaadin-button', 'Vaadin'],
|
|
140
|
+
['fluent-button', 'Fluent UI Web Components'],
|
|
141
|
+
['hy-icon', 'Hydra'],
|
|
142
|
+
];
|
|
143
|
+
const detected: string[] = [];
|
|
144
|
+
for (const [tag, name] of checks) {
|
|
145
|
+
if (customElements.get(tag)) detected.push(name);
|
|
146
|
+
}
|
|
147
|
+
const allTags = Array.from(document.querySelectorAll('*'))
|
|
148
|
+
.map((e) => e.tagName.toLowerCase())
|
|
149
|
+
.filter((t) => t.includes('-'));
|
|
150
|
+
const uniqueTags = Array.from(new Set(allTags));
|
|
151
|
+
if (uniqueTags.some((t) => t.startsWith('sl-')) && !detected.includes('Shoelace'))
|
|
152
|
+
detected.push('Shoelace');
|
|
153
|
+
if (uniqueTags.some((t) => t.startsWith('fast-')) && !detected.includes('Microsoft FAST'))
|
|
154
|
+
detected.push('Microsoft FAST');
|
|
155
|
+
if (uniqueTags.some((t) => t.startsWith('ion-')) && !detected.includes('Ionic'))
|
|
156
|
+
detected.push('Ionic');
|
|
157
|
+
return detected;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function crawlShadowDom(page: any): Promise<ShadowDomCrawlResult> {
|
|
162
|
+
const empty: ShadowDomCrawlResult = {
|
|
163
|
+
shadowHostCount: 0,
|
|
164
|
+
openShadowCount: 0,
|
|
165
|
+
closedShadowCount: 0,
|
|
166
|
+
maxNestingDepth: 0,
|
|
167
|
+
hosts: [],
|
|
168
|
+
totalInteractiveInShadow: 0,
|
|
169
|
+
detectedLibraries: [],
|
|
170
|
+
blindSpots: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const [hosts, detectedLibraries] = await Promise.all([
|
|
175
|
+
enumerateShadowHosts(page),
|
|
176
|
+
detectWebComponentLibrary(page),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const openHosts = hosts.filter((h) => h.shadowMode === 'open');
|
|
180
|
+
const closedHosts = hosts.filter((h) => h.shadowMode === 'closed');
|
|
181
|
+
const maxNestingDepth = hosts.reduce((max, h) => Math.max(max, h.depth), 0);
|
|
182
|
+
const totalInteractiveInShadow = openHosts.reduce((sum, h) => sum + h.interactiveCount, 0);
|
|
183
|
+
const blindSpots = [...new Set(closedHosts.map((h) => h.tagName))];
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
shadowHostCount: hosts.length,
|
|
187
|
+
openShadowCount: openHosts.length,
|
|
188
|
+
closedShadowCount: closedHosts.length,
|
|
189
|
+
maxNestingDepth,
|
|
190
|
+
hosts,
|
|
191
|
+
totalInteractiveInShadow,
|
|
192
|
+
detectedLibraries,
|
|
193
|
+
blindSpots,
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return empty;
|
|
197
|
+
}
|
|
198
|
+
}
|