@detiq/crawler 1.1.8 → 1.1.9
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/dist/accessibility-auditor.js +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.js +28 -67
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.js +187 -151
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
package/dist/live-view.js
CHANGED
|
@@ -1,101 +1,118 @@
|
|
|
1
|
-
import { chromium } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import { logger } from
|
|
4
|
-
const sessions =
|
|
5
|
-
const SESSION_TTL_MS = 15 * 60 *
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
const SESSION_TTL_MS = 15 * 60 * 1_000; // 15 min
|
|
6
6
|
function generateId() {
|
|
7
|
-
|
|
7
|
+
return randomBytes(16).toString('hex');
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Start a live view session: launches a headless browser, navigates to appUrl,
|
|
11
|
+
* begins capturing frames. Returns the session ID.
|
|
12
|
+
*/
|
|
13
|
+
export async function startLiveView(appUrl, storageState) {
|
|
14
|
+
const id = generateId();
|
|
15
|
+
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === 'linux';
|
|
16
|
+
const browser = await chromium.launch({
|
|
17
|
+
headless: true,
|
|
18
|
+
args: isDocker ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : [],
|
|
19
|
+
});
|
|
20
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
21
|
+
// Restore session if provided
|
|
22
|
+
if (storageState) {
|
|
23
|
+
try {
|
|
24
|
+
const state = JSON.parse(storageState);
|
|
25
|
+
if (state.cookies?.length)
|
|
26
|
+
await context.addCookies(state.cookies);
|
|
27
|
+
}
|
|
28
|
+
catch { /* ignore */ }
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setTimeout(() => closeLiveView(id).catch(() => {
|
|
48
|
-
}), SESSION_TTL_MS);
|
|
49
|
-
logger.info({ id, appUrl }, "[live-view] Session started");
|
|
50
|
-
return id;
|
|
30
|
+
const page = await context.newPage();
|
|
31
|
+
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
32
|
+
const session = {
|
|
33
|
+
id, browser, context, page, appUrl,
|
|
34
|
+
lastFrame: null, streaming: true, frameInterval: null,
|
|
35
|
+
createdAt: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
// Start capturing frames (every 500ms)
|
|
38
|
+
session.frameInterval = setInterval(async () => {
|
|
39
|
+
if (!session.streaming)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });
|
|
43
|
+
session.lastFrame = buf.toString('base64');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
session.streaming = false;
|
|
47
|
+
}
|
|
48
|
+
}, 500);
|
|
49
|
+
sessions.set(id, session);
|
|
50
|
+
// Auto-cleanup after TTL
|
|
51
|
+
setTimeout(() => closeLiveView(id).catch(() => { }), SESSION_TTL_MS);
|
|
52
|
+
logger.info({ id, appUrl }, '[live-view] Session started');
|
|
53
|
+
return id;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Get the latest frame for a session (base64 JPEG).
|
|
57
|
+
*/
|
|
58
|
+
export function getFrame(id) {
|
|
59
|
+
return sessions.get(id)?.lastFrame ?? null;
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Navigate the live view browser to a URL.
|
|
63
|
+
*/
|
|
64
|
+
export async function liveViewNavigate(id, url) {
|
|
65
|
+
const session = sessions.get(id);
|
|
66
|
+
if (!session)
|
|
67
|
+
throw new Error(`Live view session ${id} not found`);
|
|
68
|
+
await session.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
69
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Capture storageState from the live view session, close the browser, return JSON string.
|
|
72
|
+
*/
|
|
73
|
+
export async function completeLiveView(id) {
|
|
74
|
+
const session = sessions.get(id);
|
|
75
|
+
if (!session)
|
|
76
|
+
throw new Error(`Live view session ${id} not found or expired`);
|
|
77
|
+
const state = await session.context.storageState();
|
|
78
|
+
const json = JSON.stringify(state);
|
|
79
|
+
await closeLiveView(id);
|
|
80
|
+
return json;
|
|
67
81
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Send a mouse or keyboard input event to the live view browser.
|
|
84
|
+
*/
|
|
85
|
+
export async function sendInput(id, event) {
|
|
86
|
+
const session = sessions.get(id);
|
|
87
|
+
if (!session)
|
|
88
|
+
throw new Error(`Live view session ${id} not found`);
|
|
89
|
+
if (event.type === 'click' && event.x !== undefined && event.y !== undefined) {
|
|
90
|
+
await session.page.mouse.click(event.x, event.y, { button: event.button ?? 'left' });
|
|
91
|
+
}
|
|
92
|
+
else if (event.type === 'type' && event.text) {
|
|
93
|
+
await session.page.keyboard.type(event.text);
|
|
94
|
+
}
|
|
95
|
+
else if (event.type === 'key' && event.key) {
|
|
96
|
+
await session.page.keyboard.press(event.key);
|
|
97
|
+
}
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Close a live view session without capturing.
|
|
101
|
+
*/
|
|
102
|
+
export async function closeLiveView(id) {
|
|
103
|
+
const session = sessions.get(id);
|
|
104
|
+
if (!session)
|
|
105
|
+
return;
|
|
106
|
+
session.streaming = false;
|
|
107
|
+
if (session.frameInterval)
|
|
108
|
+
clearInterval(session.frameInterval);
|
|
109
|
+
sessions.delete(id);
|
|
110
|
+
await session.browser.close().catch(() => { });
|
|
111
|
+
logger.info({ id }, '[live-view] Session closed');
|
|
88
112
|
}
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
/**
|
|
114
|
+
* List all active session IDs (for health checks / cleanup).
|
|
115
|
+
*/
|
|
116
|
+
export function listLiveViews() {
|
|
117
|
+
return [...sessions.keys()];
|
|
91
118
|
}
|
|
92
|
-
export {
|
|
93
|
-
closeLiveView,
|
|
94
|
-
completeLiveView,
|
|
95
|
-
getFrame,
|
|
96
|
-
listLiveViews,
|
|
97
|
-
liveViewNavigate,
|
|
98
|
-
sendInput,
|
|
99
|
-
startLiveView
|
|
100
|
-
};
|
|
101
|
-
//# sourceMappingURL=live-view.js.map
|
|
@@ -1,163 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.
|
|
3
|
+
* Priority: data-testid → aria-label/placeholder (inputs) → role+name (buttons/links) → heading → text → id
|
|
4
|
+
*/
|
|
5
|
+
/** Parse the notes JSON string stored on Element records. */
|
|
1
6
|
function parseNotes(notes) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
if (!notes)
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;
|
|
11
|
+
if (parsed && typeof parsed === 'object')
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
return {};
|
|
9
16
|
}
|
|
17
|
+
/** Map crawler role strings to Playwright role values. */
|
|
10
18
|
function mapRole(role) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (!role)
|
|
20
|
+
return null;
|
|
21
|
+
const r = role.toLowerCase().trim();
|
|
22
|
+
if (r === 'button' || r === 'submit' || r === 'reset')
|
|
23
|
+
return 'button';
|
|
24
|
+
if (r === 'link' || r === 'a' || r === 'anchor')
|
|
25
|
+
return 'link';
|
|
26
|
+
if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search')
|
|
27
|
+
return 'textbox';
|
|
28
|
+
if (r === 'combobox' || r === 'select' || r === 'dropdown')
|
|
29
|
+
return 'combobox';
|
|
30
|
+
if (r === 'checkbox')
|
|
31
|
+
return 'checkbox';
|
|
32
|
+
if (r === 'radio')
|
|
33
|
+
return 'radio';
|
|
34
|
+
if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6')
|
|
35
|
+
return 'heading';
|
|
36
|
+
if (r === 'img' || r === 'image')
|
|
37
|
+
return 'img';
|
|
38
|
+
if (r === 'nav' || r === 'navigation')
|
|
39
|
+
return 'navigation';
|
|
40
|
+
return r;
|
|
23
41
|
}
|
|
42
|
+
/** Convert "Submit Form Button" → "submitFormButton" */
|
|
24
43
|
function toKey(text) {
|
|
25
|
-
|
|
44
|
+
return text
|
|
45
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
46
|
+
.replace(/\s+(.)/g, (_, c) => c.toUpperCase())
|
|
47
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase())
|
|
48
|
+
.replace(/\s/g, '')
|
|
49
|
+
.slice(0, 40)
|
|
50
|
+
|| 'element';
|
|
26
51
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
53
|
+
export function elementToPlaywrightLocator(element) {
|
|
54
|
+
const attrs = parseNotes(element.notes);
|
|
55
|
+
const testId = attrs['data-testid'];
|
|
56
|
+
const ariaLabel = attrs['aria-label'] || element.meaning;
|
|
57
|
+
const id = attrs['id'];
|
|
58
|
+
const placeholder = attrs['placeholder'];
|
|
59
|
+
const name = attrs['name'];
|
|
60
|
+
const text = (element.text ?? '').trim();
|
|
61
|
+
const meaning = (element.meaning ?? '').trim();
|
|
62
|
+
const role = mapRole(element.role);
|
|
63
|
+
const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';
|
|
64
|
+
const key = toKey(label);
|
|
65
|
+
// Priority 1: data-testid
|
|
66
|
+
if (testId) {
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
expression: `page.getByTestId(${JSON.stringify(testId)})`,
|
|
70
|
+
elementType: role ?? 'element',
|
|
71
|
+
confidence: 'high',
|
|
72
|
+
source: 'testid',
|
|
73
|
+
description: meaning || testId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Priority 2: input/combobox/textarea with label → getByLabel; with placeholder → getByPlaceholder
|
|
77
|
+
if (role === 'textbox' || role === 'combobox') {
|
|
78
|
+
if (ariaLabel) {
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
82
|
+
elementType: 'input',
|
|
83
|
+
confidence: 'high',
|
|
84
|
+
source: 'label',
|
|
85
|
+
description: ariaLabel,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (placeholder) {
|
|
89
|
+
return {
|
|
90
|
+
key,
|
|
91
|
+
expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
|
|
92
|
+
elementType: 'input',
|
|
93
|
+
confidence: 'high',
|
|
94
|
+
source: 'placeholder',
|
|
95
|
+
description: placeholder,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Priority 3: checkbox/radio → getByLabel
|
|
100
|
+
if ((role === 'checkbox' || role === 'radio') && ariaLabel) {
|
|
101
|
+
return {
|
|
102
|
+
key,
|
|
103
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
104
|
+
elementType: role,
|
|
105
|
+
confidence: 'high',
|
|
106
|
+
source: 'label',
|
|
107
|
+
description: ariaLabel,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Priority 4: button/link with meaningful name → getByRole
|
|
111
|
+
if ((role === 'button' || role === 'link') && meaning) {
|
|
112
|
+
return {
|
|
113
|
+
key,
|
|
114
|
+
expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
|
|
115
|
+
elementType: role,
|
|
116
|
+
confidence: 'high',
|
|
117
|
+
source: 'role-name',
|
|
118
|
+
description: meaning,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Priority 5: heading → getByRole heading
|
|
122
|
+
if (role === 'heading' && (meaning || text)) {
|
|
123
|
+
const name2 = meaning || text;
|
|
124
|
+
return {
|
|
125
|
+
key,
|
|
126
|
+
expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
|
|
127
|
+
elementType: 'heading',
|
|
128
|
+
confidence: 'medium',
|
|
129
|
+
source: 'heading',
|
|
130
|
+
description: name2,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Priority 6: existing CSS selector from crawler
|
|
134
|
+
if (element.selector && element.selector.length > 0) {
|
|
135
|
+
return {
|
|
136
|
+
key,
|
|
137
|
+
expression: `page.locator(${JSON.stringify(element.selector)})`,
|
|
138
|
+
elementType: role ?? 'element',
|
|
139
|
+
confidence: 'medium',
|
|
140
|
+
source: 'id',
|
|
141
|
+
description: meaning || text || element.selector,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Priority 7: id from attrs
|
|
145
|
+
if (id) {
|
|
146
|
+
return {
|
|
147
|
+
key,
|
|
148
|
+
expression: `page.locator(${JSON.stringify('#' + id)})`,
|
|
149
|
+
elementType: role ?? 'element',
|
|
150
|
+
confidence: 'medium',
|
|
151
|
+
source: 'id',
|
|
152
|
+
description: meaning || id,
|
|
153
|
+
};
|
|
59
154
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
155
|
+
// Priority 8: fallback to text content
|
|
156
|
+
if (meaning || text) {
|
|
157
|
+
const t = meaning || text;
|
|
158
|
+
return {
|
|
159
|
+
key,
|
|
160
|
+
expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
|
|
161
|
+
elementType: role ?? 'text',
|
|
162
|
+
confidence: 'low',
|
|
163
|
+
source: 'text',
|
|
164
|
+
description: t,
|
|
165
|
+
};
|
|
69
166
|
}
|
|
70
|
-
|
|
71
|
-
if ((role === "checkbox" || role === "radio") && ariaLabel) {
|
|
72
|
-
return {
|
|
73
|
-
key,
|
|
74
|
-
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
75
|
-
elementType: role,
|
|
76
|
-
confidence: "high",
|
|
77
|
-
source: "label",
|
|
78
|
-
description: ariaLabel
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if ((role === "button" || role === "link") && meaning) {
|
|
82
|
-
return {
|
|
83
|
-
key,
|
|
84
|
-
expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
|
|
85
|
-
elementType: role,
|
|
86
|
-
confidence: "high",
|
|
87
|
-
source: "role-name",
|
|
88
|
-
description: meaning
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (role === "heading" && (meaning || text)) {
|
|
92
|
-
const name2 = meaning || text;
|
|
93
|
-
return {
|
|
94
|
-
key,
|
|
95
|
-
expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
|
|
96
|
-
elementType: "heading",
|
|
97
|
-
confidence: "medium",
|
|
98
|
-
source: "heading",
|
|
99
|
-
description: name2
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
if (element.selector && element.selector.length > 0) {
|
|
103
|
-
return {
|
|
104
|
-
key,
|
|
105
|
-
expression: `page.locator(${JSON.stringify(element.selector)})`,
|
|
106
|
-
elementType: role ?? "element",
|
|
107
|
-
confidence: "medium",
|
|
108
|
-
source: "id",
|
|
109
|
-
description: meaning || text || element.selector
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
if (id) {
|
|
113
|
-
return {
|
|
114
|
-
key,
|
|
115
|
-
expression: `page.locator(${JSON.stringify("#" + id)})`,
|
|
116
|
-
elementType: role ?? "element",
|
|
117
|
-
confidence: "medium",
|
|
118
|
-
source: "id",
|
|
119
|
-
description: meaning || id
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (meaning || text) {
|
|
123
|
-
const t = meaning || text;
|
|
124
|
-
return {
|
|
125
|
-
key,
|
|
126
|
-
expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
|
|
127
|
-
elementType: role ?? "text",
|
|
128
|
-
confidence: "low",
|
|
129
|
-
source: "text",
|
|
130
|
-
description: t
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
167
|
+
return null;
|
|
134
168
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
171
|
+
* Deduplicates by key — higher confidence wins.
|
|
172
|
+
*/
|
|
173
|
+
export function elementsToPlaywrightLocators(elements) {
|
|
174
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
|
|
175
|
+
const byKey = new Map();
|
|
176
|
+
for (const el of elements) {
|
|
177
|
+
const loc = elementToPlaywrightLocator(el);
|
|
178
|
+
if (!loc)
|
|
179
|
+
continue;
|
|
180
|
+
const existing = byKey.get(loc.key);
|
|
181
|
+
if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
|
|
182
|
+
byKey.set(loc.key, loc);
|
|
183
|
+
}
|
|
144
184
|
}
|
|
145
|
-
|
|
146
|
-
return Array.from(byKey.values());
|
|
185
|
+
return Array.from(byKey.values());
|
|
147
186
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
189
|
+
* Returns empty string if no locators.
|
|
190
|
+
*/
|
|
191
|
+
export function formatLocatorsForPrompt(locators) {
|
|
192
|
+
if (locators.length === 0)
|
|
193
|
+
return '';
|
|
194
|
+
const lines = locators.map(l => ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`);
|
|
195
|
+
return [
|
|
196
|
+
'EXTRACTED PAGE LOCATORS (use these EXACTLY — do not invent other selectors; these were verified against the live page):',
|
|
197
|
+
...lines,
|
|
198
|
+
].join('\n');
|
|
157
199
|
}
|
|
158
|
-
export {
|
|
159
|
-
elementToPlaywrightLocator,
|
|
160
|
-
elementsToPlaywrightLocators,
|
|
161
|
-
formatLocatorsForPrompt
|
|
162
|
-
};
|
|
163
|
-
//# sourceMappingURL=locator-extractor.js.map
|
package/dist/logger.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import pino from
|
|
2
|
-
const logger = pino({
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
export const logger = pino({
|
|
3
|
+
level: process.env.LOG_LEVEL ?? 'info',
|
|
4
|
+
...(process.env.NODE_ENV === 'development'
|
|
5
|
+
? { transport: { target: 'pino-pretty', options: { colorize: true } } }
|
|
6
|
+
: {}),
|
|
7
|
+
base: { service: 'crawler' },
|
|
6
8
|
});
|
|
7
|
-
export {
|
|
8
|
-
logger
|
|
9
|
-
};
|
|
10
|
-
//# sourceMappingURL=logger.js.map
|