@skyramp/mcp 0.4.2 → 0.4.3-rc.1
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/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +91 -29
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -547,14 +547,12 @@ Baseline "${input.filename}" (${params.snapshotType})${targetDesc} recorded; the
|
|
|
547
547
|
return null;
|
|
548
548
|
const locatorExpr = locatorMatch[1].trim();
|
|
549
549
|
let parsed = this._codeToLocator(locatorExpr);
|
|
550
|
-
if (!parsed || parsed.locator.kind === "
|
|
550
|
+
if (!parsed || parsed.locator.kind === "default") {
|
|
551
551
|
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
552
552
|
if (!snapResult.isError) {
|
|
553
553
|
const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
554
554
|
const refLine = snapText.split("\n").find((l) => l.includes(`[ref=${ref}]`)) || "";
|
|
555
|
-
|
|
556
|
-
if (fromSnapshot)
|
|
557
|
-
parsed = fromSnapshot;
|
|
555
|
+
parsed = this._preferSnapshotLocator(parsed, refLine);
|
|
558
556
|
}
|
|
559
557
|
}
|
|
560
558
|
return parsed;
|
|
@@ -1348,19 +1346,18 @@ Could not capture page snapshot to verify the assertion. ${errText}` }], isError
|
|
|
1348
1346
|
const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
1349
1347
|
const snapLines = snapText.split("\n");
|
|
1350
1348
|
const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
parsed = fromSnapshot;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
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;
|
|
1358
1353
|
if (!parsed) {
|
|
1359
1354
|
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1360
1355
|
Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
1361
1356
|
}
|
|
1362
|
-
const
|
|
1363
|
-
const
|
|
1357
|
+
const inputLine = TraceRecordingBackend._isInputLine(refLine);
|
|
1358
|
+
const quotedName = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/)?.[1] ?? "";
|
|
1359
|
+
const lineText = TraceRecordingBackend._snapshotLineText(refLine) ?? "";
|
|
1360
|
+
const elementText = inputLine ? quotedName : lineText;
|
|
1364
1361
|
const valueMatch = refLine.match(/\]:\s*(.+)$/);
|
|
1365
1362
|
const elementValue = valueMatch?.[1]?.trim() || "";
|
|
1366
1363
|
let passed = false;
|
|
@@ -1375,13 +1372,8 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
|
1375
1372
|
const notReady = await this._requireLoadedPage(tab, "domcontentloaded");
|
|
1376
1373
|
if (notReady)
|
|
1377
1374
|
return notReady;
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (improved) {
|
|
1381
|
-
traceDebug(`Improved ambiguous text selector before live read: ${improved.selector}`);
|
|
1382
|
-
parsed = improved;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1375
|
+
const unique = await this._makeUnique(parsed, { snapLines, ref: params.ref, lineText, exact: false });
|
|
1376
|
+
parsed = unique.parsed;
|
|
1385
1377
|
const live = tab.page.locator(parsed.selector);
|
|
1386
1378
|
const kind = params.type;
|
|
1387
1379
|
const name = kind === "class" ? "" : params.property.trim();
|
|
@@ -1440,6 +1432,18 @@ Could not read the un-hovered state of "${params.element}": the pointer could no
|
|
|
1440
1432
|
const classes = (reading.value ?? "").split(" ").filter(Boolean);
|
|
1441
1433
|
passed = classes.includes(className);
|
|
1442
1434
|
details = passed ? `Class assertion passed: "${params.element}" carries class "${className}".` : `Class assertion FAILED: "${params.element}" has classes [${classes.join(" ")}], expected "${className}".`;
|
|
1435
|
+
if (!passed) {
|
|
1436
|
+
const depth = await live.evaluate((el, cls) => {
|
|
1437
|
+
let up = el.parentElement;
|
|
1438
|
+
for (let d = 1; up && d <= 3; d++, up = up.parentElement) {
|
|
1439
|
+
if (up.classList.contains(cls))
|
|
1440
|
+
return d;
|
|
1441
|
+
}
|
|
1442
|
+
return 0;
|
|
1443
|
+
}, className, { timeout: LIVE_READ_TIMEOUT_MS }).catch(() => 0);
|
|
1444
|
+
if (depth)
|
|
1445
|
+
details += ` "${className}" is on its ${depth === 1 ? "parent" : `ancestor ${depth} levels up`}: assert it on that element \u2014 with browser_assert on its ref if the snapshot has one, otherwise in the generated test.`;
|
|
1446
|
+
}
|
|
1443
1447
|
} else {
|
|
1444
1448
|
const actual = reading.value;
|
|
1445
1449
|
passed = actual !== null && actual === params.expected;
|
|
@@ -1450,16 +1454,18 @@ Could not read the un-hovered state of "${params.element}": the pointer could no
|
|
|
1450
1454
|
const msg = String(e?.message ?? e);
|
|
1451
1455
|
if (/strict mode violation/i.test(msg))
|
|
1452
1456
|
return { content: [{ type: "text", text: `### Error
|
|
1453
|
-
Target ${parsed.selector} matches multiple elements, so the ${kind} read is ambiguous. Use a more specific target (a data-testid, or role + exact name). ${msg.split("\n")[0]}` }], isError: true };
|
|
1457
|
+
Target ${parsed.selector} matches multiple elements, so the ${kind} read is ambiguous${unique.pinFailure ? `, and ${unique.pinFailure}` : ""}. Use a more specific target (a data-testid, or role + exact name). ${msg.split("\n")[0]}` }], isError: true };
|
|
1454
1458
|
if (e?.name === "TimeoutError")
|
|
1455
1459
|
return { content: [{ type: "text", text: `### Error
|
|
1456
1460
|
"${params.element}" (${parsed.selector}) did not resolve within ${LIVE_READ_TIMEOUT_MS} ms. Re-snapshot and retry with a fresh ref.` }], isError: true };
|
|
1457
1461
|
return { content: [{ type: "text", text: `### Error
|
|
1458
1462
|
Could not read ${kind} of "${params.element}": ${msg}` }], isError: true };
|
|
1459
1463
|
}
|
|
1464
|
+
if (passed)
|
|
1465
|
+
details += unique.note;
|
|
1460
1466
|
} else if (params.type === "text") {
|
|
1461
|
-
passed = elementText.includes(params.expected)
|
|
1462
|
-
details = passed ? `Text assertion passed: "${params.element}" contains "${params.expected}".` : `Text assertion FAILED: "${params.element}" has text "${elementText}"${elementValue ? `
|
|
1467
|
+
passed = elementText.includes(params.expected);
|
|
1468
|
+
details = passed ? `Text assertion passed: "${params.element}" contains "${params.expected}".` : `Text assertion FAILED: "${params.element}" has text "${elementText}", expected "${params.expected}".` + (inputLine && elementValue.includes(params.expected) ? ` It is an input whose value contains it: assert what it holds with type: "value".` : "");
|
|
1463
1469
|
} else if (params.type === "value") {
|
|
1464
1470
|
passed = elementValue === params.expected || elementValue.includes(params.expected);
|
|
1465
1471
|
details = passed ? `Value assertion passed: "${params.element}" has value "${params.expected}".` : `Value assertion FAILED: "${params.element}" has value "${elementValue}", expected "${params.expected}".`;
|
|
@@ -1513,76 +1519,82 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
|
|
|
1513
1519
|
if (passed) {
|
|
1514
1520
|
const recordedExpected = params.type === "text" && params.substring !== false ? (0, import_utils.stableTextLead)(params.expected) : params.expected;
|
|
1515
1521
|
const selectorText = parsed.locator.kind === "text" ? parsed.locator.body : parsed.locator.next?.kind === "text" ? parsed.locator.next.body : parsed.locator.options?.name ?? null;
|
|
1516
|
-
const tautological = Boolean(
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1522
|
+
const tautological = Boolean(params.type === "text" && selectorText && selectorText === recordedExpected);
|
|
1523
|
+
const recordType = tautological ? "visible" : params.type;
|
|
1524
|
+
if (recordType !== params.type) {
|
|
1525
|
+
traceDebug(`Tautological text assertion recorded as presence: selector already matches "${recordedExpected}"`);
|
|
1526
|
+
details += ` Recorded as a presence check (toBeVisible): the locator already selects by "${recordedExpected}".`;
|
|
1527
|
+
}
|
|
1528
|
+
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" });
|
|
1530
|
+
parsed = unique.parsed;
|
|
1531
|
+
details += unique.note;
|
|
1532
|
+
if (recordType === "visible" && !unique.pinned && parsed.locator.kind !== "test-id") {
|
|
1533
|
+
const matches = TraceRecordingBackend._snapshotMatches(snapLines, parsed.locator);
|
|
1534
|
+
if (matches !== void 0 && matches > 1) {
|
|
1535
|
+
return { content: [{ type: "text", text: `### Error
|
|
1536
|
+
"${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 };
|
|
1525
1537
|
}
|
|
1526
1538
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
case "visible":
|
|
1546
|
-
assertName = "assertVisible";
|
|
1547
|
-
args = { type: "visible", ref: params.ref, selector: parsed.selector };
|
|
1548
|
-
code = `assertVisible:${parsed.selector}`;
|
|
1549
|
-
break;
|
|
1550
|
-
case "count":
|
|
1551
|
-
assertName = "assertCount";
|
|
1552
|
-
args = { type: "count", ref: params.ref, selector: countSelector, count: Number(params.expected) };
|
|
1553
|
-
code = `assertCount:${countSelector}:${params.expected}`;
|
|
1554
|
-
break;
|
|
1555
|
-
case "value":
|
|
1556
|
-
assertName = "assertValue";
|
|
1557
|
-
args = { type: "value", ref: params.ref, selector: parsed.selector, expected: recordedExpected };
|
|
1558
|
-
code = `assertValue:${parsed.selector}:${recordedExpected}`;
|
|
1559
|
-
break;
|
|
1560
|
-
case "css":
|
|
1561
|
-
assertName = "assertCSS";
|
|
1562
|
-
args = { type: "css", ref: params.ref, selector: parsed.selector, property: params.property.trim(), expected: liveValue ?? params.expected };
|
|
1563
|
-
code = `assertCSS:${parsed.selector}:${params.property.trim()}:${liveValue ?? params.expected}`;
|
|
1564
|
-
break;
|
|
1565
|
-
case "class":
|
|
1566
|
-
assertName = "assertClass";
|
|
1567
|
-
args = { type: "class", ref: params.ref, selector: parsed.selector, expected: params.expected.trim() };
|
|
1568
|
-
code = `assertClass:${parsed.selector}:${params.expected.trim()}`;
|
|
1569
|
-
break;
|
|
1570
|
-
case "attribute":
|
|
1571
|
-
assertName = "assertAttribute";
|
|
1572
|
-
args = { type: "attribute", ref: params.ref, selector: parsed.selector, property: params.property.trim(), expected: liveValue ?? params.expected };
|
|
1573
|
-
code = `assertAttribute:${parsed.selector}:${params.property.trim()}:${liveValue ?? params.expected}`;
|
|
1574
|
-
break;
|
|
1575
|
-
default:
|
|
1576
|
-
assertName = "assertText";
|
|
1577
|
-
args = { type: "text", ref: params.ref, selector: parsed.selector, expected: recordedExpected, substring: params.substring };
|
|
1578
|
-
code = `assertText:${parsed.selector}:${recordedExpected}:${params.substring}`;
|
|
1579
|
-
break;
|
|
1539
|
+
}
|
|
1540
|
+
let assertName;
|
|
1541
|
+
let args;
|
|
1542
|
+
let code;
|
|
1543
|
+
switch (recordType) {
|
|
1544
|
+
case "checked": {
|
|
1545
|
+
const expectChecked = params.expected !== "false";
|
|
1546
|
+
assertName = "assertChecked";
|
|
1547
|
+
args = { type: "checked", ref: params.ref, selector: parsed.selector, locator: parsed.locator, checked: expectChecked };
|
|
1548
|
+
code = `assertChecked:${parsed.selector}:${expectChecked}`;
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
case "disabled": {
|
|
1552
|
+
const expectDisabled = params.expected !== "false";
|
|
1553
|
+
assertName = "assertDisabled";
|
|
1554
|
+
args = { type: "disabled", ref: params.ref, selector: parsed.selector, locator: parsed.locator, disabled: expectDisabled };
|
|
1555
|
+
code = `assertDisabled:${parsed.selector}:${expectDisabled}`;
|
|
1556
|
+
break;
|
|
1580
1557
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1558
|
+
case "visible":
|
|
1559
|
+
assertName = "assertVisible";
|
|
1560
|
+
args = { type: "visible", ref: params.ref, selector: parsed.selector, locator: parsed.locator };
|
|
1561
|
+
code = `assertVisible:${parsed.selector}`;
|
|
1562
|
+
break;
|
|
1563
|
+
case "count":
|
|
1564
|
+
assertName = "assertCount";
|
|
1565
|
+
args = { type: "count", ref: params.ref, selector: countSelector, locator: { kind: "role", body: countSelector.slice("internal:role=".length), options: { attrs: [], exact: false } }, count: Number(params.expected) };
|
|
1566
|
+
code = `assertCount:${countSelector}:${params.expected}`;
|
|
1567
|
+
break;
|
|
1568
|
+
case "value":
|
|
1569
|
+
assertName = "assertValue";
|
|
1570
|
+
args = { type: "value", ref: params.ref, selector: parsed.selector, locator: parsed.locator, expected: recordedExpected };
|
|
1571
|
+
code = `assertValue:${parsed.selector}:${recordedExpected}`;
|
|
1572
|
+
break;
|
|
1573
|
+
case "css":
|
|
1574
|
+
assertName = "assertCSS";
|
|
1575
|
+
args = { type: "css", ref: params.ref, selector: parsed.selector, locator: parsed.locator, property: params.property.trim(), expected: liveValue ?? params.expected };
|
|
1576
|
+
code = `assertCSS:${parsed.selector}:${params.property.trim()}:${liveValue ?? params.expected}`;
|
|
1577
|
+
break;
|
|
1578
|
+
case "class":
|
|
1579
|
+
assertName = "assertClass";
|
|
1580
|
+
args = { type: "class", ref: params.ref, selector: parsed.selector, locator: parsed.locator, expected: params.expected.trim() };
|
|
1581
|
+
code = `assertClass:${parsed.selector}:${params.expected.trim()}`;
|
|
1582
|
+
break;
|
|
1583
|
+
case "attribute":
|
|
1584
|
+
assertName = "assertAttribute";
|
|
1585
|
+
args = { type: "attribute", ref: params.ref, selector: parsed.selector, locator: parsed.locator, property: params.property.trim(), expected: liveValue ?? params.expected };
|
|
1586
|
+
code = `assertAttribute:${parsed.selector}:${params.property.trim()}:${liveValue ?? params.expected}`;
|
|
1587
|
+
break;
|
|
1588
|
+
default:
|
|
1589
|
+
assertName = "assertText";
|
|
1590
|
+
args = { type: "text", ref: params.ref, selector: parsed.selector, locator: parsed.locator, expected: recordedExpected, substring: params.substring };
|
|
1591
|
+
code = `assertText:${parsed.selector}:${recordedExpected}:${params.substring}`;
|
|
1592
|
+
break;
|
|
1585
1593
|
}
|
|
1594
|
+
if (unhoverMove)
|
|
1595
|
+
this._trackedActions.push({ toolName: "browser_mouse_action", args: { name: "mouse.move", position: unhoverMove }, code: "", timestamp: timestamp - 1, pageAlias: this._currentPageAlias });
|
|
1596
|
+
this._trackedActions.push({ toolName: "browser_assert", args, code, timestamp, pageAlias: this._currentPageAlias });
|
|
1597
|
+
traceDebug(`Assert: ${assertName} with selector ${args.selector}`);
|
|
1586
1598
|
}
|
|
1587
1599
|
return {
|
|
1588
1600
|
content: [{ type: "text", text: `### ${passed ? "Assertion Passed" : "Assertion Failed"}
|
|
@@ -1825,41 +1837,34 @@ ${details}` }] };
|
|
|
1825
1837
|
_codeToLocator(expr) {
|
|
1826
1838
|
const testidMatch = expr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1827
1839
|
if (testidMatch) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
locator: { kind: "test-id", body: testidMatch[1], options: {} }
|
|
1831
|
-
};
|
|
1840
|
+
const locator = { kind: "test-id", body: testidMatch[1], options: {} };
|
|
1841
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1832
1842
|
}
|
|
1833
1843
|
const roleMatch = expr.match(/getByRole\(\s*['"]([^'"]+)['"](?:\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"][^}]*\})?\s*\)/);
|
|
1834
1844
|
if (roleMatch) {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
locator: { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, ...roleMatch[2] ? { name: roleMatch[2] } : {} } }
|
|
1838
|
-
};
|
|
1845
|
+
const locator = { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, ...roleMatch[2] ? { name: roleMatch[2] } : {} } };
|
|
1846
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1839
1847
|
}
|
|
1840
1848
|
const labelMatch = expr.match(/getByLabel\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1841
1849
|
if (labelMatch) {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
locator: { kind: "label", body: labelMatch[1], options: { exact: false } }
|
|
1845
|
-
};
|
|
1850
|
+
const locator = { kind: "label", body: labelMatch[1], options: { exact: false } };
|
|
1851
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1846
1852
|
}
|
|
1847
1853
|
const textMatch = expr.match(/getByText\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1848
1854
|
if (textMatch) {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
locator: { kind: "text", body: textMatch[1], options: { exact: false } }
|
|
1852
|
-
};
|
|
1855
|
+
const locator = { kind: "text", body: textMatch[1], options: { exact: false } };
|
|
1856
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
1853
1857
|
}
|
|
1854
1858
|
const ariaRefMatch = expr.match(/locator\(\s*['"]aria-ref=(\w+)['"]\s*\)/);
|
|
1855
1859
|
if (ariaRefMatch) {
|
|
1856
1860
|
return null;
|
|
1857
1861
|
}
|
|
1858
|
-
const cssMatch = expr.match(/locator\(\s*['"
|
|
1859
|
-
|
|
1862
|
+
const cssMatch = expr.match(/locator\(\s*(?:'([^']+)'|"([^"]+)")\s*\)/);
|
|
1863
|
+
const css = cssMatch?.[1] ?? cssMatch?.[2];
|
|
1864
|
+
if (css && /^(?:[#.]|[a-z][\w-]*\[)/i.test(css)) {
|
|
1860
1865
|
return {
|
|
1861
|
-
selector:
|
|
1862
|
-
locator: { kind: "
|
|
1866
|
+
selector: css,
|
|
1867
|
+
locator: { kind: "default", body: css, options: {} }
|
|
1863
1868
|
};
|
|
1864
1869
|
}
|
|
1865
1870
|
return null;
|
|
@@ -1872,10 +1877,7 @@ ${details}` }] };
|
|
|
1872
1877
|
*/
|
|
1873
1878
|
async _improveTextSelector(snapLines, ref, original) {
|
|
1874
1879
|
const textBody = original.locator.body;
|
|
1875
|
-
const occurrences = snapLines.
|
|
1876
|
-
const lineText = TraceRecordingBackend._snapshotLineText(l);
|
|
1877
|
-
return lineText !== null && lineText.includes(textBody);
|
|
1878
|
-
}).length;
|
|
1880
|
+
const occurrences = TraceRecordingBackend._snapshotMatches(snapLines, original.locator) ?? 0;
|
|
1879
1881
|
if (occurrences <= 1)
|
|
1880
1882
|
return null;
|
|
1881
1883
|
traceDebug(`Text "${textBody}" appears ${occurrences} times in snapshot \u2014 looking for parent test-id`);
|
|
@@ -1913,18 +1915,15 @@ ${details}` }] };
|
|
|
1913
1915
|
const ancestorExpr = ancestorLocatorMatch[1].trim();
|
|
1914
1916
|
const testidMatch = ancestorExpr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1915
1917
|
if (testidMatch) {
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
locator: {
|
|
1922
|
-
kind: "test-id",
|
|
1923
|
-
body: testid,
|
|
1924
|
-
options: {},
|
|
1925
|
-
next: { kind: "text", body: textBody, options: { exact: false } }
|
|
1926
|
-
}
|
|
1918
|
+
const locator = {
|
|
1919
|
+
kind: "test-id",
|
|
1920
|
+
body: testidMatch[1],
|
|
1921
|
+
options: {},
|
|
1922
|
+
next: { kind: "text", body: textBody, options: { exact: !!original.locator.options?.exact } }
|
|
1927
1923
|
};
|
|
1924
|
+
const chainedSelector = TraceRecordingBackend._renderSelector(locator);
|
|
1925
|
+
traceDebug(`Improved to chained selector: ${chainedSelector}`);
|
|
1926
|
+
return { selector: chainedSelector, locator };
|
|
1928
1927
|
}
|
|
1929
1928
|
}
|
|
1930
1929
|
}
|
|
@@ -1935,6 +1934,175 @@ ${details}` }] };
|
|
|
1935
1934
|
}
|
|
1936
1935
|
return null;
|
|
1937
1936
|
}
|
|
1937
|
+
static {
|
|
1938
|
+
/** Roles whose colon text in the aria snapshot is the element's VALUE (what it
|
|
1939
|
+
* holds), not text it renders: `- textbox [ref=e1]: Google Pixel 4`. */
|
|
1940
|
+
this.INPUT_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "spinbutton"]);
|
|
1941
|
+
}
|
|
1942
|
+
static _isInputLine(refLine) {
|
|
1943
|
+
const role = refLine.match(/^\s*-\s*(\w+)/)?.[1];
|
|
1944
|
+
return !!role && TraceRecordingBackend.INPUT_ROLES.has(role);
|
|
1945
|
+
}
|
|
1946
|
+
/** A CSS selector that is a framework-generated id (per the shared isDynamicId). */
|
|
1947
|
+
static _isVolatileCss(selector) {
|
|
1948
|
+
return selector.startsWith("#") && (0, import_dynamicId.isDynamicId)(selector.slice(1));
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* The one renderer from a locator object to its internal selector string.
|
|
1952
|
+
* Values are JSON-quoted (as _handleAssertHidden does), so a name containing
|
|
1953
|
+
* `"` still produces a valid selector; exactness is the `s` (exact) or `i`
|
|
1954
|
+
* (substring, case-insensitive) suffix.
|
|
1955
|
+
*/
|
|
1956
|
+
static _renderSelector(locator) {
|
|
1957
|
+
const q = (v, exact) => `${JSON.stringify(v)}${exact ? "s" : "i"}`;
|
|
1958
|
+
let head;
|
|
1959
|
+
switch (locator.kind) {
|
|
1960
|
+
case "test-id":
|
|
1961
|
+
head = `internal:testid=[data-testid=${JSON.stringify(locator.body)}s]`;
|
|
1962
|
+
break;
|
|
1963
|
+
case "role":
|
|
1964
|
+
head = `internal:role=${locator.body}${typeof locator.options?.name === "string" ? `[name=${q(locator.options.name, locator.options.exact)}]` : ""}`;
|
|
1965
|
+
break;
|
|
1966
|
+
case "text":
|
|
1967
|
+
head = `internal:text=${q(locator.body, locator.options?.exact)}`;
|
|
1968
|
+
break;
|
|
1969
|
+
case "label":
|
|
1970
|
+
head = `internal:label=${q(locator.body, locator.options?.exact)}`;
|
|
1971
|
+
break;
|
|
1972
|
+
case "nth":
|
|
1973
|
+
head = `nth=${locator.body}`;
|
|
1974
|
+
break;
|
|
1975
|
+
default:
|
|
1976
|
+
head = String(locator.body);
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
return locator.next ? `${head} >> ${TraceRecordingBackend._renderSelector(locator.next)}` : head;
|
|
1980
|
+
}
|
|
1981
|
+
/** The last link of a locator chain. */
|
|
1982
|
+
static _leaf(locator) {
|
|
1983
|
+
let tail = locator;
|
|
1984
|
+
while (tail.next)
|
|
1985
|
+
tail = tail.next;
|
|
1986
|
+
return tail;
|
|
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 };
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Make a locator pick out the ref's own element: a presence check's leaf made
|
|
2003
|
+
* exact (when asked), the SKYR-4263 test-id walk for an ambiguous text
|
|
2004
|
+
* locator, and — if the live page still finds more than one element — a pin
|
|
2005
|
+
* to the ref's element by position (`>> nth=K`, the form the exporter and
|
|
2006
|
+
* codegen already carry for clicks; SKYR-4505). `note` describes a pin for the
|
|
2007
|
+
* tool result; `pinFailure` says why an ambiguous locator could not be pinned.
|
|
2008
|
+
*/
|
|
2009
|
+
async _makeUnique(parsed, opts) {
|
|
2010
|
+
if (opts.exact)
|
|
2011
|
+
parsed = TraceRecordingBackend._exactLeaf(parsed, opts.lineText);
|
|
2012
|
+
if (parsed.locator.kind === "text") {
|
|
2013
|
+
const improved = await this._improveTextSelector(opts.snapLines, opts.ref, parsed);
|
|
2014
|
+
if (improved) {
|
|
2015
|
+
traceDebug(`Improved ambiguous text selector to: ${improved.selector}`);
|
|
2016
|
+
parsed = improved;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
2020
|
+
if (!page)
|
|
2021
|
+
return { parsed, note: "", pinned: false };
|
|
2022
|
+
const pin = await TraceRecordingBackend._refIndexAmong(page, opts.ref, parsed.selector);
|
|
2023
|
+
if ("index" in pin) {
|
|
2024
|
+
if (pin.count <= 1)
|
|
2025
|
+
return { parsed, note: "", pinned: false };
|
|
2026
|
+
const unpinned = parsed.selector;
|
|
2027
|
+
parsed = TraceRecordingBackend._withNth(parsed, pin.index);
|
|
2028
|
+
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.`, pinned: true };
|
|
2030
|
+
}
|
|
2031
|
+
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
|
+
return { parsed, note: "", pinned: false, pinFailure };
|
|
2034
|
+
}
|
|
2035
|
+
/** How many elements `selector` matches on the live page and where the ref's
|
|
2036
|
+
* own element sits among them — one evaluateAll — or why it cannot tell: the
|
|
2037
|
+
* ref no longer resolves (stale snapshot), it is not one of the matches, or
|
|
2038
|
+
* the lookup failed. */
|
|
2039
|
+
static async _refIndexAmong(page, ref, selector) {
|
|
2040
|
+
let handle;
|
|
2041
|
+
try {
|
|
2042
|
+
handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout: 1e3 });
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
return { reason: "unresolved", detail: String(e?.message ?? e).split("\n")[0] };
|
|
2045
|
+
}
|
|
2046
|
+
if (!handle)
|
|
2047
|
+
return { reason: "unresolved" };
|
|
2048
|
+
try {
|
|
2049
|
+
const { count, index } = await page.locator(selector).evaluateAll((els, target) => ({ count: els.length, index: els.indexOf(target) }), handle);
|
|
2050
|
+
if (count <= 1)
|
|
2051
|
+
return { index: Math.max(index, 0), count };
|
|
2052
|
+
return index >= 0 ? { index, count } : { reason: "not-among-matches" };
|
|
2053
|
+
} catch (e) {
|
|
2054
|
+
return { reason: "error", detail: String(e?.message ?? e).split("\n")[0] };
|
|
2055
|
+
} finally {
|
|
2056
|
+
await handle.dispose().catch(() => {
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
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
|
+
/**
|
|
2090
|
+
* The locator to record for a ref: the hover's, unless it is missing or CSS
|
|
2091
|
+
* 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.
|
|
2093
|
+
* Shared by browser_assert and element visual snapshots so both paths agree.
|
|
2094
|
+
*/
|
|
2095
|
+
_preferSnapshotLocator(parsed, refLine) {
|
|
2096
|
+
if (parsed && parsed.locator.kind !== "default")
|
|
2097
|
+
return parsed;
|
|
2098
|
+
const fromSnapshot = this._extractLocatorForRef(refLine);
|
|
2099
|
+
if (!fromSnapshot || !parsed)
|
|
2100
|
+
return fromSnapshot ?? parsed;
|
|
2101
|
+
const bareRole = fromSnapshot.locator.kind === "role" && typeof fromSnapshot.locator.options?.name !== "string";
|
|
2102
|
+
if (bareRole && !TraceRecordingBackend._isVolatileCss(parsed.selector))
|
|
2103
|
+
return parsed;
|
|
2104
|
+
return fromSnapshot;
|
|
2105
|
+
}
|
|
1938
2106
|
/**
|
|
1939
2107
|
* Text value of an aria-snapshot line, in either form the renderer emits:
|
|
1940
2108
|
* `- generic "foo" [ref=e1]` (quoted accessible name) or
|
|
@@ -1999,42 +2167,27 @@ ${details}` }] };
|
|
|
1999
2167
|
_extractLocatorForRef(refLine) {
|
|
2000
2168
|
const testidMatch = refLine.match(/testid="([^"]+)"/);
|
|
2001
2169
|
if (testidMatch) {
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
locator: { kind: "test-id", body: testidMatch[1], options: {} }
|
|
2005
|
-
};
|
|
2170
|
+
const locator = { kind: "test-id", body: testidMatch[1], options: {} };
|
|
2171
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2006
2172
|
}
|
|
2007
2173
|
const roleMatch = refLine.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
|
|
2008
2174
|
if (roleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(roleMatch[1])) {
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
return {
|
|
2012
|
-
selector: `internal:role=${role}[name="${name}"i]`,
|
|
2013
|
-
locator: { kind: "role", body: role, options: { attrs: [], exact: false, name } }
|
|
2014
|
-
};
|
|
2175
|
+
const locator = { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, name: (0, import_utils.stableTextLead)(roleMatch[2]) } };
|
|
2176
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2015
2177
|
}
|
|
2016
2178
|
if (roleMatch && roleMatch[2]) {
|
|
2017
|
-
const
|
|
2018
|
-
return {
|
|
2019
|
-
selector: `internal:text="${text}"i`,
|
|
2020
|
-
locator: { kind: "text", body: text, options: { exact: false } }
|
|
2021
|
-
};
|
|
2179
|
+
const locator = { kind: "text", body: (0, import_utils.stableTextLead)(roleMatch[2]), options: { exact: false } };
|
|
2180
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2022
2181
|
}
|
|
2023
2182
|
const colonTextMatch = refLine.match(/\[ref=\w+\](?:\s*\[[^\]]*\])*\s*:\s*(.+?)\s*$/);
|
|
2024
|
-
if (colonTextMatch && colonTextMatch[1].trim()) {
|
|
2025
|
-
const
|
|
2026
|
-
return {
|
|
2027
|
-
selector: `internal:text="${text}"i`,
|
|
2028
|
-
locator: { kind: "text", body: text, options: { exact: false } }
|
|
2029
|
-
};
|
|
2183
|
+
if (colonTextMatch && colonTextMatch[1].trim() && !TraceRecordingBackend._isInputLine(refLine)) {
|
|
2184
|
+
const locator = { kind: "text", body: (0, import_utils.stableTextLead)(colonTextMatch[1].trim()), options: { exact: false } };
|
|
2185
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2030
2186
|
}
|
|
2031
2187
|
const bareRoleMatch = refLine.match(/^\s*-\s*(\w+)(?:\s+\[[^\]]*\])*\s+\[ref=\w+\]/);
|
|
2032
2188
|
if (bareRoleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(bareRoleMatch[1])) {
|
|
2033
|
-
const
|
|
2034
|
-
return {
|
|
2035
|
-
selector: `internal:role=${role}`,
|
|
2036
|
-
locator: { kind: "role", body: role, options: { attrs: [], exact: false } }
|
|
2037
|
-
};
|
|
2189
|
+
const locator = { kind: "role", body: bareRoleMatch[1], options: { attrs: [], exact: false } };
|
|
2190
|
+
return { selector: TraceRecordingBackend._renderSelector(locator), locator };
|
|
2038
2191
|
}
|
|
2039
2192
|
return null;
|
|
2040
2193
|
}
|