@skyramp/mcp 0.4.4 → 0.5.0-rc.10
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/entitlements/entitlementGate.d.ts +8 -0
- package/build/entitlements/entitlementGate.js +57 -0
- package/build/entitlements/runCodes.d.ts +18 -0
- package/build/entitlements/runCodes.js +16 -0
- package/build/index.js +32 -6
- package/build/playwright/PlaywrightTraceService.d.ts +17 -0
- package/build/playwright/PlaywrightTraceService.js +29 -1
- package/build/playwright/dataSafetyConfig.d.ts +74 -0
- package/build/playwright/dataSafetyConfig.js +109 -0
- package/build/playwright/essentialTools.d.ts +1 -0
- package/build/playwright/essentialTools.js +44 -0
- package/build/playwright/registerPlaywrightTools.d.ts +36 -1
- package/build/playwright/registerPlaywrightTools.js +87 -43
- package/build/playwright/safeResolveDataSafety.d.ts +24 -0
- package/build/playwright/safeResolveDataSafety.js +33 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +46 -177
- package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/test-maintenance/driftAnalysisShared.js +1 -1
- package/build/services/FleetExecutionService.d.ts +87 -16
- package/build/services/FleetExecutionService.js +487 -48
- package/build/services/FleetRunRegistry.d.ts +49 -0
- package/build/services/FleetRunRegistry.js +178 -0
- package/build/services/containerEnv.js +8 -2
- package/build/services/fleetLimitConfirmation.d.ts +19 -0
- package/build/services/fleetLimitConfirmation.js +126 -0
- package/build/tool-phases.d.ts +14 -8
- package/build/tool-phases.js +88 -43
- package/build/tools/code-refactor/assertion-state.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +2 -1
- package/build/tools/code-refactor/utils-verify-gates.js +1 -1
- package/build/tools/executeFleetTool.d.ts +6 -4
- package/build/tools/executeFleetTool.js +67 -23
- package/build/tools/fixErrorTool.js +2 -1
- package/build/tools/fleetLifecycleTools.d.ts +20 -0
- package/build/tools/fleetLifecycleTools.js +124 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
- package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
- package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
- package/build/tools/submitReportTool.js +4 -122
- package/build/tools/test-management/registerTestPlanTool.js +22 -6
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +11 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +111 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +40 -4
- package/build/types/AssertionOutcome.d.ts +3 -0
- package/build/types/TestbotReport.d.ts +0 -37
- package/build/types/TestbotReport.js +0 -22
- package/build/types/index.d.ts +1 -1
- package/build/types/index.js +1 -1
- package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
- package/build/utils/assertion-verify/created-add-guard.js +173 -0
- package/build/utils/assertion-verify/index.d.ts +1 -0
- package/build/utils/assertion-verify/index.js +1 -0
- package/build/utils/assertion-verify/verify.d.ts +7 -3
- package/build/utils/assertion-verify/verify.js +9 -3
- package/build/utils/featureFlags.js +6 -3
- package/build/utils/repositorySlug.d.ts +14 -0
- package/build/utils/repositorySlug.js +46 -1
- package/build/utils/telemetry.d.ts +2 -0
- package/build/utils/telemetry.js +12 -0
- package/build/utils/utils-verify/action-key.d.ts +59 -2
- package/build/utils/utils-verify/action-key.js +162 -6
- package/build/utils/utils-verify/action-sites.d.ts +7 -0
- package/build/utils/utils-verify/action-sites.js +277 -35
- package/build/utils/utils-verify/call-sites.d.ts +8 -2
- package/build/utils/utils-verify/language-spec.js +1 -1
- package/build/utils/utils-verify/parse.d.ts +4 -0
- package/build/utils/utils-verify/parse.js +2 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +63 -121
- package/build/workspace/workspace.js +45 -48
- package/build/workspace/workspaceVerifier.d.ts +22 -0
- package/build/workspace/workspaceVerifier.js +65 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
- package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
- package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
- package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
- package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
- package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harRecorder.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.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.GLqsx5cI.js → uiMode.Dl7smBJn.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/cli/program.ts +5 -0
- package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -4
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/prompts/testbot-task1.md +1 -1
- package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
- package/plugin/skills/setup-testbot-target-sut/SKILL.md +60 -0
- package/plugin/skills/setup-testbot-target-sut/reference/ci-workflow.md +26 -0
- package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
- package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +14 -0
- package/plugin/skills/setup-testbot-target-sut/reference/troubleshooting.md +20 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +0 -2
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +0 -178
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +0 -2
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +0 -311
- package/build/prompts/sut-setup/shared.d.ts +0 -62
- package/build/prompts/sut-setup/shared.js +0 -196
- package/build/resources/sutSetupResource.d.ts +0 -2
- package/build/resources/sutSetupResource.js +0 -63
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeRegExp } from "../regex.js";
|
|
2
|
+
import { paramName } from "./parse.js";
|
|
1
3
|
/**
|
|
2
4
|
* The Playwright actions a browser helper can perform — the closed vocabulary that
|
|
3
5
|
* gives the action VERB, where an API helper's method comes from the request. A
|
|
@@ -52,7 +54,10 @@ export function actionStepKeys(steps) {
|
|
|
52
54
|
* copy of a helper's sequence needs. Lines survive the Python and alias rewrites:
|
|
53
55
|
* neither adds or removes a newline.
|
|
54
56
|
*/
|
|
55
|
-
export function actionStepsOf(body, language = "typescript"
|
|
57
|
+
export function actionStepsOf(body, language = "typescript",
|
|
58
|
+
/** A helper's parameter names: a locator argument (or `name:` option) that is
|
|
59
|
+
* one of them keys as the wildcard `paramSlot(name)` instead of failing. */
|
|
60
|
+
params) {
|
|
56
61
|
const src = resolveLocatorAliases(language === "python" ? toPlaywrightJs(body) : body);
|
|
57
62
|
// Structure is read on a copy with string CONTENTS blanked (same length), so a
|
|
58
63
|
// `)` or `.` inside a literal never ends a chain, and a `.click(` written inside
|
|
@@ -63,7 +68,7 @@ export function actionStepsOf(body, language = "typescript") {
|
|
|
63
68
|
for (const m of structure.matchAll(ACTION_RE)) {
|
|
64
69
|
const verb = m[1];
|
|
65
70
|
const dot = m.index;
|
|
66
|
-
const
|
|
71
|
+
const verbLine = src.slice(0, dot).split("\n").length;
|
|
67
72
|
const argsOpen = dot + m[0].length - 1;
|
|
68
73
|
const argsClose = matchingClose(structure, argsOpen);
|
|
69
74
|
const chainStart = chainStartBefore(structure, dot);
|
|
@@ -71,16 +76,24 @@ export function actionStepsOf(body, language = "typescript") {
|
|
|
71
76
|
// An action inside another action's arguments: its chain starts before the
|
|
72
77
|
// previous action ended, so there is no gap to judge and no run to join.
|
|
73
78
|
if (argsClose === -1 || chainStart < prevEnd) {
|
|
74
|
-
steps.push({
|
|
79
|
+
steps.push({
|
|
80
|
+
line: verbLine,
|
|
81
|
+
gapBefore,
|
|
82
|
+
gapAfter: "",
|
|
83
|
+
endLine: verbLine,
|
|
84
|
+
value: "",
|
|
85
|
+
});
|
|
75
86
|
prevEnd = argsOpen + 1;
|
|
76
87
|
continue;
|
|
77
88
|
}
|
|
78
89
|
prevEnd = argsClose + 1;
|
|
79
90
|
steps.push({
|
|
80
|
-
line,
|
|
91
|
+
line: src.slice(0, chainStart).split("\n").length,
|
|
81
92
|
gapBefore,
|
|
82
93
|
gapAfter: "",
|
|
83
|
-
|
|
94
|
+
endLine: verbLine + (src.slice(dot, argsClose).match(/\n/g)?.length ?? 0),
|
|
95
|
+
value: firstArgument(structure, src, argsOpen + 1, argsClose),
|
|
96
|
+
key: stepKey(verb, src, structure, chainStart, dot, argsOpen, argsClose, params),
|
|
84
97
|
});
|
|
85
98
|
}
|
|
86
99
|
for (let i = 0; i < steps.length; i++)
|
|
@@ -89,8 +102,10 @@ export function actionStepsOf(body, language = "typescript") {
|
|
|
89
102
|
return steps;
|
|
90
103
|
}
|
|
91
104
|
/** The key of one action, or `undefined` when its locator or target is unreadable. */
|
|
92
|
-
function stepKey(verb, src, structure, chainStart, dot, argsOpen, argsClose) {
|
|
105
|
+
function stepKey(verb, src, structure, chainStart, dot, argsOpen, argsClose, params) {
|
|
93
106
|
let chain = src.slice(chainStart, dot);
|
|
107
|
+
if (params?.size)
|
|
108
|
+
chain = chain.replace(PARAM_LOCATOR_RE, (m, head, id) => params.has(id) ? `${head}"${paramSlot(id)}"` : m);
|
|
94
109
|
const args = src.slice(argsOpen + 1, argsClose);
|
|
95
110
|
let target = "";
|
|
96
111
|
if (!/\(/.test(structure.slice(chainStart, dot))) {
|
|
@@ -219,6 +234,147 @@ function hasDynamicSelector(text) {
|
|
|
219
234
|
return true;
|
|
220
235
|
return false;
|
|
221
236
|
}
|
|
237
|
+
/** A locator argument or `name:` option that is a bare identifier. */
|
|
238
|
+
const PARAM_LOCATOR_RE = /(\.(?:getBy\w+|locator|frameLocator)\s*\(\s*|\bname\s*:\s*)([A-Za-z_$][\w$]*)(?=\s*[,)}])/g;
|
|
239
|
+
/** The text a parameter-fed locator argument keys as: a string no selector holds.
|
|
240
|
+
* A key holding SLOT_MARK holds a wildcard. */
|
|
241
|
+
export const SLOT_MARK = "\u0001";
|
|
242
|
+
export function paramSlot(name) {
|
|
243
|
+
return `${SLOT_MARK}${name}${SLOT_MARK}`;
|
|
244
|
+
}
|
|
245
|
+
const LITERAL_RE = /^(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|-?\d+(?:\.\d+)?)$/;
|
|
246
|
+
const GET_VALUE_RE = /^getValue\s*\(\s*([A-Za-z_$][\w$]*)\s*,\s*(["'])([\w$ .-]*)\2\s*\)((?:\s*\.\s*[A-Za-z_$][\w$]*\s*\(\s*\))*)$/;
|
|
247
|
+
/**
|
|
248
|
+
* An action's value (`ActionStep.value`) as the sibling scan compares it: a bare
|
|
249
|
+
* parameter of the helper, a literal, or the generated `getValue(playwrightRequestN,
|
|
250
|
+
* "key")` — a dotted key read through nested objects, a chained `.toString()` kept
|
|
251
|
+
* as `suffix` — read through the binding that holds it (`requestBindingsOf`), to
|
|
252
|
+
* the parameter or literal stored under that key. Anything else is an expression,
|
|
253
|
+
* compared as written.
|
|
254
|
+
*/
|
|
255
|
+
export function resolveStepValue(raw, bindings, params = new Set()) {
|
|
256
|
+
const text = raw.trim();
|
|
257
|
+
if (params.has(text))
|
|
258
|
+
return { kind: "param", text };
|
|
259
|
+
if (LITERAL_RE.test(text))
|
|
260
|
+
return { kind: "literal", text };
|
|
261
|
+
const get = GET_VALUE_RE.exec(text);
|
|
262
|
+
const object = get ? bindings.get(get[1]) : undefined;
|
|
263
|
+
if (get && object !== undefined) {
|
|
264
|
+
let field = fieldOf(object, get[3]);
|
|
265
|
+
if (field === undefined)
|
|
266
|
+
for (const [i, part] of get[3].split(".").entries())
|
|
267
|
+
field = fieldOf(i === 0 ? object : (field ?? ""), part);
|
|
268
|
+
if (field !== undefined)
|
|
269
|
+
return {
|
|
270
|
+
...resolveStepValue(field, new Map(), params),
|
|
271
|
+
suffix: get[4].replace(/\s+/g, ""),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return { kind: "expr", text };
|
|
275
|
+
}
|
|
276
|
+
/** The value written under the TOP-LEVEL `key` of an object literal, or undefined. */
|
|
277
|
+
function fieldOf(object, key) {
|
|
278
|
+
const text = object.trim();
|
|
279
|
+
if (!text.startsWith("{") || !text.endsWith("}"))
|
|
280
|
+
return undefined;
|
|
281
|
+
const structure = blankStringContents(text);
|
|
282
|
+
let depth = 0;
|
|
283
|
+
let from = 1;
|
|
284
|
+
for (let i = 1; i < text.length; i++) {
|
|
285
|
+
const ch = structure[i];
|
|
286
|
+
if ("([{".includes(ch))
|
|
287
|
+
depth++;
|
|
288
|
+
else if (depth > 0 && ")]}".includes(ch))
|
|
289
|
+
depth--;
|
|
290
|
+
else if (depth === 0 && (ch === "," || i === text.length - 1)) {
|
|
291
|
+
const colon = structure.indexOf(":", from);
|
|
292
|
+
if (colon !== -1 && colon < i) {
|
|
293
|
+
const name = text
|
|
294
|
+
.slice(from, colon)
|
|
295
|
+
.trim()
|
|
296
|
+
.replace(/^(["'])(.*)\1$/, "$2");
|
|
297
|
+
if (name === key)
|
|
298
|
+
return text.slice(colon + 1, i).trim();
|
|
299
|
+
}
|
|
300
|
+
from = i + 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
/** The generator's `const playwrightRequestN: Record<…> = { … }` bindings in `text`,
|
|
306
|
+
* by name, to the object literal as written. A name bound twice is left out. */
|
|
307
|
+
export function requestBindingsOf(text) {
|
|
308
|
+
const structure = blankStringContents(text);
|
|
309
|
+
const out = new Map();
|
|
310
|
+
const twice = new Set();
|
|
311
|
+
for (const m of structure.matchAll(/\bconst\s+(playwrightRequest\d+)\s*(?::\s*Record\s*<[^>]*>)?\s*=\s*\{/g)) {
|
|
312
|
+
const open = m.index + m[0].length - 1;
|
|
313
|
+
const close = matchingClose(structure, open);
|
|
314
|
+
if (close === -1 || out.has(m[1]))
|
|
315
|
+
twice.add(m[1]);
|
|
316
|
+
else
|
|
317
|
+
out.set(m[1], text.slice(open, close + 1));
|
|
318
|
+
}
|
|
319
|
+
for (const name of twice)
|
|
320
|
+
out.delete(name);
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/** The normalised arguments of every `page.waitForResponse(…)` in `text`. */
|
|
324
|
+
export function responseWaitsOf(text) {
|
|
325
|
+
const structure = blankStringContents(text);
|
|
326
|
+
const out = [];
|
|
327
|
+
for (const m of structure.matchAll(/\bwaitForResponse\s*\(/g)) {
|
|
328
|
+
const open = m.index + m[0].length - 1;
|
|
329
|
+
const close = matchingClose(structure, open);
|
|
330
|
+
if (close !== -1)
|
|
331
|
+
out.push(normalize(text.slice(open + 1, close)));
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
/** A use of the identifier `name`: not part of a longer name or a property access. */
|
|
336
|
+
export function identifierUseRe(name, flags = "") {
|
|
337
|
+
return new RegExp(`(?<![\\w$.])${escapeRegExp(name)}(?![\\w$])`, flags);
|
|
338
|
+
}
|
|
339
|
+
/** A declared parameter's name (see paramName), or `undefined` for a rest or
|
|
340
|
+
* destructured one: no single value a call or a wildcard can carry. */
|
|
341
|
+
export function simpleParamName(raw) {
|
|
342
|
+
if (/^\s*\.\.\./.test(raw))
|
|
343
|
+
return undefined;
|
|
344
|
+
const name = paramName(raw);
|
|
345
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : undefined;
|
|
346
|
+
}
|
|
347
|
+
/** The helper's ActionPattern, or `undefined` when a step stays unreadable with
|
|
348
|
+
* its parameters as wildcards. `params` are the declared parameters as written. */
|
|
349
|
+
export function actionPatternOf(body, language, params) {
|
|
350
|
+
// A parameter the body reassigns (`=`, `+=`, `++`, a destructuring) is not the
|
|
351
|
+
// value its caller passed.
|
|
352
|
+
const assigned = (text, name) => {
|
|
353
|
+
const id = escapeRegExp(name);
|
|
354
|
+
return [
|
|
355
|
+
`(?<![\\w$.])${id}\\s*(?:[-+*/%&|^]|\\*\\*|<<|>>>?|&&|\\|\\||\\?\\?)?(?<![!<>=])=(?![=>])`,
|
|
356
|
+
`(?:\\+\\+|--)\\s*${id}(?![\\w$])|(?<![\\w$.])${id}\\s*(?:\\+\\+|--)`,
|
|
357
|
+
`[[{][^=;]*(?<![\\w$.])${id}(?![\\w$])[^=;]*[\\]}]\\s*=(?![=>])`,
|
|
358
|
+
].reduce((n, re) => n + (text.match(new RegExp(re, "g"))?.length ?? 0), 0);
|
|
359
|
+
};
|
|
360
|
+
const names = new Set(params
|
|
361
|
+
.map(simpleParamName)
|
|
362
|
+
.filter((n, i) => n !== undefined && assigned(body, n) <= assigned(params[i], n)));
|
|
363
|
+
const steps = actionStepsOf(body, language, names);
|
|
364
|
+
const keys = actionStepKeys(steps);
|
|
365
|
+
if (!keys)
|
|
366
|
+
return undefined;
|
|
367
|
+
const bindings = requestBindingsOf(body);
|
|
368
|
+
const uses = (text, name) => text.match(identifierUseRe(name, "g"))?.length ?? 0;
|
|
369
|
+
return {
|
|
370
|
+
steps: keys,
|
|
371
|
+
values: steps.map((s) => resolveStepValue(s.value, bindings, names)),
|
|
372
|
+
responseWaits: responseWaitsOf(body),
|
|
373
|
+
usedParams: params
|
|
374
|
+
.map(simpleParamName)
|
|
375
|
+
.filter((n, i) => n !== undefined && uses(body, n) > uses(params[i], n)),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
222
378
|
/** Whitespace and braces dropped outside string literals, `'` normalised to `"` —
|
|
223
379
|
* the spelling a locator chain has inside a key. */
|
|
224
380
|
export function normalizeChain(text) {
|
|
@@ -25,6 +25,13 @@ import { type InlineCallSite, type SiblingScan } from "./call-sites.js";
|
|
|
25
25
|
* a sibling action with an undefined key equals nothing, so no run crosses it.
|
|
26
26
|
* Both fail open — no match, no finding — as STEP 3b's decline rule does.
|
|
27
27
|
*
|
|
28
|
+
* Two widenings (SKYR-4537), each held to boundRun's checks, so that every such
|
|
29
|
+
* site names the exact call that replaces it: the generator's own per-step bindings
|
|
30
|
+
* (`const playwrightRequestN = {…}`, `const responsePromiseN =
|
|
31
|
+
* page.waitForResponse(…)`, `const responseN = await responsePromiseN`) are
|
|
32
|
+
* transparent between two actions, and a helper whose locator argument is one of
|
|
33
|
+
* its parameters matches with the sibling's literal in that place.
|
|
34
|
+
*
|
|
28
35
|
* Where two helpers' keys start the same run, the longer wins; the run's actions are
|
|
29
36
|
* consumed and the scan continues after it. A run inside the sibling's own local
|
|
30
37
|
* helper is reported with that helper's name, as the API scan does.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
-
import { blankComments, localHelperAt, localHelpersOf, } from "./parse.js";
|
|
2
|
+
import { blankComments, defaultAt, localHelperAt, localHelpersOf, } from "./parse.js";
|
|
3
3
|
import { expectArgs, HELPER_FAMILIES, } from "./language-spec.js";
|
|
4
|
-
import { actionStepsOf, blankStringContents, normalizeChain, statementSpans, } from "./action-key.js";
|
|
4
|
+
import { actionStepsOf, blankStringContents, identifierUseRe, normalizeChain, paramSlot, requestBindingsOf, SLOT_MARK, resolveStepValue, responseWaitsOf, simpleParamName, statementSpans, } from "./action-key.js";
|
|
5
5
|
import { realpath } from "./locate.js";
|
|
6
6
|
import { nonUtilsSiblings, } from "./call-sites.js";
|
|
7
7
|
/** A presence assertion as `[normalised locator, matcher]`, or undefined when the
|
|
@@ -38,6 +38,13 @@ function presenceWait(text, spec) {
|
|
|
38
38
|
* a sibling action with an undefined key equals nothing, so no run crosses it.
|
|
39
39
|
* Both fail open — no match, no finding — as STEP 3b's decline rule does.
|
|
40
40
|
*
|
|
41
|
+
* Two widenings (SKYR-4537), each held to boundRun's checks, so that every such
|
|
42
|
+
* site names the exact call that replaces it: the generator's own per-step bindings
|
|
43
|
+
* (`const playwrightRequestN = {…}`, `const responsePromiseN =
|
|
44
|
+
* page.waitForResponse(…)`, `const responseN = await responsePromiseN`) are
|
|
45
|
+
* transparent between two actions, and a helper whose locator argument is one of
|
|
46
|
+
* its parameters matches with the sibling's literal in that place.
|
|
47
|
+
*
|
|
41
48
|
* Where two helpers' keys start the same run, the longer wins; the run's actions are
|
|
42
49
|
* consumed and the scan continues after it. A run inside the sibling's own local
|
|
43
50
|
* helper is reported with that helper's name, as the API scan does.
|
|
@@ -55,25 +62,28 @@ export async function findSiblingInlineActionSites(testFile, helpersByFile, spec
|
|
|
55
62
|
const localHelpers = localHelpersOf(content, spec);
|
|
56
63
|
// Comments blanked, strings kept — the same reading the helper's key was made
|
|
57
64
|
// from, so a commented-out click is not a step and the selectors compare.
|
|
58
|
-
const
|
|
65
|
+
const blanked = blankComments(content, spec);
|
|
66
|
+
const steps = actionStepsOf(blanked, spec.language);
|
|
59
67
|
let i = 0;
|
|
60
68
|
while (i < steps.length) {
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
let run;
|
|
70
|
+
const match = sequences.find((s) => (run = runMatches(steps, i, s, spec, blanked)));
|
|
71
|
+
if (!match || !run) {
|
|
63
72
|
i++;
|
|
64
73
|
continue;
|
|
65
74
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
75
|
+
const owner = localHelperAt(localHelpers, steps[i].line);
|
|
76
|
+
// A local helper named as the shared one: a call inside it would call itself.
|
|
77
|
+
const call = owner?.name === match.helper ? undefined : run.call;
|
|
69
78
|
out.push({
|
|
70
79
|
kind: "sequence",
|
|
71
80
|
file,
|
|
72
|
-
line:
|
|
73
|
-
endLine:
|
|
74
|
-
actionKey:
|
|
81
|
+
line: run.line,
|
|
82
|
+
endLine: run.endLine,
|
|
83
|
+
actionKey: run.actionKey,
|
|
75
84
|
helper: match.helper,
|
|
76
85
|
utilsFile: match.utilsFile,
|
|
86
|
+
...(call ? { call } : {}),
|
|
77
87
|
...(owner ? { localHelper: owner.name } : {}),
|
|
78
88
|
});
|
|
79
89
|
i += match.steps.length;
|
|
@@ -81,29 +91,214 @@ export async function findSiblingInlineActionSites(testFile, helpersByFile, spec
|
|
|
81
91
|
}
|
|
82
92
|
return out;
|
|
83
93
|
}
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
94
|
+
/** The helper's sequence as the run of actions starting at `at`, with nothing
|
|
95
|
+
* between its actions that a helper body could not hold, and no wait in or right
|
|
96
|
+
* after the run that contradicts one of the helper's own — or `undefined`. A run
|
|
97
|
+
* that matches only through a generated binding or a wildcard is held to boundRun.
|
|
98
|
+
* `sibling` is the sibling's source, comments blanked, as `steps` were read. */
|
|
99
|
+
function runMatches(steps, at, helper, spec, sibling) {
|
|
88
100
|
const want = helper.steps;
|
|
89
101
|
if (at + want.length > steps.length)
|
|
90
|
-
return
|
|
102
|
+
return undefined;
|
|
103
|
+
const args = new Map();
|
|
104
|
+
const between = [];
|
|
91
105
|
for (let k = 0; k < want.length; k++) {
|
|
92
106
|
const step = steps[at + k];
|
|
93
|
-
if (step.key === undefined || step.key
|
|
94
|
-
return
|
|
107
|
+
if (step.key === undefined || !stepMatches(want[k], step.key, args))
|
|
108
|
+
return undefined;
|
|
95
109
|
if (k > 0) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return
|
|
99
|
-
if (contradictsWaits(
|
|
100
|
-
return
|
|
110
|
+
const gap = transparentStatements(step.gapBefore, spec);
|
|
111
|
+
if (gap === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (contradictsWaits(gap, helper.waits, spec))
|
|
114
|
+
return undefined;
|
|
115
|
+
between.push(...gap);
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
// The structural waits that follow the last action are the run's too: a wait the
|
|
104
119
|
// helper would perform, or its opposite.
|
|
105
120
|
const trailing = transparentStatements(steps[at + want.length - 1].gapAfter, spec, { prefix: true });
|
|
106
|
-
|
|
121
|
+
if (contradictsWaits(trailing ?? [], helper.waits, spec))
|
|
122
|
+
return undefined;
|
|
123
|
+
const run = steps.slice(at, at + want.length);
|
|
124
|
+
if (!helper.wildcard && !between.some(isGeneratedBinding)) {
|
|
125
|
+
// No call is named here, but the advisory still has the agent write one.
|
|
126
|
+
if (run.some((s, k) => liftsLiteralSecret(helper.values[k], s.value, resolveStepValue(s.value, new Map()))))
|
|
127
|
+
return undefined;
|
|
128
|
+
return {
|
|
129
|
+
line: run[0].line,
|
|
130
|
+
endLine: run[run.length - 1].endLine,
|
|
131
|
+
actionKey: helper.key,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return boundRun(steps[at - 1], run, between, helper, args, sibling, spec);
|
|
135
|
+
}
|
|
136
|
+
/** Whether a sibling step's key equals the helper's, a wildcard taking the literal
|
|
137
|
+
* in its place — recorded in `args`, and refused when the same parameter has
|
|
138
|
+
* already taken a different one. */
|
|
139
|
+
function stepMatches(want, got, args) {
|
|
140
|
+
if (!want.includes(SLOT_MARK))
|
|
141
|
+
return want === got;
|
|
142
|
+
const parts = want.split(SLOT_RE);
|
|
143
|
+
const literal = new RegExp("^" +
|
|
144
|
+
parts
|
|
145
|
+
.map((p, i) => i % 2
|
|
146
|
+
? '("(?:[^"\\\\]|\\\\.)*")'
|
|
147
|
+
: p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
148
|
+
.join("") +
|
|
149
|
+
"$").exec(got);
|
|
150
|
+
if (!literal)
|
|
151
|
+
return false;
|
|
152
|
+
return parts
|
|
153
|
+
.filter((_, i) => i % 2)
|
|
154
|
+
.every((name, i) => bindArg(args, name, literal[i + 1]));
|
|
155
|
+
}
|
|
156
|
+
/** A wildcard in a key: the parameter's name between paramSlot's markers. */
|
|
157
|
+
const SLOT_RE = new RegExp(`"${paramSlot("([A-Za-z_$][\\w$]*)")}"`);
|
|
158
|
+
/** Records `value` as the argument for `name`; false when `name` already has a
|
|
159
|
+
* different one — one parameter cannot carry two values. */
|
|
160
|
+
function bindArg(args, name, value) {
|
|
161
|
+
const held = args.get(name);
|
|
162
|
+
if (held === undefined)
|
|
163
|
+
args.set(name, value);
|
|
164
|
+
return held === undefined || normalizeChain(held) === normalizeChain(value);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A run that matched through a generated binding or a wildcard: the call that
|
|
168
|
+
* replaces it, with the range that call replaces — or `undefined`, when one call
|
|
169
|
+
* cannot replace it exactly:
|
|
170
|
+
*
|
|
171
|
+
* - **The range.** Grown back over the generated declarations right before the
|
|
172
|
+
* first action and forward over the `await responsePromiseN` right after the
|
|
173
|
+
* last, so a binding and its use go together.
|
|
174
|
+
* - **No dangling name.** Every generated name the range uses is declared in it,
|
|
175
|
+
* and no name it declares is used outside it.
|
|
176
|
+
* - **Same response waits.** The range's `waitForResponse`s and the helper's are
|
|
177
|
+
* equal as multisets: a wait the helper lacks would be lost, and one the range
|
|
178
|
+
* lacks would wait for a response the sibling may never cause.
|
|
179
|
+
* - **No lost assertion.** Between two actions, only generated bindings, waits and
|
|
180
|
+
* the presence waits the helper itself performs: any other assertion would be
|
|
181
|
+
* deleted with the range.
|
|
182
|
+
* - **No lifted secret.** A password- or secret-named value is not lifted into a
|
|
183
|
+
* literal argument of hand-maintained code.
|
|
184
|
+
* - **Values.** A value the helper takes as a parameter becomes that argument —
|
|
185
|
+
* a binding's literal lifted out of `getValue(playwrightRequestN, …)`; any other
|
|
186
|
+
* value must equal the helper's. Each parameter maps to ONE value, and every
|
|
187
|
+
* parameter but `page` and a trailing defaulted one is given one.
|
|
188
|
+
*/
|
|
189
|
+
function boundRun(before, run, between, helper, args, sibling, spec) {
|
|
190
|
+
for (const s of between) {
|
|
191
|
+
if (isGeneratedBinding(s))
|
|
192
|
+
continue;
|
|
193
|
+
const body = s.replace(/^await\s+/, "");
|
|
194
|
+
if (WAIT_STATEMENT_RE.test(blankStringContents(body)))
|
|
195
|
+
continue;
|
|
196
|
+
const wait = presenceWait(body, spec);
|
|
197
|
+
if (!wait || helper.waits.get(wait[0]) !== wait[1])
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const first = run[0];
|
|
201
|
+
const last = run[run.length - 1];
|
|
202
|
+
let line = first.line;
|
|
203
|
+
let endLine = last.endLine;
|
|
204
|
+
const lead = before ? spansOf(before.gapAfter) : [];
|
|
205
|
+
let from = lead.length;
|
|
206
|
+
while (from > 0 && DECLARATION_RE.test(lead[from - 1].structure))
|
|
207
|
+
from--;
|
|
208
|
+
if (from < lead.length)
|
|
209
|
+
line = lead[from].line + before.endLine - 1;
|
|
210
|
+
const trail = spansOf(last.gapAfter);
|
|
211
|
+
let to = 0;
|
|
212
|
+
while (to < trail.length && RESPONSE_AWAIT_RE.test(trail[to].structure))
|
|
213
|
+
to++;
|
|
214
|
+
if (to > 0)
|
|
215
|
+
endLine = trail[to - 1].endLine + last.endLine - 1;
|
|
216
|
+
const range = [
|
|
217
|
+
...lead.slice(from).map((s) => s.text),
|
|
218
|
+
...between,
|
|
219
|
+
...trail.slice(0, to).map((s) => s.text),
|
|
220
|
+
];
|
|
221
|
+
const declared = new Set(range.flatMap((s) => [...s.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)/g)].map((m) => m[1])));
|
|
222
|
+
const used = [...range, ...run.map((s) => s.value)].flatMap((s) => [...s.matchAll(GENERATED_NAME_RE)].map((m) => m[0]));
|
|
223
|
+
if (used.some((name) => !declared.has(name)))
|
|
224
|
+
return undefined;
|
|
225
|
+
const lines = sibling.split("\n");
|
|
226
|
+
for (const name of declared) {
|
|
227
|
+
const use = identifierUseRe(name);
|
|
228
|
+
if (lines.some((l, i) => (i + 1 < line || i + 1 > endLine) && use.test(l)))
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const rangeWaits = range.flatMap(responseWaitsOf).sort();
|
|
232
|
+
const helperWaits = [...helper.responseWaits].sort();
|
|
233
|
+
if (rangeWaits.length !== helperWaits.length ||
|
|
234
|
+
rangeWaits.some((w, i) => w !== helperWaits[i]))
|
|
235
|
+
return undefined;
|
|
236
|
+
const bindings = requestBindingsOf(range.join("\n"));
|
|
237
|
+
for (let k = 0; k < run.length; k++) {
|
|
238
|
+
const want = helper.values[k];
|
|
239
|
+
const got = resolveStepValue(run[k].value, bindings);
|
|
240
|
+
// A value read through a generated binding this cannot resolve: the same
|
|
241
|
+
// `playwrightRequestN` text holds a different value in each spec.
|
|
242
|
+
const opaque = (v) => v.kind === "expr" && v.text.match(GENERATED_NAME_RE) !== null;
|
|
243
|
+
const ok = !liftsLiteralSecret(want, run[k].value, got) &&
|
|
244
|
+
!opaque(want) &&
|
|
245
|
+
!opaque(got) &&
|
|
246
|
+
(want.suffix ?? "") === (got.suffix ?? "") &&
|
|
247
|
+
(want.kind === "param"
|
|
248
|
+
? ![...declared].some((n) => identifierUseRe(n).test(got.text)) &&
|
|
249
|
+
bindArg(args, want.text, got.text)
|
|
250
|
+
: normalizeChain(want.text) === normalizeChain(got.text));
|
|
251
|
+
if (!ok)
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
const call = callOf(helper, args);
|
|
255
|
+
if (call === undefined)
|
|
256
|
+
return undefined;
|
|
257
|
+
return {
|
|
258
|
+
line,
|
|
259
|
+
endLine,
|
|
260
|
+
actionKey: helper.wildcard ? run.map((s) => s.key).join("; ") : helper.key,
|
|
261
|
+
call,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** The statements of a gap, each with its text, its string-blanked text, and its
|
|
265
|
+
* first and last line counted from the gap's own first line (1). */
|
|
266
|
+
function spansOf(gap) {
|
|
267
|
+
const structure = blankStringContents(gap);
|
|
268
|
+
const lineAt = (i) => gap.slice(0, i).split("\n").length;
|
|
269
|
+
return statementSpans(structure)
|
|
270
|
+
.map(([from, to]) => ({
|
|
271
|
+
text: gap.slice(from, to).trim(),
|
|
272
|
+
structure: structure.slice(from, to).trim(),
|
|
273
|
+
line: lineAt(from),
|
|
274
|
+
endLine: lineAt(to),
|
|
275
|
+
}))
|
|
276
|
+
.filter((s) => s.text !== "await");
|
|
277
|
+
}
|
|
278
|
+
/** `await createOrder(page, "a@b.c", "option-1")`: the helper's call with each
|
|
279
|
+
* parameter's argument, or `undefined` when a parameter has none — `page` is the
|
|
280
|
+
* page, and a defaulted or optional parameter with nothing after it is left out —
|
|
281
|
+
* unless the body reads it: `express = false` gating a click the sibling makes. */
|
|
282
|
+
function callOf(helper, args) {
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const param of helper.params) {
|
|
285
|
+
const name = simpleParamName(param);
|
|
286
|
+
if (!name)
|
|
287
|
+
return undefined;
|
|
288
|
+
const optional = /^\s*[\w$]+\s*\?/.test(param) || defaultAt(param) !== -1;
|
|
289
|
+
out.push(args.get(name) ??
|
|
290
|
+
(name === "page" || /:\s*Page\b/.test(param)
|
|
291
|
+
? "page"
|
|
292
|
+
: optional && !helper.usedParams.includes(name)
|
|
293
|
+
? undefined
|
|
294
|
+
: "\0"));
|
|
295
|
+
}
|
|
296
|
+
while (out.length > 0 && out[out.length - 1] === undefined)
|
|
297
|
+
out.pop();
|
|
298
|
+
if (out.some((a) => a === undefined || a === "\0"))
|
|
299
|
+
return undefined;
|
|
300
|
+
// The browser scan runs for TypeScript and JavaScript only (inlineCallSiteFamily).
|
|
301
|
+
return `await ${helper.helper}(${out.join(", ")})`;
|
|
107
302
|
}
|
|
108
303
|
/** Whether one of `statements` asserts an element the helper waits on with a
|
|
109
304
|
* different matcher or negation. */
|
|
@@ -120,8 +315,9 @@ function contradictsWaits(statements, waits, spec) {
|
|
|
120
315
|
}
|
|
121
316
|
/**
|
|
122
317
|
* The statements of `gap` that a shared browser helper may hold — a permitted
|
|
123
|
-
* structural wait, as the verify pass judges one (`HELPER_FAMILIES.browser`),
|
|
124
|
-
* `waitFor
|
|
318
|
+
* structural wait, as the verify pass judges one (`HELPER_FAMILIES.browser`), a
|
|
319
|
+
* `waitFor…`, or between two actions one of the generator's per-step bindings
|
|
320
|
+
* (isGeneratedBinding; boundRun carries it) — or `undefined` when one is not, or when the gap closes a bracket it
|
|
125
321
|
* did not open. With `prefix`, the leading transparent statements up to the first
|
|
126
322
|
* that is not: the waits that follow a run, read until the run's own text ends.
|
|
127
323
|
* Statements are cut as statementSpans cuts them, on a string-blanked copy, so a
|
|
@@ -140,6 +336,7 @@ function transparentStatements(gap, spec, opts = {}) {
|
|
|
140
336
|
continue;
|
|
141
337
|
const body = text.replace(/^await\s+/, "");
|
|
142
338
|
const transparent = WAIT_STATEMENT_RE.test(blankStringContents(body)) ||
|
|
339
|
+
(!opts.prefix && isGeneratedBinding(text)) ||
|
|
143
340
|
(spec.assertionRe.test(body) &&
|
|
144
341
|
HELPER_FAMILIES.browser.isAllowed(spec, body));
|
|
145
342
|
if (!transparent)
|
|
@@ -161,9 +358,36 @@ function leavesBlock(structure) {
|
|
|
161
358
|
}
|
|
162
359
|
/** `page.waitForTimeout(3000)`, `page.waitForLoadState()`, `page.getByTestId("x")
|
|
163
360
|
* .waitFor({ state: "visible" })` — a wait on the page or on a locator chain, as a
|
|
164
|
-
* statement of its own (
|
|
165
|
-
*
|
|
361
|
+
* statement of its own (a BOUND wait is used later: only the generator's own, see
|
|
362
|
+
* isGeneratedBinding, is carried, by boundRun). */
|
|
166
363
|
const WAIT_STATEMENT_RE = /^(?:this\.)?page(?:\s*\.\s*[\w$]+\s*\([^;]*\))*\s*\.\s*(?:waitFor\w*|wait_for\w*)\s*\([^;]*\)\s*$/;
|
|
364
|
+
/** The generator's per-step declarations, string-blanked: `const playwrightRequestN:
|
|
365
|
+
* Record<string, any> = {…}` (the value the next fill reads) and `const
|
|
366
|
+
* responsePromiseN = page.waitForResponse(…)`. A hand-written binding is not one. */
|
|
367
|
+
const DECLARATION_RE = /^const\s+(?:playwrightRequest\d+\s*(?::\s*Record\s*<[^>]*>)?\s*=\s*\{[^;]*\}|responsePromise\d+\s*=\s*(?:this\.)?page\s*\.\s*waitForResponse\s*\([^;]*\))\s*;?$/;
|
|
368
|
+
/** The generator's `const responseN = await responsePromiseN`, string-blanked. */
|
|
369
|
+
const RESPONSE_AWAIT_RE = /^(?:const\s+response\d+\s*=\s*)?await\s+responsePromise\d+\s*;?$/;
|
|
370
|
+
/** A parameter or binding key that holds a secret: its value stays out of a call. */
|
|
371
|
+
const SECRET_NAME_RE = /password|passwd|secret|token|api_?key/i;
|
|
372
|
+
/** Whether a step would lift a literal secret into a call argument — a password-
|
|
373
|
+
* or secret-named parameter (or `getValue` key) given a literal — on either path:
|
|
374
|
+
* the literal stays out of hand-maintained code. A variable (`loginPassword`) is
|
|
375
|
+
* no secret in itself and may be passed. */
|
|
376
|
+
function liftsLiteralSecret(want, raw, got) {
|
|
377
|
+
return (want.kind === "param" &&
|
|
378
|
+
got.kind === "literal" &&
|
|
379
|
+
(SECRET_NAME_RE.test(want.text) ||
|
|
380
|
+
SECRET_NAME_RE.test(GET_VALUE_KEY_RE.exec(raw)?.[1] ?? "")));
|
|
381
|
+
}
|
|
382
|
+
/** The key a `getValue(binding, "key")` step value reads. */
|
|
383
|
+
const GET_VALUE_KEY_RE = /^\s*getValue\s*\([^,]*,\s*["']([^"']*)["']/;
|
|
384
|
+
/** A name the generator binds per step. */
|
|
385
|
+
const GENERATED_NAME_RE = /\b(?:playwrightRequest|responsePromise|response)\d+\b/g;
|
|
386
|
+
/** One of the generator's per-step bindings (statement text as written). */
|
|
387
|
+
function isGeneratedBinding(text) {
|
|
388
|
+
const structure = blankStringContents(text);
|
|
389
|
+
return DECLARATION_RE.test(structure) || RESPONSE_AWAIT_RE.test(structure);
|
|
390
|
+
}
|
|
167
391
|
/** A step whose receiver is the keyboard or the mouse rather than a locator. */
|
|
168
392
|
function isElementlessStep(step) {
|
|
169
393
|
return /^\w+ (?:this\.)?page\.(?:keyboard|mouse)(?:\s|$)/.test(step);
|
|
@@ -176,27 +400,45 @@ function helperSequences(helpersByFile, spec) {
|
|
|
176
400
|
const seen = new Set();
|
|
177
401
|
for (const [utilsFile, helpers] of helpersByFile)
|
|
178
402
|
for (const h of helpers) {
|
|
179
|
-
|
|
403
|
+
const pattern = h.actionPattern;
|
|
404
|
+
// No key: a step whose locator is fed by a parameter is a wildcard (SKYR-4537).
|
|
405
|
+
const steps = h.actionSteps ?? pattern?.steps;
|
|
406
|
+
const key = h.actionKey ?? steps?.join("; ");
|
|
407
|
+
if (!key || !steps || !pattern || seen.has(key))
|
|
180
408
|
continue;
|
|
181
409
|
// A key of bare keyboard or mouse steps names no element: `press "Escape"`
|
|
182
410
|
// is what closes a dialog in one helper and a menu in the next, and every
|
|
183
411
|
// Escape in every sibling matched it (measured: fixture 22, the advisory
|
|
184
412
|
// named a menu close for a dialog-closing helper). One locator-bearing step
|
|
185
413
|
// is enough to identify the sequence.
|
|
186
|
-
if (
|
|
414
|
+
if (key.split("; ").every(isElementlessStep))
|
|
415
|
+
continue;
|
|
416
|
+
// A wildcard widens what a step equals, so the sequence must still be named
|
|
417
|
+
// by its literal steps: at least two, and more than its wildcard steps — a
|
|
418
|
+
// `clickByTestId(page, id)` would otherwise equal every such click.
|
|
419
|
+
const wildcard = !h.actionSteps;
|
|
420
|
+
const literal = steps.filter((s) => !s.includes(SLOT_MARK) && !isElementlessStep(s)).length;
|
|
421
|
+
if (wildcard && (literal < 2 || literal <= steps.length - literal))
|
|
187
422
|
continue;
|
|
188
|
-
seen.add(
|
|
423
|
+
seen.add(key);
|
|
189
424
|
out.push({
|
|
190
425
|
helper: h.name,
|
|
191
426
|
utilsFile,
|
|
192
|
-
key
|
|
193
|
-
steps
|
|
427
|
+
key,
|
|
428
|
+
steps,
|
|
429
|
+
wildcard,
|
|
430
|
+
params: h.params ?? [],
|
|
431
|
+
values: pattern.values,
|
|
432
|
+
responseWaits: pattern.responseWaits,
|
|
433
|
+
usedParams: pattern.usedParams,
|
|
194
434
|
waits: new Map(h.assertions
|
|
195
435
|
.map((a) => presenceWait(a.text, spec))
|
|
196
436
|
.filter((w) => w !== undefined)),
|
|
197
437
|
});
|
|
198
438
|
}
|
|
199
439
|
// Longest first, so the run a longer sequence covers is not claimed by a shorter
|
|
200
|
-
// helper that shares its opening actions
|
|
201
|
-
|
|
440
|
+
// helper that shares its opening actions; at equal length, a literal sequence
|
|
441
|
+
// before a wildcard one.
|
|
442
|
+
return out.sort((a, b) => b.steps.length - a.steps.length ||
|
|
443
|
+
Number(a.wildcard) - Number(b.wildcard));
|
|
202
444
|
}
|
|
@@ -35,7 +35,8 @@ export declare const REQUEST_CALL_RE: RegExp;
|
|
|
35
35
|
interface InlineSiteBase {
|
|
36
36
|
/** Absolute path of the sibling test. */
|
|
37
37
|
file: string;
|
|
38
|
-
/** 1-based line of the request call, or of the first action of the sequence
|
|
38
|
+
/** 1-based line of the request call, or of the first action of the sequence (or
|
|
39
|
+
* of the generated declaration that precedes it, SKYR-4537). */
|
|
39
40
|
line: number;
|
|
40
41
|
/** The utils helper that wraps the same method+path, or performs the same
|
|
41
42
|
* action sequence. */
|
|
@@ -58,8 +59,13 @@ export type InlineCallSite = (InlineSiteBase & {
|
|
|
58
59
|
kind: "sequence";
|
|
59
60
|
/** The action key the sequence and the helper share (see actionKeyOf). */
|
|
60
61
|
actionKey: string;
|
|
61
|
-
/** 1-based line of the sequence's last action
|
|
62
|
+
/** 1-based line of the sequence's last action — or of the generated
|
|
63
|
+
* `await responsePromiseN` that goes with it (SKYR-4537). */
|
|
62
64
|
endLine: number;
|
|
65
|
+
/** The call that replaces lines `line`–`endLine`, arguments included, when
|
|
66
|
+
* the run matched through a generated binding or a parameter-fed locator
|
|
67
|
+
* (SKYR-4537): such a site is reported only when this can be named. */
|
|
68
|
+
call?: string;
|
|
63
69
|
});
|
|
64
70
|
/**
|
|
65
71
|
* Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
|
|
@@ -355,7 +355,7 @@ export const HELPER_FAMILIES = {
|
|
|
355
355
|
inlineSite: {
|
|
356
356
|
noun: BROWSER_HELPERS.inlineSite.noun,
|
|
357
357
|
already: "performs",
|
|
358
|
-
instruction: "Each is a candidate: the sequence's actions and locators equal the helper's, in order. Replace the sequence with a call to the helper only if the remaining differences are literals — fill values, option labels, literal arguments — which become the arguments (import the helper into that file; every assertion and the page-error guard stay in the test body where they are; change nothing else there).",
|
|
358
|
+
instruction: "Each is a candidate: the sequence's actions and locators equal the helper's, in order, except a locator the helper takes as a parameter, which becomes that argument. Replace the sequence with a call to the helper only if the remaining differences are literals — fill values, option labels, literal arguments — which become the arguments (import the helper into that file; every assertion and the page-error guard stay in the test body where they are; change nothing else there).",
|
|
359
359
|
localCopy: "the sequence",
|
|
360
360
|
},
|
|
361
361
|
},
|