@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
|
@@ -166,10 +166,22 @@ function elementToPlaywrightLocator(element) {
|
|
|
166
166
|
}
|
|
167
167
|
return null;
|
|
168
168
|
}
|
|
169
|
+
const META_ANNOTATION_RE = /^(css animations|scroll effects detected|design tokens?\s*:|font imports?:|contrast ratio|aria-label audit|landmark|wcag|color contrast|background.*foreground.*card|--[\w-]+,\s*--)/i;
|
|
170
|
+
const AUTH_WALL_TEXT_RE = /sign\s*in|log\s*in|platform access required|please sign in|access required|authentication required|session expired|unauthorized/i;
|
|
171
|
+
function isMetaAnnotation(el) {
|
|
172
|
+
const t = (el.meaning ?? el.text ?? "").trim();
|
|
173
|
+
if (!t) return false;
|
|
174
|
+
if (META_ANNOTATION_RE.test(t)) return true;
|
|
175
|
+
const interactiveRoles = /* @__PURE__ */ new Set(["button", "link", "textbox", "combobox", "checkbox", "radio", "tab", "option", "menuitem"]);
|
|
176
|
+
if (t.length > 120 && !interactiveRoles.has((el.role ?? "").toLowerCase())) return true;
|
|
177
|
+
if ((t.match(/--[\w-]+/g) ?? []).length > 3) return true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
169
180
|
function elementsToPlaywrightLocators(elements) {
|
|
170
181
|
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
|
|
171
182
|
const byKey = /* @__PURE__ */ new Map();
|
|
172
183
|
for (const el of elements) {
|
|
184
|
+
if (isMetaAnnotation(el)) continue;
|
|
173
185
|
const loc = elementToPlaywrightLocator(el);
|
|
174
186
|
if (!loc) continue;
|
|
175
187
|
const existing = byKey.get(loc.key);
|
|
@@ -179,6 +191,16 @@ function elementsToPlaywrightLocators(elements) {
|
|
|
179
191
|
}
|
|
180
192
|
return Array.from(byKey.values());
|
|
181
193
|
}
|
|
194
|
+
function detectAuthWallLocators(locators) {
|
|
195
|
+
if (locators.length === 0) return null;
|
|
196
|
+
const lowCount = locators.filter((l) => l.confidence === "low").length;
|
|
197
|
+
const hasAuthText = locators.some((l) => AUTH_WALL_TEXT_RE.test(l.description));
|
|
198
|
+
const highCount = locators.filter((l) => l.confidence === "high").length;
|
|
199
|
+
if (hasAuthText && highCount === 0 && lowCount / locators.length > 0.5) {
|
|
200
|
+
return "These locators appear to be from a login/access wall, not the target page. The page requires authentication. Add credentials in Project Settings and re-crawl to get accurate locators for the protected content.";
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
182
204
|
function formatLocatorsForPrompt(locators) {
|
|
183
205
|
if (locators.length === 0) return "";
|
|
184
206
|
const lines = locators.map(
|
|
@@ -190,6 +212,7 @@ function formatLocatorsForPrompt(locators) {
|
|
|
190
212
|
].join("\n");
|
|
191
213
|
}
|
|
192
214
|
export {
|
|
215
|
+
detectAuthWallLocators,
|
|
193
216
|
elementToPlaywrightLocator,
|
|
194
217
|
elementsToPlaywrightLocators,
|
|
195
218
|
formatLocatorsForPrompt
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/locator-extractor.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.\n * Priority: data-testid \u2192 aria-label/placeholder (inputs) \u2192 role+name (buttons/links) \u2192 heading \u2192 text \u2192 id\n */\n\nexport interface PlaywrightLocator {\n key: string; // camelCase property name for page object\n expression: string; // e.g. \"page.getByRole('button', { name: 'Submit' })\"\n elementType: string; // button | input | link | heading | text | element\n confidence: 'high' | 'medium' | 'low';\n source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';\n description: string; // human-readable label\n}\n\n/** Parse the notes JSON string stored on Element records. */\nfunction parseNotes(notes: unknown): Record<string, string> {\n if (!notes) return {};\n try {\n const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;\n if (parsed && typeof parsed === 'object') return parsed as Record<string, string>;\n } catch {}\n return {};\n}\n\n/** Map crawler role strings to Playwright role values. */\nfunction mapRole(role?: string | null): string | null {\n if (!role) return null;\n const r = role.toLowerCase().trim();\n if (r === 'button' || r === 'submit' || r === 'reset') return 'button';\n if (r === 'link' || r === 'a' || r === 'anchor') return 'link';\n if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search') return 'textbox';\n if (r === 'combobox' || r === 'select' || r === 'dropdown') return 'combobox';\n if (r === 'checkbox') return 'checkbox';\n if (r === 'radio') return 'radio';\n if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6') return 'heading';\n if (r === 'img' || r === 'image') return 'img';\n if (r === 'nav' || r === 'navigation') return 'navigation';\n return r;\n}\n\n/** Convert \"Submit Form Button\" \u2192 \"submitFormButton\" */\nfunction toKey(text: string): string {\n return text\n .replace(/[^a-zA-Z0-9\\s]/g, ' ')\n .replace(/\\s+(.)/g, (_, c) => c.toUpperCase())\n .replace(/^(.)/, (_, c) => c.toLowerCase())\n .replace(/\\s/g, '')\n .slice(0, 40)\n || 'element';\n}\n\n/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */\nexport function elementToPlaywrightLocator(element: {\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n ariaState?: Record<string, unknown> | null;\n}): PlaywrightLocator | null {\n const attrs = parseNotes(element.notes);\n const testId = attrs['data-testid'];\n const ariaLabel = attrs['aria-label'] || element.meaning;\n const id = attrs['id'];\n const placeholder = attrs['placeholder'];\n const name = attrs['name'];\n const text = (element.text ?? '').trim();\n const meaning = (element.meaning ?? '').trim();\n const role = mapRole(element.role);\n const ariaState: Record<string, string> = {};\n if (element.ariaState && typeof element.ariaState === 'object') {\n for (const [k, v] of Object.entries(element.ariaState)) {\n if (typeof v === 'string') ariaState[k] = v;\n }\n }\n\n const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';\n const key = toKey(label);\n\n // Priority 1: data-testid\n if (testId) {\n return {\n key,\n expression: `page.getByTestId(${JSON.stringify(testId)})`,\n elementType: role ?? 'element',\n confidence: 'high',\n source: 'testid',\n description: meaning || testId,\n };\n }\n\n // Priority 2: input/combobox/textarea with label \u2192 getByLabel; with placeholder \u2192 getByPlaceholder\n if (role === 'textbox' || role === 'combobox') {\n if (ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n if (placeholder) {\n return {\n key,\n expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'placeholder',\n description: placeholder,\n };\n }\n }\n\n // Priority 3: checkbox/radio \u2192 getByRole with optional checked state from ariaState\n if ((role === 'checkbox' || role === 'radio') && meaning) {\n const isChecked = ariaState['checked'];\n const stateOpt = isChecked === 'true' ? `, { name: ${JSON.stringify(meaning)}, checked: true }` : isChecked === 'false' ? `, { name: ${JSON.stringify(meaning)}, checked: false }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role,\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n if ((role === 'checkbox' || role === 'radio') && ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: role,\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n\n // Priority 3b: tab/option/treeitem \u2192 getByRole with optional selected state\n const tabLikeRoles = ['tab', 'option', 'treeitem', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'gridcell'];\n if (tabLikeRoles.includes(role ?? '') && meaning) {\n const isSelected = ariaState['selected'];\n const isCurrent = ariaState['current'];\n const stateOpt = isSelected === 'true' ? `, { name: ${JSON.stringify(meaning)}, selected: true }` : isCurrent ? `, { name: ${JSON.stringify(meaning)} }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role ?? 'element',\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n\n // Priority 4: button/link with meaningful name \u2192 getByRole; include pressed state for toggles\n if ((role === 'button' || role === 'link') && meaning) {\n const isPressed = ariaState['pressed'];\n const stateOpt = isPressed === 'true' ? `, { name: ${JSON.stringify(meaning)}, pressed: true }` : isPressed === 'false' ? `, { name: ${JSON.stringify(meaning)}, pressed: false }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role,\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n\n // Priority 5: heading \u2192 getByRole heading\n if (role === 'heading' && (meaning || text)) {\n const name2 = meaning || text;\n return {\n key,\n expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,\n elementType: 'heading',\n confidence: 'medium',\n source: 'heading',\n description: name2,\n };\n }\n\n // Priority 6: existing CSS selector from crawler\n if (element.selector && element.selector.length > 0) {\n return {\n key,\n expression: `page.locator(${JSON.stringify(element.selector)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || text || element.selector,\n };\n }\n\n // Priority 7: id from attrs\n if (id) {\n return {\n key,\n expression: `page.locator(${JSON.stringify('#' + id)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || id,\n };\n }\n\n // Priority 8: fallback to text content\n if (meaning || text) {\n const t = meaning || text;\n return {\n key,\n expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,\n elementType: role ?? 'text',\n confidence: 'low',\n source: 'text',\n description: t,\n };\n }\n\n return null;\n}\n\n/**\n * Convert an array of DB Element records to deduplicated Playwright locators.\n * Deduplicates by key \u2014 higher confidence wins.\n */\nexport function elementsToPlaywrightLocators(elements: Array<{\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n isInteractive?: boolean | null;\n ariaState?: Record<string, unknown> | null;\n}>): PlaywrightLocator[] {\n const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };\n const byKey = new Map<string, PlaywrightLocator>();\n\n for (const el of elements) {\n const loc = elementToPlaywrightLocator(el);\n if (!loc) continue;\n const existing = byKey.get(loc.key);\n if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {\n byKey.set(loc.key, loc);\n }\n }\n\n return Array.from(byKey.values());\n}\n\n/**\n * Format locators as a block for injection into LLM script generation prompts.\n * Returns empty string if no locators.\n */\nexport function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string {\n if (locators.length === 0) return '';\n const lines = locators.map(l =>\n ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`\n );\n return [\n 'EXTRACTED PAGE LOCATORS (use these EXACTLY \u2014 do not invent other selectors; these were verified against the live page):',\n ...lines,\n ].join('\\n');\n}\n"],
|
|
5
|
-
"mappings": "AAeA,SAAS,WAAW,OAAwC;AAC1D,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI;AACF,UAAM,SAAS,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;AAC/D,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AAAA,EACnD,QAAQ;AAAA,EAAC;AACT,SAAO,CAAC;AACV;AAGA,SAAS,QAAQ,MAAqC;AACpD,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAI,KAAK,YAAY,EAAE,KAAK;AAClC,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,QAAS,QAAO;AAC9D,MAAI,MAAM,UAAU,MAAM,OAAO,MAAM,SAAU,QAAO;AACxD,MAAI,MAAM,aAAa,MAAM,WAAW,MAAM,UAAU,MAAM,WAAW,MAAM,cAAc,MAAM,SAAS,MAAM,YAAY,MAAM,SAAU,QAAO;AACrJ,MAAI,MAAM,cAAc,MAAM,YAAY,MAAM,WAAY,QAAO;AACnE,MAAI,MAAM,WAAY,QAAO;AAC7B,MAAI,MAAM,QAAS,QAAO;AAC1B,MAAI,MAAM,aAAa,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAM,QAAO;AAChH,MAAI,MAAM,SAAS,MAAM,QAAS,QAAO;AACzC,MAAI,MAAM,SAAS,MAAM,aAAc,QAAO;AAC9C,SAAO;AACT;AAGA,SAAS,MAAM,MAAsB;AACnC,SAAO,KACJ,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,WAAW,CAAC,GAAG,MAAM,EAAE,YAAY,CAAC,EAC5C,QAAQ,QAAQ,CAAC,GAAG,MAAM,EAAE,YAAY,CAAC,EACzC,QAAQ,OAAO,EAAE,EACjB,MAAM,GAAG,EAAE,KACT;AACP;AAGO,SAAS,2BAA2B,SAOd;AAC3B,QAAM,QAAQ,WAAW,QAAQ,KAAK;AACtC,QAAM,SAAS,MAAM,aAAa;AAClC,QAAM,YAAY,MAAM,YAAY,KAAK,QAAQ;AACjD,QAAM,KAAK,MAAM,IAAI;AACrB,QAAM,cAAc,MAAM,aAAa;AACvC,QAAM,OAAO,MAAM,MAAM;AACzB,QAAM,QAAQ,QAAQ,QAAQ,IAAI,KAAK;AACvC,QAAM,WAAW,QAAQ,WAAW,IAAI,KAAK;AAC7C,QAAM,OAAO,QAAQ,QAAQ,IAAI;AACjC,QAAM,YAAoC,CAAC;AAC3C,MAAI,QAAQ,aAAa,OAAO,QAAQ,cAAc,UAAU;AAC9D,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,SAAS,GAAG;AACtD,UAAI,OAAO,MAAM,SAAU,WAAU,CAAC,IAAI;AAAA,IAC5C;AAAA,EACF;AAEA,QAAM,QAAQ,WAAW,QAAQ,UAAU,aAAa,eAAe,MAAM,QAAQ;AACrF,QAAM,MAAM,MAAM,KAAK;AAGvB,MAAI,QAAQ;AACV,WAAO;AAAA,MACL;AAAA,MACA,YAAY,oBAAoB,KAAK,UAAU,MAAM,CAAC;AAAA,MACtD,aAAa,QAAQ;AAAA,MACrB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa,WAAW;AAAA,IAC1B;AAAA,EACF;AAGA,MAAI,SAAS,aAAa,SAAS,YAAY;AAC7C,QAAI,WAAW;AACb,aAAO;AAAA,QACL;AAAA,QACA,YAAY,mBAAmB,KAAK,UAAU,SAAS,CAAC;AAAA,QACxD,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,aAAa;AAAA,MACf;AAAA,IACF;AACA,QAAI,aAAa;AACf,aAAO;AAAA,QACL;AAAA,QACA,YAAY,yBAAyB,KAAK,UAAU,WAAW,CAAC;AAAA,QAChE,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,aAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAGA,OAAK,SAAS,cAAc,SAAS,YAAY,SAAS;AACxD,UAAM,YAAY,UAAU,SAAS;AACrC,UAAM,WAAW,cAAc,SAAS,aAAa,KAAK,UAAU,OAAO,CAAC,sBAAsB,cAAc,UAAU,aAAa,KAAK,UAAU,OAAO,CAAC,uBAAuB,aAAa,KAAK,UAAU,OAAO,CAAC;AACzN,WAAO;AAAA,MACL;AAAA,MACA,YAAY,kBAAkB,KAAK,UAAU,IAAI,CAAC,GAAG,QAAQ;AAAA,MAC7D,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AACA,OAAK,SAAS,cAAc,SAAS,YAAY,WAAW;AAC1D,WAAO;AAAA,MACL;AAAA,MACA,YAAY,mBAAmB,KAAK,UAAU,SAAS,CAAC;AAAA,MACxD,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AAGA,QAAM,eAAe,CAAC,OAAO,UAAU,YAAY,YAAY,oBAAoB,iBAAiB,UAAU;AAC9G,MAAI,aAAa,SAAS,QAAQ,EAAE,KAAK,SAAS;AAChD,UAAM,aAAa,UAAU,UAAU;AACvC,UAAM,YAAY,UAAU,SAAS;AACrC,UAAM,WAAW,eAAe,SAAS,aAAa,KAAK,UAAU,OAAO,CAAC,uBAAuB,YAAY,aAAa,KAAK,UAAU,OAAO,CAAC,OAAO,aAAa,KAAK,UAAU,OAAO,CAAC;AAC/L,WAAO;AAAA,MACL;AAAA,MACA,YAAY,kBAAkB,KAAK,UAAU,IAAI,CAAC,GAAG,QAAQ;AAAA,MAC7D,aAAa,QAAQ;AAAA,MACrB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AAGA,OAAK,SAAS,YAAY,SAAS,WAAW,SAAS;AACrD,UAAM,YAAY,UAAU,SAAS;AACrC,UAAM,WAAW,cAAc,SAAS,aAAa,KAAK,UAAU,OAAO,CAAC,sBAAsB,cAAc,UAAU,aAAa,KAAK,UAAU,OAAO,CAAC,uBAAuB,aAAa,KAAK,UAAU,OAAO,CAAC;AACzN,WAAO;AAAA,MACL;AAAA,MACA,YAAY,kBAAkB,KAAK,UAAU,IAAI,CAAC,GAAG,QAAQ;AAAA,MAC7D,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AAGA,MAAI,SAAS,cAAc,WAAW,OAAO;AAC3C,UAAM,QAAQ,WAAW;AACzB,WAAO;AAAA,MACL;AAAA,MACA,YAAY,qCAAqC,KAAK,UAAU,KAAK,CAAC;AAAA,MACtE,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AAGA,MAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,WAAO;AAAA,MACL;AAAA,MACA,YAAY,gBAAgB,KAAK,UAAU,QAAQ,QAAQ,CAAC;AAAA,MAC5D,aAAa,QAAQ;AAAA,MACrB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa,WAAW,QAAQ,QAAQ;AAAA,IAC1C;AAAA,EACF;AAGA,MAAI,IAAI;AACN,WAAO;AAAA,MACL;AAAA,MACA,YAAY,gBAAgB,KAAK,UAAU,MAAM,EAAE,CAAC;AAAA,MACpD,aAAa,QAAQ;AAAA,MACrB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa,WAAW;AAAA,IAC1B;AAAA,EACF;AAGA,MAAI,WAAW,MAAM;AACnB,UAAM,IAAI,WAAW;AACrB,WAAO;AAAA,MACL;AAAA,MACA,YAAY,kBAAkB,KAAK,UAAU,CAAC,CAAC;AAAA,MAC/C,aAAa,QAAQ;AAAA,MACrB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["/**\n * Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.\n * Priority: data-testid \u2192 aria-label/placeholder (inputs) \u2192 role+name (buttons/links) \u2192 heading \u2192 text \u2192 id\n */\n\nexport interface PlaywrightLocator {\n key: string; // camelCase property name for page object\n expression: string; // e.g. \"page.getByRole('button', { name: 'Submit' })\"\n elementType: string; // button | input | link | heading | text | element\n confidence: 'high' | 'medium' | 'low';\n source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';\n description: string; // human-readable label\n}\n\n/** Parse the notes JSON string stored on Element records. */\nfunction parseNotes(notes: unknown): Record<string, string> {\n if (!notes) return {};\n try {\n const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;\n if (parsed && typeof parsed === 'object') return parsed as Record<string, string>;\n } catch {}\n return {};\n}\n\n/** Map crawler role strings to Playwright role values. */\nfunction mapRole(role?: string | null): string | null {\n if (!role) return null;\n const r = role.toLowerCase().trim();\n if (r === 'button' || r === 'submit' || r === 'reset') return 'button';\n if (r === 'link' || r === 'a' || r === 'anchor') return 'link';\n if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search') return 'textbox';\n if (r === 'combobox' || r === 'select' || r === 'dropdown') return 'combobox';\n if (r === 'checkbox') return 'checkbox';\n if (r === 'radio') return 'radio';\n if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6') return 'heading';\n if (r === 'img' || r === 'image') return 'img';\n if (r === 'nav' || r === 'navigation') return 'navigation';\n return r;\n}\n\n/** Convert \"Submit Form Button\" \u2192 \"submitFormButton\" */\nfunction toKey(text: string): string {\n return text\n .replace(/[^a-zA-Z0-9\\s]/g, ' ')\n .replace(/\\s+(.)/g, (_, c) => c.toUpperCase())\n .replace(/^(.)/, (_, c) => c.toLowerCase())\n .replace(/\\s/g, '')\n .slice(0, 40)\n || 'element';\n}\n\n/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */\nexport function elementToPlaywrightLocator(element: {\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n ariaState?: Record<string, unknown> | null;\n}): PlaywrightLocator | null {\n const attrs = parseNotes(element.notes);\n const testId = attrs['data-testid'];\n const ariaLabel = attrs['aria-label'] || element.meaning;\n const id = attrs['id'];\n const placeholder = attrs['placeholder'];\n const name = attrs['name'];\n const text = (element.text ?? '').trim();\n const meaning = (element.meaning ?? '').trim();\n const role = mapRole(element.role);\n const ariaState: Record<string, string> = {};\n if (element.ariaState && typeof element.ariaState === 'object') {\n for (const [k, v] of Object.entries(element.ariaState)) {\n if (typeof v === 'string') ariaState[k] = v;\n }\n }\n\n const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';\n const key = toKey(label);\n\n // Priority 1: data-testid\n if (testId) {\n return {\n key,\n expression: `page.getByTestId(${JSON.stringify(testId)})`,\n elementType: role ?? 'element',\n confidence: 'high',\n source: 'testid',\n description: meaning || testId,\n };\n }\n\n // Priority 2: input/combobox/textarea with label \u2192 getByLabel; with placeholder \u2192 getByPlaceholder\n if (role === 'textbox' || role === 'combobox') {\n if (ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n if (placeholder) {\n return {\n key,\n expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'placeholder',\n description: placeholder,\n };\n }\n }\n\n // Priority 3: checkbox/radio \u2192 getByRole with optional checked state from ariaState\n if ((role === 'checkbox' || role === 'radio') && meaning) {\n const isChecked = ariaState['checked'];\n const stateOpt = isChecked === 'true' ? `, { name: ${JSON.stringify(meaning)}, checked: true }` : isChecked === 'false' ? `, { name: ${JSON.stringify(meaning)}, checked: false }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role,\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n if ((role === 'checkbox' || role === 'radio') && ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: role,\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n\n // Priority 3b: tab/option/treeitem \u2192 getByRole with optional selected state\n const tabLikeRoles = ['tab', 'option', 'treeitem', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'gridcell'];\n if (tabLikeRoles.includes(role ?? '') && meaning) {\n const isSelected = ariaState['selected'];\n const isCurrent = ariaState['current'];\n const stateOpt = isSelected === 'true' ? `, { name: ${JSON.stringify(meaning)}, selected: true }` : isCurrent ? `, { name: ${JSON.stringify(meaning)} }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role ?? 'element',\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n\n // Priority 4: button/link with meaningful name \u2192 getByRole; include pressed state for toggles\n if ((role === 'button' || role === 'link') && meaning) {\n const isPressed = ariaState['pressed'];\n const stateOpt = isPressed === 'true' ? `, { name: ${JSON.stringify(meaning)}, pressed: true }` : isPressed === 'false' ? `, { name: ${JSON.stringify(meaning)}, pressed: false }` : `, { name: ${JSON.stringify(meaning)} }`;\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}${stateOpt})`,\n elementType: role,\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n\n // Priority 5: heading \u2192 getByRole heading\n if (role === 'heading' && (meaning || text)) {\n const name2 = meaning || text;\n return {\n key,\n expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,\n elementType: 'heading',\n confidence: 'medium',\n source: 'heading',\n description: name2,\n };\n }\n\n // Priority 6: existing CSS selector from crawler\n if (element.selector && element.selector.length > 0) {\n return {\n key,\n expression: `page.locator(${JSON.stringify(element.selector)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || text || element.selector,\n };\n }\n\n // Priority 7: id from attrs\n if (id) {\n return {\n key,\n expression: `page.locator(${JSON.stringify('#' + id)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || id,\n };\n }\n\n // Priority 8: fallback to text content\n if (meaning || text) {\n const t = meaning || text;\n return {\n key,\n expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,\n elementType: role ?? 'text',\n confidence: 'low',\n source: 'text',\n description: t,\n };\n }\n\n return null;\n}\n\n// Patterns that indicate crawl-analysis meta-text stored as element records (not real UI elements).\n// These come from accessibility-auditor, screen-reader analysis, or CSS inspector annotations.\nconst META_ANNOTATION_RE = /^(css animations|scroll effects detected|design tokens?\\s*:|font imports?:|contrast ratio|aria-label audit|landmark|wcag|color contrast|background.*foreground.*card|--[\\w-]+,\\s*--)/i;\n\n// Auth-wall element patterns \u2014 if >50% of locators match these, the page was an auth wall.\nconst AUTH_WALL_TEXT_RE = /sign\\s*in|log\\s*in|platform access required|please sign in|access required|authentication required|session expired|unauthorized/i;\n\n/**\n * Returns true if an element's text content is crawl-analysis meta-text, not a real UI element.\n * These are annotations stored by the accessibility auditor, not actual DOM elements.\n */\nfunction isMetaAnnotation(el: { role?: string | null; meaning?: string | null; text?: string | null }): boolean {\n const t = (el.meaning ?? el.text ?? '').trim();\n if (!t) return false;\n // Filter crawl-analysis annotations\n if (META_ANNOTATION_RE.test(t)) return true;\n // Filter very long text strings with no interactive role \u2014 these are paragraph/body copy,\n // not useful UI locators. Threshold: 120 chars with no testId/label/role\n const interactiveRoles = new Set(['button', 'link', 'textbox', 'combobox', 'checkbox', 'radio', 'tab', 'option', 'menuitem']);\n if (t.length > 120 && !interactiveRoles.has((el.role ?? '').toLowerCase())) return true;\n // Filter CSS variable dumps (contain multiple --var-name patterns)\n if ((t.match(/--[\\w-]+/g) ?? []).length > 3) return true;\n return false;\n}\n\n/**\n * Convert an array of DB Element records to deduplicated Playwright locators.\n * Deduplicates by key \u2014 higher confidence wins.\n * Filters out crawl-analysis meta-annotations and produces an auth-wall warning when applicable.\n */\nexport function elementsToPlaywrightLocators(elements: Array<{\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n isInteractive?: boolean | null;\n ariaState?: Record<string, unknown> | null;\n}>): PlaywrightLocator[] {\n const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };\n const byKey = new Map<string, PlaywrightLocator>();\n\n for (const el of elements) {\n // Skip crawl-analysis meta-annotations \u2014 these are not real page elements\n if (isMetaAnnotation(el)) continue;\n\n const loc = elementToPlaywrightLocator(el);\n if (!loc) continue;\n const existing = byKey.get(loc.key);\n if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {\n byKey.set(loc.key, loc);\n }\n }\n\n return Array.from(byKey.values());\n}\n\n/**\n * Detect whether a set of locators was extracted from an auth-wall page.\n * Returns a warning message if auth-wall detected, null otherwise.\n * Heuristic: >50% of locators are low-confidence text matches AND at least one matches auth-wall pattern.\n */\nexport function detectAuthWallLocators(locators: PlaywrightLocator[]): string | null {\n if (locators.length === 0) return null;\n const lowCount = locators.filter(l => l.confidence === 'low').length;\n const hasAuthText = locators.some(l => AUTH_WALL_TEXT_RE.test(l.description));\n const highCount = locators.filter(l => l.confidence === 'high').length;\n if (hasAuthText && highCount === 0 && lowCount / locators.length > 0.5) {\n return 'These locators appear to be from a login/access wall, not the target page. The page requires authentication. Add credentials in Project Settings and re-crawl to get accurate locators for the protected content.';\n }\n return null;\n}\n\n/**\n * Format locators as a block for injection into LLM script generation prompts.\n * Returns empty string if no locators.\n */\nexport function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string {\n if (locators.length === 0) return '';\n const lines = locators.map(l =>\n ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`\n );\n return [\n 'EXTRACTED PAGE LOCATORS (use these EXACTLY \u2014 do not invent other selectors; these were verified against the live page):',\n ...lines,\n ].join('\\n');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
async function enumerateIframes(page, seedUrl) {
|
|
2
|
+
const records = [];
|
|
3
|
+
try {
|
|
4
|
+
const seedHost = new URL(seedUrl).hostname;
|
|
5
|
+
const iframeLocators = await page.locator("iframe").all();
|
|
6
|
+
const limited = iframeLocators.slice(0, 20);
|
|
7
|
+
for (let idx = 0; idx < limited.length; idx++) {
|
|
8
|
+
try {
|
|
9
|
+
const loc = limited[idx];
|
|
10
|
+
const src = await loc.getAttribute("src") ?? "";
|
|
11
|
+
const sandboxValue = await loc.getAttribute("sandbox");
|
|
12
|
+
const sandboxed = sandboxValue !== null;
|
|
13
|
+
let crossOrigin = false;
|
|
14
|
+
if (src) {
|
|
15
|
+
try {
|
|
16
|
+
const iframeHost = new URL(src, seedUrl).hostname;
|
|
17
|
+
crossOrigin = iframeHost !== seedHost;
|
|
18
|
+
} catch {
|
|
19
|
+
crossOrigin = false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let accessible = false;
|
|
23
|
+
let contentSample = "";
|
|
24
|
+
let hasForm = false;
|
|
25
|
+
try {
|
|
26
|
+
const fl = page.frameLocator("iframe").nth(idx);
|
|
27
|
+
const body = await fl.locator("body").textContent({ timeout: 2e3 });
|
|
28
|
+
if (body !== null) {
|
|
29
|
+
accessible = true;
|
|
30
|
+
contentSample = body.trim().slice(0, 300);
|
|
31
|
+
}
|
|
32
|
+
const formCount = await fl.locator("form, input, textarea, select").count();
|
|
33
|
+
hasForm = formCount > 0;
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
records.push({
|
|
37
|
+
src,
|
|
38
|
+
crossOrigin,
|
|
39
|
+
sandboxed,
|
|
40
|
+
sandboxValue,
|
|
41
|
+
accessible,
|
|
42
|
+
contentSample,
|
|
43
|
+
hasForm
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
return records;
|
|
51
|
+
}
|
|
52
|
+
async function detectPopups(page, result) {
|
|
53
|
+
try {
|
|
54
|
+
const candidates = [
|
|
55
|
+
...await page.locator('[onclick*="window.open"]').all(),
|
|
56
|
+
...await page.locator('button:has-text("Open in")').all()
|
|
57
|
+
];
|
|
58
|
+
const limited = candidates.slice(0, 3);
|
|
59
|
+
for (const trigger of limited) {
|
|
60
|
+
try {
|
|
61
|
+
const triggerLabel = (await trigger.textContent())?.trim() ?? "";
|
|
62
|
+
const outcome = await Promise.all([
|
|
63
|
+
page.waitForEvent("popup", { timeout: 2e3 }),
|
|
64
|
+
trigger.click()
|
|
65
|
+
]).catch(() => null);
|
|
66
|
+
if (!outcome) continue;
|
|
67
|
+
const popup = outcome[0];
|
|
68
|
+
if (!popup) continue;
|
|
69
|
+
const url = popup.url();
|
|
70
|
+
let isOAuthLike = false;
|
|
71
|
+
try {
|
|
72
|
+
const host = new URL(url).hostname;
|
|
73
|
+
isOAuthLike = /google|github|microsoft|okta|auth0/.test(host);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
result.popups.push({ url, triggerLabel, isOAuthLike });
|
|
77
|
+
await popup.close().catch(() => {
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function detectDownloads(page, result) {
|
|
86
|
+
try {
|
|
87
|
+
const candidates = [
|
|
88
|
+
...await page.locator("a[download]").all(),
|
|
89
|
+
...await page.locator('button:has-text("Export")').all(),
|
|
90
|
+
...await page.locator('button:has-text("Download")').all()
|
|
91
|
+
];
|
|
92
|
+
const limited = candidates.slice(0, 3);
|
|
93
|
+
for (const trigger of limited) {
|
|
94
|
+
try {
|
|
95
|
+
const triggerLabel = (await trigger.textContent())?.trim() ?? "";
|
|
96
|
+
const outcome = await Promise.all([
|
|
97
|
+
page.waitForEvent("download", { timeout: 2e3 }),
|
|
98
|
+
trigger.click()
|
|
99
|
+
]).catch(() => null);
|
|
100
|
+
if (!outcome) continue;
|
|
101
|
+
const download = outcome[0];
|
|
102
|
+
if (!download) continue;
|
|
103
|
+
const filename = download.suggestedFilename();
|
|
104
|
+
const downloadUrl = download.url();
|
|
105
|
+
await download.cancel().catch(() => {
|
|
106
|
+
});
|
|
107
|
+
result.downloads.push({ filename, downloadUrl, triggerLabel });
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function attachNewTabListener(context, seedUrl, result) {
|
|
115
|
+
let seedHost = "";
|
|
116
|
+
try {
|
|
117
|
+
seedHost = new URL(seedUrl).hostname;
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
const handler = async (newPage) => {
|
|
121
|
+
try {
|
|
122
|
+
await newPage.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
|
|
123
|
+
});
|
|
124
|
+
const url = newPage.url();
|
|
125
|
+
if (!url || url === "about:blank") {
|
|
126
|
+
await newPage.close().catch(() => {
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let isInternal = false;
|
|
131
|
+
let httpStatus;
|
|
132
|
+
try {
|
|
133
|
+
const tabHost = new URL(url).hostname;
|
|
134
|
+
isInternal = tabHost === seedHost;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
if (!isInternal) {
|
|
138
|
+
try {
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
141
|
+
const resp = await fetch(url, { method: "HEAD", signal: controller.signal });
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
httpStatus = resp.status;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
result.newTabs.push({ url, linkText: "", isInternal, httpStatus });
|
|
148
|
+
await newPage.close().catch(() => {
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
context.on("page", handler);
|
|
154
|
+
return () => context.off("page", handler);
|
|
155
|
+
}
|
|
156
|
+
async function probeMultiContext(page, context, seedUrl) {
|
|
157
|
+
void context;
|
|
158
|
+
const result = {
|
|
159
|
+
newTabs: [],
|
|
160
|
+
iframes: [],
|
|
161
|
+
popups: [],
|
|
162
|
+
downloads: []
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
result.iframes = await enumerateIframes(page, seedUrl);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await detectPopups(page, result);
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await detectDownloads(page, result);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
attachNewTabListener,
|
|
180
|
+
probeMultiContext
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=multi-context-handler.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/multi-context-handler.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Multi-context browser handling \u2014 detects and records new tabs, iframes,\n * popups, and file downloads triggered during a page crawl.\n * This lets ZeTa discover cross-origin flows, OAuth redirects, embedded\n * third-party content, and downloadable resources that single-page HTTP\n * crawlers would miss entirely.\n */\n\nimport type { Page, BrowserContext } from 'playwright';\n\nexport interface NewTabRecord {\n url: string;\n linkText: string;\n isInternal: boolean;\n httpStatus?: number;\n}\n\nexport interface IframeRecord {\n src: string;\n crossOrigin: boolean;\n sandboxed: boolean;\n sandboxValue: string | null;\n accessible: boolean;\n contentSample: string;\n hasForm: boolean;\n}\n\nexport interface PopupRecord {\n url: string;\n triggerLabel: string;\n isOAuthLike: boolean;\n}\n\nexport interface DownloadRecord {\n filename: string;\n downloadUrl: string;\n triggerLabel: string;\n}\n\nexport interface MultiContextResult {\n newTabs: NewTabRecord[];\n iframes: IframeRecord[];\n popups: PopupRecord[];\n downloads: DownloadRecord[];\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function enumerateIframes(\n page: Page,\n seedUrl: string,\n): Promise<IframeRecord[]> {\n const records: IframeRecord[] = [];\n try {\n const seedHost = new URL(seedUrl).hostname;\n const iframeLocators = await page.locator('iframe').all();\n const limited = iframeLocators.slice(0, 20);\n\n for (let idx = 0; idx < limited.length; idx++) {\n try {\n const loc = limited[idx];\n const src = (await loc.getAttribute('src')) ?? '';\n const sandboxValue = await loc.getAttribute('sandbox');\n const sandboxed = sandboxValue !== null;\n\n let crossOrigin = false;\n if (src) {\n try {\n const iframeHost = new URL(src, seedUrl).hostname;\n crossOrigin = iframeHost !== seedHost;\n } catch {\n crossOrigin = false;\n }\n }\n\n let accessible = false;\n let contentSample = '';\n let hasForm = false;\n\n try {\n const fl = page.frameLocator('iframe').nth(idx);\n const body = await fl.locator('body').textContent({ timeout: 2000 });\n if (body !== null) {\n accessible = true;\n contentSample = body.trim().slice(0, 300);\n }\n const formCount = await fl.locator('form, input, textarea, select').count();\n hasForm = formCount > 0;\n } catch {\n // cross-origin or sandboxed \u2014 not accessible from parent frame\n }\n\n records.push({\n src,\n crossOrigin,\n sandboxed,\n sandboxValue,\n accessible,\n contentSample,\n hasForm,\n });\n } catch {\n // skip individual iframe errors\n }\n }\n } catch {\n // skip entirely if locator fails\n }\n return records;\n}\n\nasync function detectPopups(page: Page, result: MultiContextResult): Promise<void> {\n try {\n const candidates = [\n ...await page.locator('[onclick*=\"window.open\"]').all(),\n ...await page.locator('button:has-text(\"Open in\")').all(),\n ];\n const limited = candidates.slice(0, 3);\n\n for (const trigger of limited) {\n try {\n const triggerLabel = (await trigger.textContent())?.trim() ?? '';\n const outcome = await Promise.all([\n page.waitForEvent('popup', { timeout: 2000 }),\n trigger.click(),\n ]).catch(() => null);\n\n if (!outcome) continue;\n const popup = outcome[0] as Page;\n if (!popup) continue;\n\n const url = popup.url();\n let isOAuthLike = false;\n try {\n const host = new URL(url).hostname;\n isOAuthLike = /google|github|microsoft|okta|auth0/.test(host);\n } catch {\n // not a valid URL\n }\n\n result.popups.push({ url, triggerLabel, isOAuthLike });\n await popup.close().catch(() => {});\n } catch {\n // skip this trigger\n }\n }\n } catch {\n // skip popup detection entirely\n }\n}\n\nasync function detectDownloads(page: Page, result: MultiContextResult): Promise<void> {\n try {\n const candidates = [\n ...await page.locator('a[download]').all(),\n ...await page.locator('button:has-text(\"Export\")').all(),\n ...await page.locator('button:has-text(\"Download\")').all(),\n ];\n const limited = candidates.slice(0, 3);\n\n for (const trigger of limited) {\n try {\n const triggerLabel = (await trigger.textContent())?.trim() ?? '';\n const outcome = await Promise.all([\n page.waitForEvent('download', { timeout: 2000 }),\n trigger.click(),\n ]).catch(() => null);\n\n if (!outcome) continue;\n const download = outcome[0] as unknown as import('playwright').Download;\n if (!download) continue;\n\n const filename = download.suggestedFilename();\n const downloadUrl = download.url();\n await download.cancel().catch(() => {});\n\n result.downloads.push({ filename, downloadUrl, triggerLabel });\n } catch {\n // skip this trigger\n }\n }\n } catch {\n // skip download detection entirely\n }\n}\n\n// ---------------------------------------------------------------------------\n// Exported functions\n// ---------------------------------------------------------------------------\n\n/**\n * Attaches a listener to the BrowserContext that records every new tab\n * opened while the listener is active. Returns an unsubscribe function.\n *\n * Call this before navigating / clicking so that tabs opened during the\n * crawl are captured, then call the returned function to stop listening.\n */\nexport function attachNewTabListener(\n context: BrowserContext,\n seedUrl: string,\n result: MultiContextResult,\n): () => void {\n let seedHost = '';\n try {\n seedHost = new URL(seedUrl).hostname;\n } catch {\n // leave empty \u2014 all tabs will be treated as external\n }\n\n const handler = async (newPage: Page) => {\n try {\n await newPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});\n const url = newPage.url();\n\n if (!url || url === 'about:blank') {\n await newPage.close().catch(() => {});\n return;\n }\n\n let isInternal = false;\n let httpStatus: number | undefined;\n\n try {\n const tabHost = new URL(url).hostname;\n isInternal = tabHost === seedHost;\n } catch {\n // non-parseable URL \u2014 treat as external\n }\n\n if (!isInternal) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 3000);\n const resp = await fetch(url, { method: 'HEAD', signal: controller.signal });\n clearTimeout(timer);\n httpStatus = resp.status;\n } catch {\n // network unreachable or CORS \u2014 leave httpStatus undefined\n }\n }\n\n // linkText is not available at context level; caller fills it if needed\n result.newTabs.push({ url, linkText: '', isInternal, httpStatus });\n await newPage.close().catch(() => {});\n } catch {\n // swallow handler errors so the listener stays alive\n }\n };\n\n context.on('page', handler);\n return () => context.off('page', handler);\n}\n\n/**\n * Probes the current page for multi-context browser events: iframes,\n * popup windows, and file downloads.\n *\n * New-tab tracking is intentionally excluded here \u2014 it must be set up at\n * the crawler level (before navigation) using `attachNewTabListener`.\n */\nexport async function probeMultiContext(\n page: Page,\n context: BrowserContext,\n seedUrl: string,\n): Promise<MultiContextResult> {\n void context; // reserved for future use (e.g. routing overrides)\n\n const result: MultiContextResult = {\n newTabs: [],\n iframes: [],\n popups: [],\n downloads: [],\n };\n\n try {\n result.iframes = await enumerateIframes(page, seedUrl);\n } catch {\n // keep empty\n }\n\n try {\n await detectPopups(page, result);\n } catch {\n // keep empty\n }\n\n try {\n await detectDownloads(page, result);\n } catch {\n // keep empty\n }\n\n return result;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
function generateContextId(type, url) {
|
|
2
|
+
let h = 2166136261;
|
|
3
|
+
const s = type + url;
|
|
4
|
+
for (let i = 0; i < s.length; i++) {
|
|
5
|
+
h ^= s.charCodeAt(i);
|
|
6
|
+
h = h * 16777619 >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return h.toString(16).padStart(8, "0").slice(0, 12);
|
|
9
|
+
}
|
|
10
|
+
function computeFrameDepth(frame) {
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let current = frame.parentFrame();
|
|
13
|
+
while (current !== null) {
|
|
14
|
+
depth++;
|
|
15
|
+
current = current.parentFrame();
|
|
16
|
+
}
|
|
17
|
+
return depth;
|
|
18
|
+
}
|
|
19
|
+
async function crawlFrameTree(page, maxDepth = 4) {
|
|
20
|
+
const nodes = [];
|
|
21
|
+
try {
|
|
22
|
+
const frames = page.frames();
|
|
23
|
+
const limited = frames.slice(0, 200);
|
|
24
|
+
for (const frame of limited) {
|
|
25
|
+
try {
|
|
26
|
+
const url = frame.url();
|
|
27
|
+
if (!url || url === "about:blank" || url.startsWith("javascript:") || url.startsWith("data:")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const depth = computeFrameDepth(frame);
|
|
31
|
+
if (depth > maxDepth) continue;
|
|
32
|
+
const parentFrame = frame.parentFrame();
|
|
33
|
+
const parentId = parentFrame ? generateContextId("frame", parentFrame.url()) : null;
|
|
34
|
+
let title = "";
|
|
35
|
+
let interactiveCount = 0;
|
|
36
|
+
let accessible = false;
|
|
37
|
+
try {
|
|
38
|
+
title = await frame.title();
|
|
39
|
+
interactiveCount = await frame.locator("button, a[href], input, select").count();
|
|
40
|
+
accessible = true;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
nodes.push({
|
|
44
|
+
id: generateContextId("frame", url),
|
|
45
|
+
type: "frame",
|
|
46
|
+
url,
|
|
47
|
+
parentId,
|
|
48
|
+
depth,
|
|
49
|
+
title,
|
|
50
|
+
interactiveCount,
|
|
51
|
+
accessible,
|
|
52
|
+
isEmbeddedApp: false
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
return nodes;
|
|
60
|
+
}
|
|
61
|
+
async function detectEmbeddedApps(page) {
|
|
62
|
+
try {
|
|
63
|
+
const srcs = await page.evaluate(() => {
|
|
64
|
+
return Array.from(document.querySelectorAll("iframe")).filter((f) => {
|
|
65
|
+
const r = f.getBoundingClientRect();
|
|
66
|
+
const vw = window.innerWidth;
|
|
67
|
+
const vh = window.innerHeight;
|
|
68
|
+
const isLarge = r.width >= vw * 0.7 && r.height >= vh * 0.7;
|
|
69
|
+
const hasAppClass = /embed|dashboard|app-frame|portal/i.test(
|
|
70
|
+
f.className + f.id + (f.getAttribute("src") ?? "")
|
|
71
|
+
);
|
|
72
|
+
return isLarge || hasAppClass;
|
|
73
|
+
}).map((f) => f.src).filter(Boolean);
|
|
74
|
+
});
|
|
75
|
+
const baseUrl = page.url();
|
|
76
|
+
const resolved = [];
|
|
77
|
+
for (const src of srcs.slice(0, 5)) {
|
|
78
|
+
try {
|
|
79
|
+
resolved.push(new URL(src, baseUrl).href);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return resolved;
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function crawlPopupChain(context, seedUrl) {
|
|
89
|
+
const nodes = [];
|
|
90
|
+
try {
|
|
91
|
+
const pages = context.pages();
|
|
92
|
+
for (const pg of pages) {
|
|
93
|
+
try {
|
|
94
|
+
const url = pg.url();
|
|
95
|
+
if (!url || url === "about:blank" || url === seedUrl) continue;
|
|
96
|
+
let title = "";
|
|
97
|
+
let interactiveCount = 0;
|
|
98
|
+
let accessible = false;
|
|
99
|
+
try {
|
|
100
|
+
title = await pg.title();
|
|
101
|
+
interactiveCount = await pg.locator("button, a[href], input, select").count();
|
|
102
|
+
accessible = true;
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
nodes.push({
|
|
106
|
+
id: generateContextId("popup", url),
|
|
107
|
+
type: "popup",
|
|
108
|
+
url,
|
|
109
|
+
parentId: null,
|
|
110
|
+
// chain depth not reliably trackable for existing pages
|
|
111
|
+
depth: 1,
|
|
112
|
+
title,
|
|
113
|
+
interactiveCount,
|
|
114
|
+
accessible,
|
|
115
|
+
isEmbeddedApp: false
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
return nodes;
|
|
123
|
+
}
|
|
124
|
+
async function crawlNestedContexts(page, context, seedUrl, opts) {
|
|
125
|
+
const empty = {
|
|
126
|
+
contextTree: [],
|
|
127
|
+
totalContextsFound: 0,
|
|
128
|
+
maxDepthReached: 0,
|
|
129
|
+
inaccessibleCount: 0,
|
|
130
|
+
frameUrls: [],
|
|
131
|
+
embeddedAppUrls: []
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
const maxFrameDepth = opts?.maxFrameDepth ?? 4;
|
|
135
|
+
const [frameNodes, embeddedAppUrls, popupNodes] = await Promise.all([
|
|
136
|
+
crawlFrameTree(page, maxFrameDepth).catch(() => []),
|
|
137
|
+
detectEmbeddedApps(page).catch(() => []),
|
|
138
|
+
context ? crawlPopupChain(context, seedUrl).catch(() => []) : Promise.resolve([])
|
|
139
|
+
]);
|
|
140
|
+
const embeddedSet = new Set(embeddedAppUrls);
|
|
141
|
+
for (const node of frameNodes) {
|
|
142
|
+
if (embeddedSet.has(node.url)) {
|
|
143
|
+
node.isEmbeddedApp = true;
|
|
144
|
+
node.type = "embedded-app";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const contextTree = [...frameNodes, ...popupNodes];
|
|
148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149
|
+
const deduped = contextTree.filter((n) => {
|
|
150
|
+
if (seen.has(n.id)) return false;
|
|
151
|
+
seen.add(n.id);
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
const maxDepthReached = deduped.reduce((max, n) => Math.max(max, n.depth), 0);
|
|
155
|
+
const inaccessibleCount = deduped.filter((n) => !n.accessible).length;
|
|
156
|
+
const frameUrls = [...new Set(frameNodes.map((n) => n.url))];
|
|
157
|
+
return {
|
|
158
|
+
contextTree: deduped,
|
|
159
|
+
totalContextsFound: deduped.length,
|
|
160
|
+
maxDepthReached,
|
|
161
|
+
inaccessibleCount,
|
|
162
|
+
frameUrls,
|
|
163
|
+
embeddedAppUrls
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return empty;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
crawlNestedContexts
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=nested-window-crawler.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/nested-window-crawler.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Nested window / browsing-context crawler \u2014 discovers and records frames,\n * embedded apps, and popup windows that are already open in the browser\n * context at the time of a crawl step.\n *\n * Complements multi-context-handler.ts: that module handles proactive popup\n * interception via context.on('page'); this module snapshots what is already\n * present in the frame tree and context page list.\n */\n\nimport type { Page, BrowserContext } from 'playwright';\n\nexport type BrowsingContextType = 'page' | 'frame' | 'popup' | 'embedded-app';\n\nexport interface BrowsingContextNode {\n id: string;\n type: BrowsingContextType;\n url: string;\n parentId: string | null;\n depth: number;\n title: string;\n interactiveCount: number;\n accessible: boolean;\n isEmbeddedApp: boolean;\n}\n\nexport interface NestedCrawlResult {\n contextTree: BrowsingContextNode[];\n totalContextsFound: number;\n maxDepthReached: number;\n inaccessibleCount: number;\n frameUrls: string[];\n embeddedAppUrls: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Deterministic 12-char hex id derived from context type + URL via FNV-1a.\n * No external deps \u2014 algorithm implemented inline.\n */\nfunction generateContextId(type: string, url: string): string {\n let h = 0x811c9dc5;\n const s = type + url;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = (h * 0x01000193) >>> 0;\n }\n return h.toString(16).padStart(8, '0').slice(0, 12);\n}\n\n/**\n * Returns the frame depth by walking the parentFrame() chain.\n * The main frame has depth 0.\n */\nfunction computeFrameDepth(frame: import('playwright').Frame): number {\n let depth = 0;\n let current = frame.parentFrame();\n while (current !== null) {\n depth++;\n current = current.parentFrame();\n }\n return depth;\n}\n\n/**\n * Walks all frames attached to the page and returns a BrowsingContextNode\n * for each reachable, non-trivial frame up to maxDepth.\n */\nasync function crawlFrameTree(\n page: any,\n maxDepth = 4,\n): Promise<BrowsingContextNode[]> {\n const nodes: BrowsingContextNode[] = [];\n\n try {\n const frames = page.frames();\n const limited = frames.slice(0, 200);\n\n for (const frame of limited) {\n try {\n const url = frame.url();\n\n // Skip blank / synthetic frames\n if (\n !url ||\n url === 'about:blank' ||\n url.startsWith('javascript:') ||\n url.startsWith('data:')\n ) {\n continue;\n }\n\n const depth = computeFrameDepth(frame);\n if (depth > maxDepth) continue;\n\n const parentFrame = frame.parentFrame();\n const parentId = parentFrame\n ? generateContextId('frame', parentFrame.url())\n : null;\n\n let title = '';\n let interactiveCount = 0;\n let accessible = false;\n\n try {\n title = await frame.title();\n interactiveCount = await frame\n .locator('button, a[href], input, select')\n .count();\n accessible = true;\n } catch {\n // cross-origin or sandboxed \u2014 treat as inaccessible\n }\n\n nodes.push({\n id: generateContextId('frame', url),\n type: 'frame',\n url,\n parentId,\n depth,\n title,\n interactiveCount,\n accessible,\n isEmbeddedApp: false,\n });\n } catch {\n // skip individual frame errors\n }\n }\n } catch {\n // skip entirely if frames() fails\n }\n\n return nodes;\n}\n\n/**\n * Evaluates the page DOM to identify iframes that look like full embedded\n * applications (large viewport coverage, or app-class/id/src signals).\n * Returns up to 5 absolute URLs.\n */\nasync function detectEmbeddedApps(page: any): Promise<string[]> {\n try {\n const srcs = await (page.evaluate as any)(() => {\n return Array.from(document.querySelectorAll('iframe'))\n .filter((f) => {\n const r = f.getBoundingClientRect();\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const isLarge = r.width >= vw * 0.7 && r.height >= vh * 0.7;\n const hasAppClass = /embed|dashboard|app-frame|portal/i.test(\n f.className + f.id + (f.getAttribute('src') ?? ''),\n );\n return isLarge || hasAppClass;\n })\n .map((f) => f.src)\n .filter(Boolean);\n });\n\n const baseUrl = page.url();\n const resolved: string[] = [];\n\n for (const src of srcs.slice(0, 5)) {\n try {\n resolved.push(new URL(src, baseUrl).href);\n } catch {\n // skip unparseable src\n }\n }\n\n return resolved;\n } catch {\n return [];\n }\n}\n\n/**\n * Snapshots all pages currently open in the context that are not the seed\n * URL and records them as popup nodes at depth 1.\n *\n * Proactive interception (context.on('page')) is handled separately by\n * attachNewTabListener in multi-context-handler.ts.\n */\nasync function crawlPopupChain(\n context: BrowserContext,\n seedUrl: string,\n): Promise<BrowsingContextNode[]> {\n const nodes: BrowsingContextNode[] = [];\n\n try {\n const pages = context.pages();\n\n for (const pg of pages) {\n try {\n const url = pg.url();\n\n if (!url || url === 'about:blank' || url === seedUrl) continue;\n\n let title = '';\n let interactiveCount = 0;\n let accessible = false;\n\n try {\n title = await pg.title();\n interactiveCount = await pg\n .locator('button, a[href], input, select')\n .count();\n accessible = true;\n } catch {\n // page may be closed or cross-origin restricted\n }\n\n nodes.push({\n id: generateContextId('popup', url),\n type: 'popup',\n url,\n parentId: null, // chain depth not reliably trackable for existing pages\n depth: 1,\n title,\n interactiveCount,\n accessible,\n isEmbeddedApp: false,\n });\n } catch {\n // skip individual page errors\n }\n }\n } catch {\n // skip if context.pages() fails\n }\n\n return nodes;\n}\n\n// ---------------------------------------------------------------------------\n// Exported entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Discovers all nested browsing contexts reachable from the current page:\n * sub-frames, embedded applications, and already-open popup pages.\n *\n * All failures are non-fatal \u2014 errors are caught and an empty result is\n * returned so the caller's crawl step can continue uninterrupted.\n */\nexport async function crawlNestedContexts(\n page: any,\n context: BrowserContext | null,\n seedUrl: string,\n opts?: { maxFrameDepth?: number; maxPopupDepth?: number },\n): Promise<NestedCrawlResult> {\n const empty: NestedCrawlResult = {\n contextTree: [],\n totalContextsFound: 0,\n maxDepthReached: 0,\n inaccessibleCount: 0,\n frameUrls: [],\n embeddedAppUrls: [],\n };\n\n try {\n const maxFrameDepth = opts?.maxFrameDepth ?? 4;\n\n // Run independent discovery passes concurrently\n const [frameNodes, embeddedAppUrls, popupNodes] = await Promise.all([\n crawlFrameTree(page, maxFrameDepth).catch((): BrowsingContextNode[] => []),\n detectEmbeddedApps(page).catch((): string[] => []),\n context\n ? crawlPopupChain(context, seedUrl).catch((): BrowsingContextNode[] => [])\n : Promise.resolve<BrowsingContextNode[]>([]),\n ]);\n\n // Promote frame nodes that match an embedded-app URL\n const embeddedSet = new Set(embeddedAppUrls);\n for (const node of frameNodes) {\n if (embeddedSet.has(node.url)) {\n node.isEmbeddedApp = true;\n node.type = 'embedded-app';\n }\n }\n\n const contextTree: BrowsingContextNode[] = [...frameNodes, ...popupNodes];\n\n // Deduplicate by id (same URL appearing in both lists)\n const seen = new Set<string>();\n const deduped = contextTree.filter((n) => {\n if (seen.has(n.id)) return false;\n seen.add(n.id);\n return true;\n });\n\n const maxDepthReached = deduped.reduce((max, n) => Math.max(max, n.depth), 0);\n const inaccessibleCount = deduped.filter((n) => !n.accessible).length;\n const frameUrls = [...new Set(frameNodes.map((n) => n.url))];\n\n return {\n contextTree: deduped,\n totalContextsFound: deduped.length,\n maxDepthReached,\n inaccessibleCount,\n frameUrls,\n embeddedAppUrls,\n };\n } catch {\n return empty;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|