@detiq/crawler 1.1.7 → 1.1.8
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/extractors/advanced.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +2 -2
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +163 -0
- package/dist/locator-extractor.js.map +7 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
|
@@ -13,11 +13,21 @@ export interface PageMetadata {
|
|
|
13
13
|
ogImage: string | null;
|
|
14
14
|
ogUrl: string | null;
|
|
15
15
|
ogType: string | null;
|
|
16
|
+
ogSiteName: string | null;
|
|
17
|
+
ogLocale: string | null;
|
|
16
18
|
twitterCard: string | null;
|
|
17
19
|
twitterTitle: string | null;
|
|
18
20
|
twitterDescription: string | null;
|
|
21
|
+
twitterImage: string | null;
|
|
22
|
+
twitterSite: string | null;
|
|
19
23
|
canonical: string | null;
|
|
20
24
|
robots: string | null;
|
|
25
|
+
hreflang: Array<{
|
|
26
|
+
lang: string;
|
|
27
|
+
href: string;
|
|
28
|
+
}>;
|
|
29
|
+
themeColor: string | null;
|
|
30
|
+
favicon: string | null;
|
|
21
31
|
jsonLd: object[];
|
|
22
32
|
schemaTypes: string[];
|
|
23
33
|
lang: string | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -35,3 +35,5 @@ export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemet
|
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
36
|
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
37
|
export type { FailureContext } from './failure-analysis-agent.js';
|
|
38
|
+
export { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from './locator-extractor.js';
|
|
39
|
+
export type { PlaywrightLocator } from './locator-extractor.js';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { TieredProxyManager } from "./proxy.js";
|
|
|
25
25
|
import { RedisCrawlFrontier, isRedisFrontierEnabled } from "./redis-frontier.js";
|
|
26
26
|
import { startSpan, endSpan, withSpan, increment, flushMetrics } from "./telemetry.js";
|
|
27
27
|
import { analyzeFailure } from "./failure-analysis-agent.js";
|
|
28
|
+
import { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from "./locator-extractor.js";
|
|
28
29
|
export {
|
|
29
30
|
CHROME_TLS_HEADERS,
|
|
30
31
|
RedisCrawlFrontier,
|
|
@@ -38,9 +39,12 @@ export {
|
|
|
38
39
|
createRedisQueue,
|
|
39
40
|
discoverProjectUrls,
|
|
40
41
|
discoverSitemapUrls,
|
|
42
|
+
elementToPlaywrightLocator,
|
|
43
|
+
elementsToPlaywrightLocators,
|
|
41
44
|
endSpan,
|
|
42
45
|
extractMarkdown,
|
|
43
46
|
flushMetrics,
|
|
47
|
+
formatLocatorsForPrompt,
|
|
44
48
|
getFrame,
|
|
45
49
|
getInboxEmail,
|
|
46
50
|
getSsoClientCredentialsToken,
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\nexport { analyzeFailure } from './failure-analysis-agent.js';\nexport type { FailureContext } from './failure-analysis-agent.js';\n"],
|
|
5
|
-
"mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;AAEtE,SAAS,sBAAsB;",
|
|
4
|
+
"sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\nexport { analyzeFailure } from './failure-analysis-agent.js';\nexport type { FailureContext } from './failure-analysis-agent.js';\nexport { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from './locator-extractor.js';\nexport type { PlaywrightLocator } from './locator-extractor.js';\n"],
|
|
5
|
+
"mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;AAEtE,SAAS,sBAAsB;AAE/B,SAAS,4BAA4B,8BAA8B,+BAA+B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -43,6 +43,35 @@ export interface ConditionalReveal {
|
|
|
43
43
|
triggerValue: string;
|
|
44
44
|
revealedText: string;
|
|
45
45
|
}
|
|
46
|
+
export interface TooltipCapture {
|
|
47
|
+
triggerLabel: string;
|
|
48
|
+
tooltipText: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TabPanelCapture {
|
|
51
|
+
tabLabel: string;
|
|
52
|
+
panelContent: string;
|
|
53
|
+
panelHeading?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface CarouselSlide {
|
|
56
|
+
slideIndex: number;
|
|
57
|
+
heading?: string;
|
|
58
|
+
content: string;
|
|
59
|
+
}
|
|
60
|
+
export interface WizardStep {
|
|
61
|
+
stepIndex: number;
|
|
62
|
+
stepLabel?: string;
|
|
63
|
+
content: string;
|
|
64
|
+
fields: string[];
|
|
65
|
+
}
|
|
66
|
+
export interface FocusTrapResult {
|
|
67
|
+
modalTrigger: string;
|
|
68
|
+
focusTrapped: boolean;
|
|
69
|
+
sampledFocused: string[];
|
|
70
|
+
}
|
|
71
|
+
export interface ContextMenuCapture {
|
|
72
|
+
trigger: string;
|
|
73
|
+
items: string[];
|
|
74
|
+
}
|
|
46
75
|
export interface ProbeResult {
|
|
47
76
|
screenId: string;
|
|
48
77
|
url: string;
|
|
@@ -51,6 +80,13 @@ export interface ProbeResult {
|
|
|
51
80
|
modals: ModalObservation[];
|
|
52
81
|
conditionals: ConditionalReveal[];
|
|
53
82
|
loadingPatterns: string[];
|
|
83
|
+
tooltips?: TooltipCapture[];
|
|
84
|
+
tabPanels?: TabPanelCapture[];
|
|
85
|
+
carouselSlides?: CarouselSlide[];
|
|
86
|
+
wizardSteps?: WizardStep[];
|
|
87
|
+
focusTraps?: FocusTrapResult[];
|
|
88
|
+
skipNavIssues?: string[];
|
|
89
|
+
contextMenus?: ContextMenuCapture[];
|
|
54
90
|
}
|
|
55
91
|
/**
|
|
56
92
|
* Run interactive probing on a single screen page.
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export interface PlaywrightLocator {
|
|
6
|
+
key: string;
|
|
7
|
+
expression: string;
|
|
8
|
+
elementType: string;
|
|
9
|
+
confidence: 'high' | 'medium' | 'low';
|
|
10
|
+
source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
14
|
+
export declare function elementToPlaywrightLocator(element: {
|
|
15
|
+
role?: string | null;
|
|
16
|
+
meaning?: string | null;
|
|
17
|
+
text?: string | null;
|
|
18
|
+
notes?: unknown;
|
|
19
|
+
selector?: string | null;
|
|
20
|
+
}): PlaywrightLocator | null;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
23
|
+
* Deduplicates by key — higher confidence wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function elementsToPlaywrightLocators(elements: Array<{
|
|
26
|
+
role?: string | null;
|
|
27
|
+
meaning?: string | null;
|
|
28
|
+
text?: string | null;
|
|
29
|
+
notes?: unknown;
|
|
30
|
+
selector?: string | null;
|
|
31
|
+
isInteractive?: boolean | null;
|
|
32
|
+
}>): PlaywrightLocator[];
|
|
33
|
+
/**
|
|
34
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
35
|
+
* Returns empty string if no locators.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
function parseNotes(notes) {
|
|
2
|
+
if (!notes) return {};
|
|
3
|
+
try {
|
|
4
|
+
const parsed = typeof notes === "string" ? JSON.parse(notes) : notes;
|
|
5
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
function mapRole(role) {
|
|
11
|
+
if (!role) return null;
|
|
12
|
+
const r = role.toLowerCase().trim();
|
|
13
|
+
if (r === "button" || r === "submit" || r === "reset") return "button";
|
|
14
|
+
if (r === "link" || r === "a" || r === "anchor") return "link";
|
|
15
|
+
if (r === "textbox" || r === "input" || r === "text" || r === "email" || r === "password" || r === "tel" || r === "number" || r === "search") return "textbox";
|
|
16
|
+
if (r === "combobox" || r === "select" || r === "dropdown") return "combobox";
|
|
17
|
+
if (r === "checkbox") return "checkbox";
|
|
18
|
+
if (r === "radio") return "radio";
|
|
19
|
+
if (r === "heading" || r === "h1" || r === "h2" || r === "h3" || r === "h4" || r === "h5" || r === "h6") return "heading";
|
|
20
|
+
if (r === "img" || r === "image") return "img";
|
|
21
|
+
if (r === "nav" || r === "navigation") return "navigation";
|
|
22
|
+
return r;
|
|
23
|
+
}
|
|
24
|
+
function toKey(text) {
|
|
25
|
+
return text.replace(/[^a-zA-Z0-9\s]/g, " ").replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase()).replace(/\s/g, "").slice(0, 40) || "element";
|
|
26
|
+
}
|
|
27
|
+
function elementToPlaywrightLocator(element) {
|
|
28
|
+
const attrs = parseNotes(element.notes);
|
|
29
|
+
const testId = attrs["data-testid"];
|
|
30
|
+
const ariaLabel = attrs["aria-label"] || element.meaning;
|
|
31
|
+
const id = attrs["id"];
|
|
32
|
+
const placeholder = attrs["placeholder"];
|
|
33
|
+
const name = attrs["name"];
|
|
34
|
+
const text = (element.text ?? "").trim();
|
|
35
|
+
const meaning = (element.meaning ?? "").trim();
|
|
36
|
+
const role = mapRole(element.role);
|
|
37
|
+
const label = meaning || text || testId || ariaLabel || placeholder || id || name || "element";
|
|
38
|
+
const key = toKey(label);
|
|
39
|
+
if (testId) {
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
expression: `page.getByTestId(${JSON.stringify(testId)})`,
|
|
43
|
+
elementType: role ?? "element",
|
|
44
|
+
confidence: "high",
|
|
45
|
+
source: "testid",
|
|
46
|
+
description: meaning || testId
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (role === "textbox" || role === "combobox") {
|
|
50
|
+
if (ariaLabel) {
|
|
51
|
+
return {
|
|
52
|
+
key,
|
|
53
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
54
|
+
elementType: "input",
|
|
55
|
+
confidence: "high",
|
|
56
|
+
source: "label",
|
|
57
|
+
description: ariaLabel
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (placeholder) {
|
|
61
|
+
return {
|
|
62
|
+
key,
|
|
63
|
+
expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
|
|
64
|
+
elementType: "input",
|
|
65
|
+
confidence: "high",
|
|
66
|
+
source: "placeholder",
|
|
67
|
+
description: placeholder
|
|
68
|
+
};
|
|
69
|
+
}
|
|
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;
|
|
134
|
+
}
|
|
135
|
+
function elementsToPlaywrightLocators(elements) {
|
|
136
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
|
|
137
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const el of elements) {
|
|
139
|
+
const loc = elementToPlaywrightLocator(el);
|
|
140
|
+
if (!loc) continue;
|
|
141
|
+
const existing = byKey.get(loc.key);
|
|
142
|
+
if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
|
|
143
|
+
byKey.set(loc.key, loc);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Array.from(byKey.values());
|
|
147
|
+
}
|
|
148
|
+
function formatLocatorsForPrompt(locators) {
|
|
149
|
+
if (locators.length === 0) return "";
|
|
150
|
+
const lines = locators.map(
|
|
151
|
+
(l) => ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`
|
|
152
|
+
);
|
|
153
|
+
return [
|
|
154
|
+
"EXTRACTED PAGE LOCATORS (use these EXACTLY \u2014 do not invent other selectors; these were verified against the live page):",
|
|
155
|
+
...lines
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
elementToPlaywrightLocator,
|
|
160
|
+
elementsToPlaywrightLocators,
|
|
161
|
+
formatLocatorsForPrompt
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=locator-extractor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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}): 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\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 getByLabel\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 4: button/link with meaningful name \u2192 getByRole\n if ((role === 'button' || role === 'link') && meaning) {\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,\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}>): 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": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -35,3 +35,5 @@ export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemet
|
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
36
|
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
37
|
export type { FailureContext } from './failure-analysis-agent.js';
|
|
38
|
+
export { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from './locator-extractor.js';
|
|
39
|
+
export type { PlaywrightLocator } from './locator-extractor.js';
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
|
|
6
|
+
export interface PlaywrightLocator {
|
|
7
|
+
key: string; // camelCase property name for page object
|
|
8
|
+
expression: string; // e.g. "page.getByRole('button', { name: 'Submit' })"
|
|
9
|
+
elementType: string; // button | input | link | heading | text | element
|
|
10
|
+
confidence: 'high' | 'medium' | 'low';
|
|
11
|
+
source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';
|
|
12
|
+
description: string; // human-readable label
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse the notes JSON string stored on Element records. */
|
|
16
|
+
function parseNotes(notes: unknown): Record<string, string> {
|
|
17
|
+
if (!notes) return {};
|
|
18
|
+
try {
|
|
19
|
+
const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;
|
|
20
|
+
if (parsed && typeof parsed === 'object') return parsed as Record<string, string>;
|
|
21
|
+
} catch {}
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Map crawler role strings to Playwright role values. */
|
|
26
|
+
function mapRole(role?: string | null): string | null {
|
|
27
|
+
if (!role) return null;
|
|
28
|
+
const r = role.toLowerCase().trim();
|
|
29
|
+
if (r === 'button' || r === 'submit' || r === 'reset') return 'button';
|
|
30
|
+
if (r === 'link' || r === 'a' || r === 'anchor') return 'link';
|
|
31
|
+
if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search') return 'textbox';
|
|
32
|
+
if (r === 'combobox' || r === 'select' || r === 'dropdown') return 'combobox';
|
|
33
|
+
if (r === 'checkbox') return 'checkbox';
|
|
34
|
+
if (r === 'radio') return 'radio';
|
|
35
|
+
if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6') return 'heading';
|
|
36
|
+
if (r === 'img' || r === 'image') return 'img';
|
|
37
|
+
if (r === 'nav' || r === 'navigation') return 'navigation';
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Convert "Submit Form Button" → "submitFormButton" */
|
|
42
|
+
function toKey(text: string): string {
|
|
43
|
+
return text
|
|
44
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
45
|
+
.replace(/\s+(.)/g, (_, c) => c.toUpperCase())
|
|
46
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase())
|
|
47
|
+
.replace(/\s/g, '')
|
|
48
|
+
.slice(0, 40)
|
|
49
|
+
|| 'element';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
53
|
+
export function elementToPlaywrightLocator(element: {
|
|
54
|
+
role?: string | null;
|
|
55
|
+
meaning?: string | null;
|
|
56
|
+
text?: string | null;
|
|
57
|
+
notes?: unknown;
|
|
58
|
+
selector?: string | null;
|
|
59
|
+
}): PlaywrightLocator | null {
|
|
60
|
+
const attrs = parseNotes(element.notes);
|
|
61
|
+
const testId = attrs['data-testid'];
|
|
62
|
+
const ariaLabel = attrs['aria-label'] || element.meaning;
|
|
63
|
+
const id = attrs['id'];
|
|
64
|
+
const placeholder = attrs['placeholder'];
|
|
65
|
+
const name = attrs['name'];
|
|
66
|
+
const text = (element.text ?? '').trim();
|
|
67
|
+
const meaning = (element.meaning ?? '').trim();
|
|
68
|
+
const role = mapRole(element.role);
|
|
69
|
+
|
|
70
|
+
const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';
|
|
71
|
+
const key = toKey(label);
|
|
72
|
+
|
|
73
|
+
// Priority 1: data-testid
|
|
74
|
+
if (testId) {
|
|
75
|
+
return {
|
|
76
|
+
key,
|
|
77
|
+
expression: `page.getByTestId(${JSON.stringify(testId)})`,
|
|
78
|
+
elementType: role ?? 'element',
|
|
79
|
+
confidence: 'high',
|
|
80
|
+
source: 'testid',
|
|
81
|
+
description: meaning || testId,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Priority 2: input/combobox/textarea with label → getByLabel; with placeholder → getByPlaceholder
|
|
86
|
+
if (role === 'textbox' || role === 'combobox') {
|
|
87
|
+
if (ariaLabel) {
|
|
88
|
+
return {
|
|
89
|
+
key,
|
|
90
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
91
|
+
elementType: 'input',
|
|
92
|
+
confidence: 'high',
|
|
93
|
+
source: 'label',
|
|
94
|
+
description: ariaLabel,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (placeholder) {
|
|
98
|
+
return {
|
|
99
|
+
key,
|
|
100
|
+
expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
|
|
101
|
+
elementType: 'input',
|
|
102
|
+
confidence: 'high',
|
|
103
|
+
source: 'placeholder',
|
|
104
|
+
description: placeholder,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Priority 3: checkbox/radio → getByLabel
|
|
110
|
+
if ((role === 'checkbox' || role === 'radio') && ariaLabel) {
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
114
|
+
elementType: role,
|
|
115
|
+
confidence: 'high',
|
|
116
|
+
source: 'label',
|
|
117
|
+
description: ariaLabel,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Priority 4: button/link with meaningful name → getByRole
|
|
122
|
+
if ((role === 'button' || role === 'link') && meaning) {
|
|
123
|
+
return {
|
|
124
|
+
key,
|
|
125
|
+
expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
|
|
126
|
+
elementType: role,
|
|
127
|
+
confidence: 'high',
|
|
128
|
+
source: 'role-name',
|
|
129
|
+
description: meaning,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Priority 5: heading → getByRole heading
|
|
134
|
+
if (role === 'heading' && (meaning || text)) {
|
|
135
|
+
const name2 = meaning || text;
|
|
136
|
+
return {
|
|
137
|
+
key,
|
|
138
|
+
expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
|
|
139
|
+
elementType: 'heading',
|
|
140
|
+
confidence: 'medium',
|
|
141
|
+
source: 'heading',
|
|
142
|
+
description: name2,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Priority 6: existing CSS selector from crawler
|
|
147
|
+
if (element.selector && element.selector.length > 0) {
|
|
148
|
+
return {
|
|
149
|
+
key,
|
|
150
|
+
expression: `page.locator(${JSON.stringify(element.selector)})`,
|
|
151
|
+
elementType: role ?? 'element',
|
|
152
|
+
confidence: 'medium',
|
|
153
|
+
source: 'id',
|
|
154
|
+
description: meaning || text || element.selector,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Priority 7: id from attrs
|
|
159
|
+
if (id) {
|
|
160
|
+
return {
|
|
161
|
+
key,
|
|
162
|
+
expression: `page.locator(${JSON.stringify('#' + id)})`,
|
|
163
|
+
elementType: role ?? 'element',
|
|
164
|
+
confidence: 'medium',
|
|
165
|
+
source: 'id',
|
|
166
|
+
description: meaning || id,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Priority 8: fallback to text content
|
|
171
|
+
if (meaning || text) {
|
|
172
|
+
const t = meaning || text;
|
|
173
|
+
return {
|
|
174
|
+
key,
|
|
175
|
+
expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
|
|
176
|
+
elementType: role ?? 'text',
|
|
177
|
+
confidence: 'low',
|
|
178
|
+
source: 'text',
|
|
179
|
+
description: t,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
188
|
+
* Deduplicates by key — higher confidence wins.
|
|
189
|
+
*/
|
|
190
|
+
export function elementsToPlaywrightLocators(elements: Array<{
|
|
191
|
+
role?: string | null;
|
|
192
|
+
meaning?: string | null;
|
|
193
|
+
text?: string | null;
|
|
194
|
+
notes?: unknown;
|
|
195
|
+
selector?: string | null;
|
|
196
|
+
isInteractive?: boolean | null;
|
|
197
|
+
}>): PlaywrightLocator[] {
|
|
198
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
|
|
199
|
+
const byKey = new Map<string, PlaywrightLocator>();
|
|
200
|
+
|
|
201
|
+
for (const el of elements) {
|
|
202
|
+
const loc = elementToPlaywrightLocator(el);
|
|
203
|
+
if (!loc) continue;
|
|
204
|
+
const existing = byKey.get(loc.key);
|
|
205
|
+
if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
|
|
206
|
+
byKey.set(loc.key, loc);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Array.from(byKey.values());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
215
|
+
* Returns empty string if no locators.
|
|
216
|
+
*/
|
|
217
|
+
export function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string {
|
|
218
|
+
if (locators.length === 0) return '';
|
|
219
|
+
const lines = locators.map(l =>
|
|
220
|
+
` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`
|
|
221
|
+
);
|
|
222
|
+
return [
|
|
223
|
+
'EXTRACTED PAGE LOCATORS (use these EXACTLY — do not invent other selectors; these were verified against the live page):',
|
|
224
|
+
...lines,
|
|
225
|
+
].join('\n');
|
|
226
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -35,3 +35,5 @@ export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemet
|
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
36
|
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
37
|
export type { FailureContext } from './failure-analysis-agent.js';
|
|
38
|
+
export { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from './locator-extractor.js';
|
|
39
|
+
export type { PlaywrightLocator } from './locator-extractor.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export interface PlaywrightLocator {
|
|
6
|
+
key: string;
|
|
7
|
+
expression: string;
|
|
8
|
+
elementType: string;
|
|
9
|
+
confidence: 'high' | 'medium' | 'low';
|
|
10
|
+
source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
14
|
+
export declare function elementToPlaywrightLocator(element: {
|
|
15
|
+
role?: string | null;
|
|
16
|
+
meaning?: string | null;
|
|
17
|
+
text?: string | null;
|
|
18
|
+
notes?: unknown;
|
|
19
|
+
selector?: string | null;
|
|
20
|
+
}): PlaywrightLocator | null;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
23
|
+
* Deduplicates by key — higher confidence wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function elementsToPlaywrightLocators(elements: Array<{
|
|
26
|
+
role?: string | null;
|
|
27
|
+
meaning?: string | null;
|
|
28
|
+
text?: string | null;
|
|
29
|
+
notes?: unknown;
|
|
30
|
+
selector?: string | null;
|
|
31
|
+
isInteractive?: boolean | null;
|
|
32
|
+
}>): PlaywrightLocator[];
|
|
33
|
+
/**
|
|
34
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
35
|
+
* Returns empty string if no locators.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string;
|