@skyramp/mcp 0.4.3-rc.1 → 0.4.3
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/build/recommendation/answers.d.ts +5 -3
- package/build/recommendation/answers.js +5 -2
- package/build/recommendation/verifierContracts.d.ts +8 -0
- package/build/recommendation/verifierContracts.js +13 -0
- package/build/recommendation/verifiers/coverage.js +143 -4
- package/build/tools/code-refactor/removed-modules.d.ts +33 -0
- package/build/tools/code-refactor/removed-modules.js +61 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +27 -1
- package/build/tools/code-refactor/reuse-record-store.d.ts +5 -0
- package/build/tools/code-refactor/reuse-record-store.js +15 -1
- package/build/tools/code-refactor/reuse-state.d.ts +17 -4
- package/build/tools/code-refactor/reuse-state.js +67 -9
- package/build/tools/code-refactor/utils-verify-gates.js +39 -15
- package/build/tools/submitReportTool.js +47 -18
- package/build/tools/test-management/registerTestPlanTool.js +1 -1
- package/build/types/ReuseOutcome.d.ts +27 -0
- package/build/utils/utils-verify/allow.d.ts +1 -1
- package/build/utils/utils-verify/allow.js +15 -0
- package/build/utils/utils-verify/changed-lines.d.ts +48 -0
- package/build/utils/utils-verify/changed-lines.js +106 -0
- package/build/utils/utils-verify/head.d.ts +32 -7
- package/build/utils/utils-verify/head.js +55 -14
- package/build/utils/utils-verify/typecheck.d.ts +25 -4
- package/build/utils/utils-verify/typecheck.js +95 -13
- package/build/utils/utils-verify/verify.d.ts +24 -5
- package/build/utils/utils-verify/verify.js +156 -8
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
- package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +221 -233
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.ZYuEFHsi.js → index.uiPTFncY.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
|
@@ -55,6 +55,7 @@ var import_visualSnapshotTool = require("./visualSnapshotTool");
|
|
|
55
55
|
var import_gojsTool = require("./gojsTool");
|
|
56
56
|
var import_cssValue = require("./common/cssValue");
|
|
57
57
|
var import_visualSnapshot = require("./common/visualSnapshot");
|
|
58
|
+
var import_snapshotLocators = require("./snapshotLocators");
|
|
58
59
|
var import_tableAssertTool = require("./tableAssertTool");
|
|
59
60
|
var import_tableSelectorBuilder = require("./tableSelectorBuilder");
|
|
60
61
|
var import_types = require("./types");
|
|
@@ -82,11 +83,14 @@ function dropDynamicParentChain(chain) {
|
|
|
82
83
|
return m && (0, import_dynamicId.isDynamicId)(m[1]) ? null : chain;
|
|
83
84
|
}
|
|
84
85
|
const LIVE_READ_TIMEOUT_MS = 5e3;
|
|
86
|
+
const REF_RESOLVE_TIMEOUT_MS = 1e3;
|
|
85
87
|
const MAX_ANCESTOR_HOVERS = 8;
|
|
86
88
|
const ANCESTOR_WALK_BUDGET_MS = 1e4;
|
|
87
89
|
class TraceRecordingBackend {
|
|
88
90
|
// per-type baseline filename counter
|
|
89
91
|
constructor(options) {
|
|
92
|
+
// The attribute getByTestId() resolves against (configured or the default).
|
|
93
|
+
this._testIdAttribute = "data-testid";
|
|
90
94
|
this._trackedActions = [];
|
|
91
95
|
this._droppedActions = [];
|
|
92
96
|
this._initialized = false;
|
|
@@ -120,6 +124,7 @@ class TraceRecordingBackend {
|
|
|
120
124
|
] : void 0;
|
|
121
125
|
const headless = loadExtension?.length ? false : this._options.headless ?? (!!process.env.CI || import_os.default.platform() === "linux" && !process.env.DISPLAY);
|
|
122
126
|
const testIdAttribute = process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE?.trim() || this._options.testIdAttribute?.trim();
|
|
127
|
+
this._testIdAttribute = testIdAttribute || "data-testid";
|
|
123
128
|
const config = await (0, import_config.resolveConfig)({
|
|
124
129
|
...testIdAttribute ? { testIdAttribute } : {},
|
|
125
130
|
browser: {
|
|
@@ -490,10 +495,9 @@ Reloaded current page: ${currentUrl}
|
|
|
490
495
|
if (!params.ref)
|
|
491
496
|
return { content: [{ type: "text", text: '### Error\nsnapshotType "element" requires a ref (from the latest browser_snapshot).' }], isError: true };
|
|
492
497
|
const resolved = await this._resolveRefToLocator(params.ref, params.element ?? "");
|
|
493
|
-
if (
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
input.selector = resolved.selector;
|
|
498
|
+
if ("error" in resolved)
|
|
499
|
+
return resolved.error;
|
|
500
|
+
input.selector = resolved.parsed.selector;
|
|
497
501
|
}
|
|
498
502
|
if (params.snapshotType === "gojsDiagram") {
|
|
499
503
|
const diagramSelector = params.diagramSelector ?? "#divCenter";
|
|
@@ -531,31 +535,68 @@ Baseline "${input.filename}" (${params.snapshotType})${targetDesc} recorded; the
|
|
|
531
535
|
return (0, import_visualSnapshot.nextSnapshotFilename)(type, this._visualSnapshotCounters[type] + 1);
|
|
532
536
|
}
|
|
533
537
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
538
|
+
* The shared front of browser_assert and element visual snapshots: hover the
|
|
539
|
+
* ref for the generator's selector, parse it, take the snapshot, and prefer
|
|
540
|
+
* the snapshot's accessible name when the hover gave CSS (the Flutter-durable
|
|
541
|
+
* fallback). Hover records nothing and changes no data, but it does move the
|
|
542
|
+
* pointer: :hover styles and mouseenter handlers fire (the live css/class/
|
|
543
|
+
* attribute reads move it off again). `error` is the message body; each caller
|
|
544
|
+
* adds its own heading.
|
|
539
545
|
*/
|
|
540
|
-
async
|
|
546
|
+
async _resolveRef(ref, element) {
|
|
541
547
|
const hoverResult = await this._browserBackend.callTool("browser_hover", { element, ref });
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (!
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
548
|
+
let locatorExpr;
|
|
549
|
+
if (hoverResult.isError) {
|
|
550
|
+
const errText = hoverResult.content?.[0]?.type === "text" ? hoverResult.content[0].text : "";
|
|
551
|
+
const tab = /intercepts pointer events/.test(errText) ? this._browserBackend.context?.currentTab() : void 0;
|
|
552
|
+
let tabError = "";
|
|
553
|
+
const viaTab = tab ? await tab.refLocator({ ref, element }).then((r) => r.resolved, (e) => {
|
|
554
|
+
tabError = String(e?.message ?? e).split("\n")[0];
|
|
555
|
+
traceDebug(`Ref ${ref} could not be resolved without hovering either: ${tabError}`);
|
|
556
|
+
return void 0;
|
|
557
|
+
}) : void 0;
|
|
558
|
+
if (!viaTab)
|
|
559
|
+
return { error: `Could not resolve element ref=${ref}. ${errText}${tabError ? ` Resolving it without hovering failed too: ${tabError}` : ""}` };
|
|
560
|
+
traceDebug(`Hover on ref ${ref} is intercepted; resolved its locator without hovering: ${viaTab}`);
|
|
561
|
+
locatorExpr = viaTab;
|
|
562
|
+
} else {
|
|
563
|
+
const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
|
|
564
|
+
const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
565
|
+
if (!locatorMatch)
|
|
566
|
+
return { error: "Could not extract selector from hover result." };
|
|
567
|
+
locatorExpr = locatorMatch[1].trim();
|
|
557
568
|
}
|
|
558
|
-
|
|
569
|
+
const fromHover = this._codeToLocator(locatorExpr);
|
|
570
|
+
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
571
|
+
if (snapResult.isError) {
|
|
572
|
+
const errText = snapResult.content?.[0]?.type === "text" ? snapResult.content[0].text : "";
|
|
573
|
+
return { error: `Could not capture page snapshot to verify ref=${ref}. ${errText}` };
|
|
574
|
+
}
|
|
575
|
+
const snapLines = (snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "").split("\n");
|
|
576
|
+
const refIdx = snapLines.findIndex((l) => l.includes(`[ref=${ref}]`));
|
|
577
|
+
const refLine = refIdx >= 0 ? snapLines[refIdx] : "";
|
|
578
|
+
const parsed = this._preferSnapshotLocator(fromHover, refLine);
|
|
579
|
+
if (parsed !== fromHover)
|
|
580
|
+
traceDebug(`Resolved selector from snapshot accessible name (hover code was ${fromHover ? "CSS" : "unparseable"}: ${locatorExpr})`);
|
|
581
|
+
if (!parsed)
|
|
582
|
+
return { error: `Could not parse locator: ${locatorExpr}` };
|
|
583
|
+
return { parsed, snapLines, refIdx, refLine };
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The selector an element visual snapshot records for a ref: _resolveRef,
|
|
587
|
+
* then made to pick out the ref's own element as an assertion's is (the
|
|
588
|
+
* screenshot locator is strict at replay too) — a name repeated across a list
|
|
589
|
+
* is pinned, and one the live page says is another element is refused.
|
|
590
|
+
*/
|
|
591
|
+
async _resolveRefToLocator(ref, element) {
|
|
592
|
+
const resolved = await this._resolveRef(ref, element);
|
|
593
|
+
if ("error" in resolved)
|
|
594
|
+
return { error: { content: [{ type: "text", text: `### Error
|
|
595
|
+
${resolved.error} Take a fresh browser_snapshot and retry, or use snapshotType "region".` }], isError: true } };
|
|
596
|
+
const unique = await this._makeUnique(resolved.parsed, { snapLines: resolved.snapLines, ref, element, lineText: (0, import_snapshotLocators.snapshotLineText)(resolved.refLine) ?? "", exact: false });
|
|
597
|
+
if (unique.refusal)
|
|
598
|
+
return { error: unique.refusal };
|
|
599
|
+
return { parsed: unique.parsed };
|
|
559
600
|
}
|
|
560
601
|
/**
|
|
561
602
|
* Load a prior Skyramp trace and replay it against the live browser, honoring
|
|
@@ -1320,46 +1361,23 @@ For "${params.type}" assertions, "property" (the ${params.type === "css" ? "CSS
|
|
|
1320
1361
|
if ((params.type === "checked" || params.type === "disabled") && params.expected !== void 0 && params.expected !== "true" && params.expected !== "false")
|
|
1321
1362
|
return { content: [{ type: "text", text: `### Error
|
|
1322
1363
|
For "${params.type}" assertions, "expected" must be "true" or "false".` }], isError: true };
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
ref: params.ref
|
|
1326
|
-
});
|
|
1327
|
-
if (hoverResult.isError) {
|
|
1328
|
-
const errText = hoverResult.content?.[0]?.type === "text" ? hoverResult.content[0].text : "";
|
|
1329
|
-
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1330
|
-
Could not resolve element ref=${params.ref}. ${errText}` }], isError: true };
|
|
1331
|
-
}
|
|
1332
|
-
const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
|
|
1333
|
-
const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
1334
|
-
if (!locatorMatch) {
|
|
1364
|
+
const resolved = await this._resolveRef(params.ref, params.element);
|
|
1365
|
+
if ("error" in resolved)
|
|
1335
1366
|
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
1341
|
-
if (snapResult.isError) {
|
|
1342
|
-
const errText = snapResult.content?.[0]?.type === "text" ? snapResult.content[0].text : "";
|
|
1343
|
-
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1344
|
-
Could not capture page snapshot to verify the assertion. ${errText}` }], isError: true };
|
|
1345
|
-
}
|
|
1346
|
-
const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
1347
|
-
const snapLines = snapText.split("\n");
|
|
1348
|
-
const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
|
|
1349
|
-
const resolved = this._preferSnapshotLocator(parsed, refLine);
|
|
1350
|
-
if (resolved !== parsed)
|
|
1351
|
-
traceDebug(`Resolved assert selector from snapshot accessible name (hover code was ${parsed ? "CSS" : "unparseable"}: ${locatorExpr})`);
|
|
1352
|
-
parsed = resolved;
|
|
1353
|
-
if (!parsed) {
|
|
1354
|
-
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1355
|
-
Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
1356
|
-
}
|
|
1357
|
-
const inputLine = TraceRecordingBackend._isInputLine(refLine);
|
|
1367
|
+
${resolved.error}` }], isError: true };
|
|
1368
|
+
let parsed = resolved.parsed;
|
|
1369
|
+
const { snapLines, refIdx, refLine } = resolved;
|
|
1370
|
+
const inputLine = (0, import_snapshotLocators.isInputLine)(refLine);
|
|
1358
1371
|
const quotedName = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/)?.[1] ?? "";
|
|
1359
|
-
const lineText =
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1372
|
+
const lineText = (0, import_snapshotLocators.snapshotLineText)(refLine) ?? "";
|
|
1373
|
+
let elementText = inputLine ? quotedName : lineText;
|
|
1374
|
+
if (params.type === "text" && !inputLine) {
|
|
1375
|
+
const live = await this._liveText(params.ref);
|
|
1376
|
+
if (live !== void 0)
|
|
1377
|
+
elementText = live;
|
|
1378
|
+
}
|
|
1379
|
+
const readsValue = params.type === "value" || params.type === "text" && inputLine;
|
|
1380
|
+
const elementValue = readsValue ? await this._inputValue(snapLines, refIdx, params.ref) : "";
|
|
1363
1381
|
let passed = false;
|
|
1364
1382
|
let details = "";
|
|
1365
1383
|
let countSelector = "";
|
|
@@ -1372,7 +1390,9 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
|
1372
1390
|
const notReady = await this._requireLoadedPage(tab, "domcontentloaded");
|
|
1373
1391
|
if (notReady)
|
|
1374
1392
|
return notReady;
|
|
1375
|
-
const unique = await this._makeUnique(parsed, { snapLines, ref: params.ref, lineText, exact: false });
|
|
1393
|
+
const unique = await this._makeUnique(parsed, { snapLines, ref: params.ref, element: params.element, lineText, exact: false });
|
|
1394
|
+
if (unique.refusal)
|
|
1395
|
+
return unique.refusal;
|
|
1376
1396
|
parsed = unique.parsed;
|
|
1377
1397
|
const live = tab.page.locator(parsed.selector);
|
|
1378
1398
|
const kind = params.type;
|
|
@@ -1518,7 +1538,8 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
|
|
|
1518
1538
|
}
|
|
1519
1539
|
if (passed) {
|
|
1520
1540
|
const recordedExpected = params.type === "text" && params.substring !== false ? (0, import_utils.stableTextLead)(params.expected) : params.expected;
|
|
1521
|
-
const
|
|
1541
|
+
const tail = (0, import_snapshotLocators.leaf)(parsed.locator);
|
|
1542
|
+
const selectorText = tail.kind === "text" ? tail.body : tail.kind === "role" ? tail.options?.name ?? null : null;
|
|
1522
1543
|
const tautological = Boolean(params.type === "text" && selectorText && selectorText === recordedExpected);
|
|
1523
1544
|
const recordType = tautological ? "visible" : params.type;
|
|
1524
1545
|
if (recordType !== params.type) {
|
|
@@ -1526,11 +1547,13 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
|
|
|
1526
1547
|
details += ` Recorded as a presence check (toBeVisible): the locator already selects by "${recordedExpected}".`;
|
|
1527
1548
|
}
|
|
1528
1549
|
if (params.type !== "count" && params.type !== "css" && params.type !== "class" && params.type !== "attribute") {
|
|
1529
|
-
const unique = await this._makeUnique(parsed, { snapLines, ref: params.ref, lineText, exact: recordType === "visible" });
|
|
1550
|
+
const unique = await this._makeUnique(parsed, { snapLines, ref: params.ref, element: params.element, lineText, exact: recordType === "visible" });
|
|
1551
|
+
if (unique.refusal)
|
|
1552
|
+
return unique.refusal;
|
|
1530
1553
|
parsed = unique.parsed;
|
|
1531
1554
|
details += unique.note;
|
|
1532
|
-
if (recordType === "visible" &&
|
|
1533
|
-
const matches =
|
|
1555
|
+
if (recordType === "visible" && unique.verdict === "unverified") {
|
|
1556
|
+
const matches = (0, import_snapshotLocators.snapshotMatches)(snapLines, parsed.locator);
|
|
1534
1557
|
if (matches !== void 0 && matches > 1) {
|
|
1535
1558
|
return { content: [{ type: "text", text: `### Error
|
|
1536
1559
|
"${params.element}" is present, but ${parsed.selector} matches ${matches} elements on the page, so a presence check on it would be ambiguous at replay${unique.pinFailure ? `, and ${unique.pinFailure}` : ""}. Nothing was recorded. Assert it through a test id or a role with its exact name, or write the check in the generated test.` }], isError: true };
|
|
@@ -1780,8 +1803,8 @@ ${text}` }], isError: true });
|
|
|
1780
1803
|
let locator;
|
|
1781
1804
|
if (params.testId?.trim()) {
|
|
1782
1805
|
const testId = params.testId.trim();
|
|
1783
|
-
|
|
1784
|
-
|
|
1806
|
+
locator = { kind: "test-id", body: testId, options: this._testIdAttribute === "data-testid" ? {} : { attribute: this._testIdAttribute } };
|
|
1807
|
+
selector = (0, import_snapshotLocators.renderSelector)(locator);
|
|
1785
1808
|
} else if (params.role?.trim()) {
|
|
1786
1809
|
const role = params.role.trim();
|
|
1787
1810
|
if (!TraceRecordingBackend.ASSERTABLE_ROLES.has(role))
|
|
@@ -1834,40 +1857,30 @@ ${text}` }], isError: true });
|
|
|
1834
1857
|
${details}` }] };
|
|
1835
1858
|
}
|
|
1836
1859
|
/** Convert a Playwright locator expression to a selector + locator object for JSONL. */
|
|
1860
|
+
/**
|
|
1861
|
+
* The recorder locator for a hover's generated locator code, e.g.
|
|
1862
|
+
* `getByRole('listitem').filter({ hasText: 'Walk the dog' }).getByLabel('Toggle Todo')`.
|
|
1863
|
+
* playwright-core's own parser turns the code into its internal selector —
|
|
1864
|
+
* chains, `.filter({ hasText })`, options and string escapes included — and
|
|
1865
|
+
* locatorFromSelector maps each part to a kind the exporter and codegen carry.
|
|
1866
|
+
* Null when the code does not parse, is an aria-ref (no durable selector), or
|
|
1867
|
+
* has a part that cannot be carried (xpath, `has`, a frame hop, a regex): the
|
|
1868
|
+
* caller then falls back to the snapshot's accessible name. The old
|
|
1869
|
+
* per-method regexes matched anywhere in the expression, so a chain was cut
|
|
1870
|
+
* to its first link (`getByRole('listitem')` for a checkbox inside a row).
|
|
1871
|
+
*/
|
|
1837
1872
|
_codeToLocator(expr) {
|
|
1838
|
-
const
|
|
1839
|
-
if (
|
|
1840
|
-
|
|
1841
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1842
|
-
}
|
|
1843
|
-
const roleMatch = expr.match(/getByRole\(\s*['"]([^'"]+)['"](?:\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"][^}]*\})?\s*\)/);
|
|
1844
|
-
if (roleMatch) {
|
|
1845
|
-
const locator = { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, ...roleMatch[2] ? { name: roleMatch[2] } : {} } };
|
|
1846
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1847
|
-
}
|
|
1848
|
-
const labelMatch = expr.match(/getByLabel\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1849
|
-
if (labelMatch) {
|
|
1850
|
-
const locator = { kind: "label", body: labelMatch[1], options: { exact: false } };
|
|
1851
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1852
|
-
}
|
|
1853
|
-
const textMatch = expr.match(/getByText\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1854
|
-
if (textMatch) {
|
|
1855
|
-
const locator = { kind: "text", body: textMatch[1], options: { exact: false } };
|
|
1856
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1857
|
-
}
|
|
1858
|
-
const ariaRefMatch = expr.match(/locator\(\s*['"]aria-ref=(\w+)['"]\s*\)/);
|
|
1859
|
-
if (ariaRefMatch) {
|
|
1873
|
+
const selector = (0, import_utils.locatorOrSelectorAsSelector)("javascript", expr, this._testIdAttribute);
|
|
1874
|
+
if (!selector) {
|
|
1875
|
+
traceDebug(`Hover code did not parse as a locator (or did not round-trip): ${expr}`);
|
|
1860
1876
|
return null;
|
|
1861
1877
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
return null;
|
|
1878
|
+
if (selector.startsWith("aria-ref="))
|
|
1879
|
+
return null;
|
|
1880
|
+
const locator = (0, import_snapshotLocators.locatorFromSelector)(selector);
|
|
1881
|
+
if (!locator)
|
|
1882
|
+
traceDebug(`Hover code has a part the trace format cannot carry: ${expr} \u2192 ${selector}`);
|
|
1883
|
+
return locator ? { selector: (0, import_snapshotLocators.renderSelector)(locator), locator } : null;
|
|
1871
1884
|
}
|
|
1872
1885
|
/**
|
|
1873
1886
|
* When hover resolves to a text-based locator (e.g. getByText("$849.98")),
|
|
@@ -1877,7 +1890,7 @@ ${details}` }] };
|
|
|
1877
1890
|
*/
|
|
1878
1891
|
async _improveTextSelector(snapLines, ref, original) {
|
|
1879
1892
|
const textBody = original.locator.body;
|
|
1880
|
-
const occurrences =
|
|
1893
|
+
const occurrences = (0, import_snapshotLocators.snapshotMatches)(snapLines, original.locator) ?? 0;
|
|
1881
1894
|
if (occurrences <= 1)
|
|
1882
1895
|
return null;
|
|
1883
1896
|
traceDebug(`Text "${textBody}" appears ${occurrences} times in snapshot \u2014 looking for parent test-id`);
|
|
@@ -1918,10 +1931,12 @@ ${details}` }] };
|
|
|
1918
1931
|
const locator = {
|
|
1919
1932
|
kind: "test-id",
|
|
1920
1933
|
body: testidMatch[1],
|
|
1921
|
-
|
|
1934
|
+
// Under a configured testIdAttribute (data-cy) the rendered pin
|
|
1935
|
+
// selector must use it, or the live pin finds nothing.
|
|
1936
|
+
options: this._testIdAttribute === "data-testid" ? {} : { attribute: this._testIdAttribute },
|
|
1922
1937
|
next: { kind: "text", body: textBody, options: { exact: !!original.locator.options?.exact } }
|
|
1923
1938
|
};
|
|
1924
|
-
const chainedSelector =
|
|
1939
|
+
const chainedSelector = (0, import_snapshotLocators.renderSelector)(locator);
|
|
1925
1940
|
traceDebug(`Improved to chained selector: ${chainedSelector}`);
|
|
1926
1941
|
return { selector: chainedSelector, locator };
|
|
1927
1942
|
}
|
|
@@ -1934,81 +1949,83 @@ ${details}` }] };
|
|
|
1934
1949
|
}
|
|
1935
1950
|
return null;
|
|
1936
1951
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
return selector.startsWith("#") && (0, import_dynamicId.isDynamicId)(selector.slice(1));
|
|
1952
|
+
/** The ref's value as Playwright's inputValue() reads it (which retargets a
|
|
1953
|
+
* `<label>` ref to its control), or undefined on any failure: not an
|
|
1954
|
+
* input/textarea/select, a ref that does not resolve within
|
|
1955
|
+
* REF_RESOLVE_TIMEOUT_MS, a closed page. The reason goes to the trace debug log. */
|
|
1956
|
+
/** What an input holds, as toHaveValue compares at replay: read live from the
|
|
1957
|
+
* element, and from the snapshot (inline, or the child line under a
|
|
1958
|
+
* placeholder) when the live read is not available. */
|
|
1959
|
+
async _inputValue(snapLines, refIdx, ref) {
|
|
1960
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
1961
|
+
const live = page ? await TraceRecordingBackend._liveInputValue(page, ref) : void 0;
|
|
1962
|
+
return live ?? (0, import_snapshotLocators.snapshotInputValue)(snapLines, refIdx);
|
|
1949
1963
|
}
|
|
1950
|
-
/**
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1964
|
+
/** The ref's rendered text as toContainText compares it — Playwright's
|
|
1965
|
+
* injected elementText: a submit/button input's value, shadow-root text
|
|
1966
|
+
* included, script/style/noscript and <head> skipped; whitespace normalized —
|
|
1967
|
+
* or undefined when the page or ref is unavailable. */
|
|
1968
|
+
async _liveText(ref) {
|
|
1969
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
1970
|
+
if (!page)
|
|
1971
|
+
return void 0;
|
|
1972
|
+
try {
|
|
1973
|
+
const text = await page.locator(`aria-ref=${ref}`).evaluate((root) => {
|
|
1974
|
+
const skip = (n) => n.nodeName === "SCRIPT" || n.nodeName === "NOSCRIPT" || n.nodeName === "STYLE" || !!(n.ownerDocument?.head && n.ownerDocument.head.contains(n));
|
|
1975
|
+
const read = (n) => {
|
|
1976
|
+
if (skip(n))
|
|
1977
|
+
return "";
|
|
1978
|
+
if (n instanceof HTMLInputElement && (n.type === "submit" || n.type === "button"))
|
|
1979
|
+
return n.value;
|
|
1980
|
+
let full = "";
|
|
1981
|
+
for (let c = n.firstChild; c; c = c.nextSibling) {
|
|
1982
|
+
if (c.nodeType === Node.TEXT_NODE)
|
|
1983
|
+
full += c.nodeValue || "";
|
|
1984
|
+
else if (c.nodeType === Node.ELEMENT_NODE)
|
|
1985
|
+
full += read(c);
|
|
1986
|
+
}
|
|
1987
|
+
if (n.shadowRoot)
|
|
1988
|
+
full += read(n.shadowRoot);
|
|
1989
|
+
return full;
|
|
1990
|
+
};
|
|
1991
|
+
return read(root);
|
|
1992
|
+
}, void 0, { timeout: REF_RESOLVE_TIMEOUT_MS });
|
|
1993
|
+
return text.replace(/[\u200b\u00ad]/g, "").trim().replace(/\s+/g, " ");
|
|
1994
|
+
} catch (e) {
|
|
1995
|
+
traceDebug(`Live text read of ref ${ref} failed, using the snapshot: ${String(e?.message ?? e).split("\n")[0]}`);
|
|
1996
|
+
return void 0;
|
|
1978
1997
|
}
|
|
1979
|
-
return locator.next ? `${head} >> ${TraceRecordingBackend._renderSelector(locator.next)}` : head;
|
|
1980
1998
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
/** The chain with its leaf made exact, when the element's whole name/text is
|
|
1989
|
-
* exactly the leaf's (so a sibling whose name merely contains it cannot match).
|
|
1990
|
-
* Returns the locator unchanged otherwise, or for a leaf with no text. */
|
|
1991
|
-
static _exactLeaf(parsed, fullText) {
|
|
1992
|
-
const copy = (l) => ({ ...l, options: { ...l.options }, ...l.next ? { next: copy(l.next) } : {} });
|
|
1993
|
-
const locator = copy(parsed.locator);
|
|
1994
|
-
const leaf = TraceRecordingBackend._leaf(locator);
|
|
1995
|
-
const leafText = leaf.kind === "text" ? leaf.body : leaf.kind === "role" ? leaf.options?.name : void 0;
|
|
1996
|
-
if (typeof leafText !== "string" || leafText !== fullText)
|
|
1997
|
-
return parsed;
|
|
1998
|
-
leaf.options = { ...leaf.options, exact: true };
|
|
1999
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1999
|
+
static async _liveInputValue(page, ref) {
|
|
2000
|
+
try {
|
|
2001
|
+
return await page.locator(`aria-ref=${ref}`).inputValue({ timeout: REF_RESOLVE_TIMEOUT_MS });
|
|
2002
|
+
} catch (e) {
|
|
2003
|
+
traceDebug(`Live value read of ref ${ref} failed, using the snapshot: ${String(e?.message ?? e).split("\n")[0]}`);
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2000
2006
|
}
|
|
2001
2007
|
/**
|
|
2002
2008
|
* Make a locator pick out the ref's own element: a presence check's leaf made
|
|
2003
2009
|
* exact (when asked), the SKYR-4263 test-id walk for an ambiguous text
|
|
2004
2010
|
* locator, and — if the live page still finds more than one element — a pin
|
|
2005
2011
|
* to the ref's element by position (`>> nth=K`, the form the exporter and
|
|
2006
|
-
* codegen already carry for clicks; SKYR-4505).
|
|
2007
|
-
*
|
|
2012
|
+
* codegen already carry for clicks; SKYR-4505).
|
|
2013
|
+
*
|
|
2014
|
+
* `verdict`: `unique` (the live page found one match, the ref's own),
|
|
2015
|
+
* `pinned`, or `unverified` (no page, or the lookup could not place the ref;
|
|
2016
|
+
* `pinFailure` says why). `refusal` is set when what would be read or recorded
|
|
2017
|
+
* is not this element: the locator matches nothing or only other elements
|
|
2018
|
+
* (any kind), or it has a CSS link and could not be placed at all (CSS has no
|
|
2019
|
+
* snapshot count to fall back on — a `row >> td` chain included). A role or
|
|
2020
|
+
* text locator that could not be placed goes on unverified: a single-link
|
|
2021
|
+
* `visible` record is then counted against the snapshot; other kinds, chains
|
|
2022
|
+
* and the element visual snapshot path record it unpinned, as they did before
|
|
2023
|
+
* the pin existed. A ref inside an iframe (`f<N>e<M>`) gets its own refusal: the
|
|
2024
|
+
* lookup counts in the main frame, and the recorded selector has no frame.
|
|
2008
2025
|
*/
|
|
2009
2026
|
async _makeUnique(parsed, opts) {
|
|
2010
2027
|
if (opts.exact)
|
|
2011
|
-
parsed =
|
|
2028
|
+
parsed = (0, import_snapshotLocators.exactLeaf)(parsed, opts.lineText);
|
|
2012
2029
|
if (parsed.locator.kind === "text") {
|
|
2013
2030
|
const improved = await this._improveTextSelector(opts.snapLines, opts.ref, parsed);
|
|
2014
2031
|
if (improved) {
|
|
@@ -2018,28 +2035,38 @@ ${details}` }] };
|
|
|
2018
2035
|
}
|
|
2019
2036
|
const page = this._browserBackend.context?.currentTab()?.page;
|
|
2020
2037
|
if (!page)
|
|
2021
|
-
return { parsed, note: "",
|
|
2038
|
+
return { parsed, note: "", verdict: "unverified" };
|
|
2022
2039
|
const pin = await TraceRecordingBackend._refIndexAmong(page, opts.ref, parsed.selector);
|
|
2023
2040
|
if ("index" in pin) {
|
|
2024
|
-
if (pin.count
|
|
2025
|
-
return { parsed, note: "",
|
|
2041
|
+
if (pin.count === 1)
|
|
2042
|
+
return { parsed, note: "", verdict: "unique" };
|
|
2026
2043
|
const unpinned = parsed.selector;
|
|
2027
|
-
parsed =
|
|
2044
|
+
parsed = (0, import_snapshotLocators.withNth)(parsed, pin.index);
|
|
2028
2045
|
traceDebug(`${unpinned} matches ${pin.count} elements; pinned to the ref's own, nth=${pin.index}`);
|
|
2029
|
-
return { parsed, note: ` Recorded as ${parsed.selector}: ${unpinned} matches ${pin.count} elements, so the assertion is pinned to this one by position.`,
|
|
2046
|
+
return { parsed, note: ` Recorded as ${parsed.selector}: ${unpinned} matches ${pin.count} elements, so the assertion is pinned to this one by position.`, verdict: "pinned" };
|
|
2030
2047
|
}
|
|
2031
2048
|
traceDebug(`Could not pin ref ${opts.ref} among ${parsed.selector}: ${pin.reason}${pin.detail ? ` (${pin.detail})` : ""}`);
|
|
2032
|
-
const pinFailure = pin.reason === "unresolved" ? `ref ${opts.ref} no longer resolves \u2014 take a new snapshot and use its ref` : pin.reason === "not-among-matches" ? `ref ${opts.ref} is not one of them \u2014 the hover locator points elsewhere; try the ref of the element that carries the property` : `placing ref ${opts.ref} among them failed (${pin.detail})`;
|
|
2033
|
-
|
|
2049
|
+
const pinFailure = pin.reason === "unresolved" ? `ref ${opts.ref} no longer resolves \u2014 take a new snapshot and use its ref` : pin.reason === "no-match" ? `it matches nothing on the live page` : pin.reason === "not-among-matches" ? `ref ${opts.ref} is not one of them \u2014 the hover locator points elsewhere; try the ref of the element that carries the property` : `placing ref ${opts.ref} among them failed (${pin.detail})`;
|
|
2050
|
+
const notThisElement = pin.reason === "no-match" || pin.reason === "not-among-matches";
|
|
2051
|
+
let hasCss = false;
|
|
2052
|
+
for (let link = parsed.locator; link; link = link.next)
|
|
2053
|
+
hasCss ||= link.kind === "default";
|
|
2054
|
+
if (!notThisElement && !hasCss)
|
|
2055
|
+
return { parsed, note: "", verdict: "unverified", pinFailure };
|
|
2056
|
+
const text = /^f\d+e\d+$/.test(opts.ref) ? `### Error
|
|
2057
|
+
"${opts.element}" (ref ${opts.ref}) is inside an iframe, and ${parsed.selector} addresses the main page only, so it cannot pick the element out at replay. Nothing was recorded. Assert it in the generated test through the frame (page.frameLocator(\u2026) or locator.contentFrame()).` : `### Error
|
|
2058
|
+
${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFailure}. Nothing was recorded. Assert it through a test id or a role with its name, or on the ref of the element that carries it.`;
|
|
2059
|
+
return { parsed, note: "", verdict: "unverified", pinFailure, refusal: { content: [{ type: "text", text }], isError: true } };
|
|
2034
2060
|
}
|
|
2035
2061
|
/** How many elements `selector` matches on the live page and where the ref's
|
|
2036
2062
|
* own element sits among them — one evaluateAll — or why it cannot tell: the
|
|
2037
|
-
* ref no longer resolves (stale snapshot),
|
|
2038
|
-
* the
|
|
2063
|
+
* ref no longer resolves (stale snapshot), the selector matches nothing, the
|
|
2064
|
+
* ref is not one of the matches (a single match included), or the lookup
|
|
2065
|
+
* failed. An index is returned only when it is the ref's own element's. */
|
|
2039
2066
|
static async _refIndexAmong(page, ref, selector) {
|
|
2040
2067
|
let handle;
|
|
2041
2068
|
try {
|
|
2042
|
-
handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout:
|
|
2069
|
+
handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout: REF_RESOLVE_TIMEOUT_MS });
|
|
2043
2070
|
} catch (e) {
|
|
2044
2071
|
return { reason: "unresolved", detail: String(e?.message ?? e).split("\n")[0] };
|
|
2045
2072
|
}
|
|
@@ -2047,8 +2074,8 @@ ${details}` }] };
|
|
|
2047
2074
|
return { reason: "unresolved" };
|
|
2048
2075
|
try {
|
|
2049
2076
|
const { count, index } = await page.locator(selector).evaluateAll((els, target) => ({ count: els.length, index: els.indexOf(target) }), handle);
|
|
2050
|
-
if (count
|
|
2051
|
-
return {
|
|
2077
|
+
if (!count)
|
|
2078
|
+
return { reason: "no-match" };
|
|
2052
2079
|
return index >= 0 ? { index, count } : { reason: "not-among-matches" };
|
|
2053
2080
|
} catch (e) {
|
|
2054
2081
|
return { reason: "error", detail: String(e?.message ?? e).split("\n")[0] };
|
|
@@ -2057,65 +2084,26 @@ ${details}` }] };
|
|
|
2057
2084
|
});
|
|
2058
2085
|
}
|
|
2059
2086
|
}
|
|
2060
|
-
/** The locator with a trailing `nth` link, re-rendered. */
|
|
2061
|
-
static _withNth(parsed, index) {
|
|
2062
|
-
const copy = (l) => ({ ...l, options: { ...l.options }, ...l.next ? { next: copy(l.next) } : {} });
|
|
2063
|
-
const locator = copy(parsed.locator);
|
|
2064
|
-
TraceRecordingBackend._leaf(locator).next = { kind: "nth", body: String(index), options: { attrs: [], exact: false } };
|
|
2065
|
-
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2066
|
-
}
|
|
2067
|
-
/**
|
|
2068
|
-
* How many snapshot lines a single role or text locator matches, with the
|
|
2069
|
-
* locator's own semantics: exact is equality, otherwise case-insensitive
|
|
2070
|
-
* substring (what `"X"i` does at replay). Undefined for a shape it cannot
|
|
2071
|
-
* count from the snapshot (a chain, CSS, a label).
|
|
2072
|
-
*/
|
|
2073
|
-
static _snapshotMatches(snapLines, locator) {
|
|
2074
|
-
if (locator.next)
|
|
2075
|
-
return void 0;
|
|
2076
|
-
const text = locator.kind === "text" ? locator.body : locator.kind === "role" ? locator.options?.name : void 0;
|
|
2077
|
-
if (typeof text !== "string")
|
|
2078
|
-
return void 0;
|
|
2079
|
-
const exact = !!locator.options?.exact;
|
|
2080
|
-
const same = (v) => v !== null && (exact ? v === text : v.toLowerCase().includes(text.toLowerCase()));
|
|
2081
|
-
return snapLines.filter((l) => {
|
|
2082
|
-
if (locator.kind === "role") {
|
|
2083
|
-
const m = l.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
|
|
2084
|
-
return !!m && m[1] === locator.body && same(m[2]);
|
|
2085
|
-
}
|
|
2086
|
-
return same(TraceRecordingBackend._snapshotLineText(l));
|
|
2087
|
-
}).length;
|
|
2088
|
-
}
|
|
2089
2087
|
/**
|
|
2090
2088
|
* The locator to record for a ref: the hover's, unless it is missing or CSS
|
|
2091
2089
|
* and the snapshot names the element better. A bare role (no name) is no
|
|
2092
|
-
* better than the hover's CSS unless that CSS is a framework-generated id
|
|
2090
|
+
* better than the hover's CSS unless that CSS is a framework-generated id,
|
|
2091
|
+
* and nothing in the snapshot beats a data-* test hook on the element itself:
|
|
2092
|
+
* a role name repeated across a list ("View Details" on every card) would
|
|
2093
|
+
* otherwise replace the one handle that picks this card out.
|
|
2093
2094
|
* Shared by browser_assert and element visual snapshots so both paths agree.
|
|
2094
2095
|
*/
|
|
2095
2096
|
_preferSnapshotLocator(parsed, refLine) {
|
|
2096
|
-
if (parsed && parsed.locator.kind !== "default")
|
|
2097
|
+
if (parsed && (parsed.locator.kind !== "default" || (0, import_snapshotLocators.isTestHookCss)(parsed.selector)))
|
|
2097
2098
|
return parsed;
|
|
2098
2099
|
const fromSnapshot = this._extractLocatorForRef(refLine);
|
|
2099
2100
|
if (!fromSnapshot || !parsed)
|
|
2100
2101
|
return fromSnapshot ?? parsed;
|
|
2101
2102
|
const bareRole = fromSnapshot.locator.kind === "role" && typeof fromSnapshot.locator.options?.name !== "string";
|
|
2102
|
-
if (bareRole && !
|
|
2103
|
+
if (bareRole && !(0, import_snapshotLocators.isVolatileCss)(parsed.selector))
|
|
2103
2104
|
return parsed;
|
|
2104
2105
|
return fromSnapshot;
|
|
2105
2106
|
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Text value of an aria-snapshot line, in either form the renderer emits:
|
|
2108
|
-
* `- generic "foo" [ref=e1]` (quoted accessible name) or
|
|
2109
|
-
* `- generic [ref=e1]: foo` (colon-delimited text for nodes with no name).
|
|
2110
|
-
* Returns null for a line that carries no text.
|
|
2111
|
-
*/
|
|
2112
|
-
static _snapshotLineText(line) {
|
|
2113
|
-
const quoted = line.match(/^\s*-\s*\w+\s+"([^"]*)"/);
|
|
2114
|
-
if (quoted)
|
|
2115
|
-
return quoted[1];
|
|
2116
|
-
const colon = line.match(/\[ref=\w+\](?:\s*\[[^\]]*\])*\s*:\s*(.+?)\s*$/);
|
|
2117
|
-
return colon?.[1]?.trim() || null;
|
|
2118
|
-
}
|
|
2119
2107
|
static {
|
|
2120
2108
|
/** Extract selector and locator info from a snapshot line for assertion tracking. */
|
|
2121
2109
|
// Roles that map to valid Playwright getByRole() selectors.
|
|
@@ -2168,26 +2156,26 @@ ${details}` }] };
|
|
|
2168
2156
|
const testidMatch = refLine.match(/testid="([^"]+)"/);
|
|
2169
2157
|
if (testidMatch) {
|
|
2170
2158
|
const locator = { kind: "test-id", body: testidMatch[1], options: {} };
|
|
2171
|
-
return { selector:
|
|
2159
|
+
return { selector: (0, import_snapshotLocators.renderSelector)(locator), locator };
|
|
2172
2160
|
}
|
|
2173
2161
|
const roleMatch = refLine.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
|
|
2174
2162
|
if (roleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(roleMatch[1])) {
|
|
2175
2163
|
const locator = { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, name: (0, import_utils.stableTextLead)(roleMatch[2]) } };
|
|
2176
|
-
return { selector:
|
|
2164
|
+
return { selector: (0, import_snapshotLocators.renderSelector)(locator), locator };
|
|
2177
2165
|
}
|
|
2178
2166
|
if (roleMatch && roleMatch[2]) {
|
|
2179
2167
|
const locator = { kind: "text", body: (0, import_utils.stableTextLead)(roleMatch[2]), options: { exact: false } };
|
|
2180
|
-
return { selector:
|
|
2168
|
+
return { selector: (0, import_snapshotLocators.renderSelector)(locator), locator };
|
|
2181
2169
|
}
|
|
2182
2170
|
const colonTextMatch = refLine.match(/\[ref=\w+\](?:\s*\[[^\]]*\])*\s*:\s*(.+?)\s*$/);
|
|
2183
|
-
if (colonTextMatch && colonTextMatch[1].trim() && !
|
|
2184
|
-
const locator = { kind: "text", body: (0, import_utils.stableTextLead)(colonTextMatch[1].trim()), options: { exact: false } };
|
|
2185
|
-
return { selector:
|
|
2171
|
+
if (colonTextMatch && colonTextMatch[1].trim() && !(0, import_snapshotLocators.isInputLine)(refLine)) {
|
|
2172
|
+
const locator = { kind: "text", body: (0, import_utils.stableTextLead)((0, import_snapshotLocators.unquoteYaml)(colonTextMatch[1].trim())), options: { exact: false } };
|
|
2173
|
+
return { selector: (0, import_snapshotLocators.renderSelector)(locator), locator };
|
|
2186
2174
|
}
|
|
2187
2175
|
const bareRoleMatch = refLine.match(/^\s*-\s*(\w+)(?:\s+\[[^\]]*\])*\s+\[ref=\w+\]/);
|
|
2188
2176
|
if (bareRoleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(bareRoleMatch[1])) {
|
|
2189
2177
|
const locator = { kind: "role", body: bareRoleMatch[1], options: { attrs: [], exact: false } };
|
|
2190
|
-
return { selector:
|
|
2178
|
+
return { selector: (0, import_snapshotLocators.renderSelector)(locator), locator };
|
|
2191
2179
|
}
|
|
2192
2180
|
return null;
|
|
2193
2181
|
}
|
|
@@ -2593,7 +2581,7 @@ ${details}` }] };
|
|
|
2593
2581
|
return null;
|
|
2594
2582
|
let stripped = false;
|
|
2595
2583
|
try {
|
|
2596
|
-
const handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout:
|
|
2584
|
+
const handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout: REF_RESOLVE_TIMEOUT_MS });
|
|
2597
2585
|
if (!handle)
|
|
2598
2586
|
return null;
|
|
2599
2587
|
try {
|