donobu 5.33.0 → 5.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/lib/ai/cache/assertCache.d.ts +14 -0
- package/dist/esm/lib/ai/cache/assertCache.js +24 -7
- package/dist/esm/lib/ai/locate/buildLocator.d.ts +5 -1
- package/dist/esm/lib/ai/locate/buildLocator.js +63 -14
- package/dist/esm/lib/ai/locate/locateElement.d.ts +1 -0
- package/dist/esm/lib/ai/locate/locateElement.js +107 -10
- package/dist/esm/lib/ai/locate/locateSchema.d.ts +2 -0
- package/dist/esm/lib/ai/locate/locateSchema.js +9 -1
- package/dist/esm/lib/ai/locate/locateTypes.d.ts +59 -2
- package/dist/esm/lib/page/DonobuExtendedPage.d.ts +24 -0
- package/dist/esm/lib/page/extendPage.js +125 -27
- package/dist/esm/managers/ToolManager.js +3 -0
- package/dist/esm/models/ToolCallContext.d.ts +11 -0
- package/dist/esm/tools/AssertTool.js +32 -2
- package/dist/lib/ai/cache/assertCache.d.ts +14 -0
- package/dist/lib/ai/cache/assertCache.js +24 -7
- package/dist/lib/ai/locate/buildLocator.d.ts +5 -1
- package/dist/lib/ai/locate/buildLocator.js +63 -14
- package/dist/lib/ai/locate/locateElement.d.ts +1 -0
- package/dist/lib/ai/locate/locateElement.js +107 -10
- package/dist/lib/ai/locate/locateSchema.d.ts +2 -0
- package/dist/lib/ai/locate/locateSchema.js +9 -1
- package/dist/lib/ai/locate/locateTypes.d.ts +59 -2
- package/dist/lib/page/DonobuExtendedPage.d.ts +24 -0
- package/dist/lib/page/extendPage.js +125 -27
- package/dist/managers/ToolManager.js +3 -0
- package/dist/models/ToolCallContext.d.ts +11 -0
- package/dist/tools/AssertTool.js +32 -2
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ const GptApiKeysNotSetupException_1 = require("../../exceptions/GptApiKeysNotSet
|
|
|
10
10
|
const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
|
|
11
11
|
const ToolCallFailedException_1 = require("../../exceptions/ToolCallFailedException");
|
|
12
12
|
const ToolRequiresGptException_1 = require("../../exceptions/ToolRequiresGptException");
|
|
13
|
+
const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager");
|
|
13
14
|
const InteractionVisualizer_1 = require("../../managers/InteractionVisualizer");
|
|
14
15
|
const ToolManager_1 = require("../../managers/ToolManager");
|
|
15
16
|
const WebTargetInspector_1 = require("../../managers/WebTargetInspector");
|
|
@@ -220,6 +221,33 @@ Valid options:
|
|
|
220
221
|
const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
|
|
221
222
|
const retries = options?.retries ?? 0;
|
|
222
223
|
const retryDelaySeconds = options?.retryDelaySeconds ?? 3;
|
|
224
|
+
// Distill env var names from `{{$.env.*}}` interpolations in the
|
|
225
|
+
// assertion plus any explicitly provided names/overrides. Cached
|
|
226
|
+
// Playwright steps may carry the same `{{$.env.X}}` placeholders in
|
|
227
|
+
// their `value`/`attributeValue` fields, so we resolve env data at
|
|
228
|
+
// replay time and let the executor interpolate before applying.
|
|
229
|
+
const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(assertion, [
|
|
230
|
+
...(options?.envVars ?? []),
|
|
231
|
+
...Object.keys(options?.envVals ?? {}),
|
|
232
|
+
]);
|
|
233
|
+
const hasEnvRefs = envVarNames.length > 0;
|
|
234
|
+
const resolveEnvData = async () => {
|
|
235
|
+
if (!hasEnvRefs) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const envData = await sharedState.donobuStack.envDataManager.getByNames(envVarNames);
|
|
239
|
+
if (options?.envVals) {
|
|
240
|
+
for (const [k, v] of Object.entries(options.envVals)) {
|
|
241
|
+
if (v === undefined) {
|
|
242
|
+
delete envData[k];
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
envData[k] = v;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return envData;
|
|
250
|
+
};
|
|
223
251
|
// --- Cache lookup (when enabled and not clearing) ---
|
|
224
252
|
if (useCache && !clearCache) {
|
|
225
253
|
const cache = getOrInitPageAiCache();
|
|
@@ -227,6 +255,7 @@ Valid options:
|
|
|
227
255
|
const cached = await cache.getAssert({ pageUrl, assertion });
|
|
228
256
|
if (cached) {
|
|
229
257
|
Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
|
|
258
|
+
const envData = await resolveEnvData();
|
|
230
259
|
let lastError = null;
|
|
231
260
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
232
261
|
if (attempt > 0) {
|
|
@@ -234,7 +263,7 @@ Valid options:
|
|
|
234
263
|
await page.waitForTimeout(retryDelaySeconds * 1000);
|
|
235
264
|
}
|
|
236
265
|
try {
|
|
237
|
-
await cached.run({ page });
|
|
266
|
+
await cached.run({ page, envData });
|
|
238
267
|
return; // Assertion passed
|
|
239
268
|
}
|
|
240
269
|
catch (error) {
|
|
@@ -263,12 +292,28 @@ Valid options:
|
|
|
263
292
|
await cache.deleteAssert({ pageUrl, assertion });
|
|
264
293
|
Logger_1.appLogger.debug(`Assert cache invalidated for: "${assertion}"`);
|
|
265
294
|
}
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
// Make env vars available to runTool's envData for `{{$.env.*}}`
|
|
296
|
+
// interpolation inside `assertionToTestFor` and so AssertTool can
|
|
297
|
+
// instruct the AI to emit placeholders in cached step values. Mirrors
|
|
298
|
+
// PageAi.ai for `act`: metadata.envVars is set (overwriting), envVals
|
|
299
|
+
// is restored.
|
|
300
|
+
if (hasEnvRefs) {
|
|
301
|
+
sharedState.donobuFlowMetadata.envVars = envVarNames;
|
|
302
|
+
}
|
|
303
|
+
const previousEnvVals = sharedState.envVals;
|
|
304
|
+
sharedState.envVals = options?.envVals;
|
|
305
|
+
let result;
|
|
306
|
+
try {
|
|
307
|
+
// --- Cache miss or cache disabled: run AI assertion ---
|
|
308
|
+
result = await runTool(page, AssertTool_1.AssertTool.NAME, {
|
|
309
|
+
assertionToTestFor: assertion,
|
|
310
|
+
retries: options?.retries,
|
|
311
|
+
retryWaitSeconds: options?.retryDelaySeconds,
|
|
312
|
+
}, options?.gptClient);
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
sharedState.envVals = previousEnvVals;
|
|
316
|
+
}
|
|
272
317
|
if (!result.outcome.isSuccessful) {
|
|
273
318
|
throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
|
|
274
319
|
}
|
|
@@ -363,33 +408,86 @@ Use this information to return an appropriate JSON object.`,
|
|
|
363
408
|
const useCache = options?.cache !== false;
|
|
364
409
|
const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
|
|
365
410
|
const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
411
|
+
// Distill env var names referenced by the description plus any
|
|
412
|
+
// explicitly provided names/overrides. Resolve env data locally — locate
|
|
413
|
+
// does not flow through `runTool`, so we don't mutate sharedState here.
|
|
414
|
+
const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(description, [
|
|
415
|
+
...(options?.envVars ?? []),
|
|
416
|
+
...Object.keys(options?.envVals ?? {}),
|
|
417
|
+
]);
|
|
418
|
+
const hasEnvRefs = envVarNames.length > 0;
|
|
419
|
+
const resolveEnvData = async () => {
|
|
420
|
+
if (!hasEnvRefs) {
|
|
421
|
+
return undefined;
|
|
373
422
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
423
|
+
const envData = await sharedState.donobuStack.envDataManager.getByNames(envVarNames);
|
|
424
|
+
if (options?.envVals) {
|
|
425
|
+
for (const [k, v] of Object.entries(options.envVals)) {
|
|
426
|
+
if (v === undefined) {
|
|
427
|
+
delete envData[k];
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
envData[k] = v;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return envData;
|
|
435
|
+
};
|
|
436
|
+
// The user-supplied `timeout` (default 30s) is the budget for the
|
|
437
|
+
// ENTIRE locate operation — cache-hit hydration wait + AI fallback.
|
|
438
|
+
// We start the abort timer here so the cache path's `waitFor` and the
|
|
439
|
+
// AI path share one bounded clock.
|
|
386
440
|
const timeoutMillis = options?.timeout ?? 30_000;
|
|
441
|
+
const startedAt = Date.now();
|
|
387
442
|
const abortController = new AbortController();
|
|
388
443
|
const timeoutId = setTimeout(() => {
|
|
389
444
|
abortController.abort(`Locate operation timed out after ${timeoutMillis} milliseconds`);
|
|
390
445
|
}, timeoutMillis);
|
|
391
446
|
try {
|
|
392
|
-
|
|
447
|
+
// --- Cache lookup (when enabled and not clearing) ---
|
|
448
|
+
if (useCache && !clearCache) {
|
|
449
|
+
const cache = getOrInitPageAiCache();
|
|
450
|
+
const cached = await cache.getLocate({ pageUrl, description });
|
|
451
|
+
if (cached) {
|
|
452
|
+
const envData = await resolveEnvData();
|
|
453
|
+
const candidate = cached.run({ page, envData });
|
|
454
|
+
// Cache replay can outrun page hydration — the no-cache path
|
|
455
|
+
// gets an implicit hydration window from the AI round-trip
|
|
456
|
+
// latency, but a cache hit fires immediately and may see a
|
|
457
|
+
// partially-mounted DOM. Wait (within the operation's overall
|
|
458
|
+
// budget) for the locator to attach before validating.
|
|
459
|
+
const remaining = Math.max(timeoutMillis - (Date.now() - startedAt), 100);
|
|
460
|
+
try {
|
|
461
|
+
await candidate.first().waitFor({
|
|
462
|
+
state: 'attached',
|
|
463
|
+
timeout: remaining,
|
|
464
|
+
});
|
|
465
|
+
Logger_1.appLogger.debug(`Locate cache HIT for: "${description}" — rebuilt locator from cache`);
|
|
466
|
+
return candidate;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Locator did not attach within the patience window. Either
|
|
470
|
+
// the page has drifted or the cache is genuinely stale.
|
|
471
|
+
// Invalidate and fall through to the AI path; the AI call
|
|
472
|
+
// gets whatever budget remains on the abort timer.
|
|
473
|
+
Logger_1.appLogger.debug(`Locate cache STALE for "${description}" (no match within ${remaining}ms) — re-running AI`);
|
|
474
|
+
await cache.deleteLocate({ pageUrl, description });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// --- Cache invalidation (when clearing) ---
|
|
479
|
+
if (useCache && clearCache) {
|
|
480
|
+
const cache = getOrInitPageAiCache();
|
|
481
|
+
await cache.deleteLocate({ pageUrl, description });
|
|
482
|
+
Logger_1.appLogger.debug(`Locate cache invalidated for: "${description}"`);
|
|
483
|
+
}
|
|
484
|
+
// --- Cache miss / cache disabled / stale-cache fallthrough: run AI ---
|
|
485
|
+
const gptClient = getGptClient(page, options?.gptClient);
|
|
486
|
+
if (!gptClient) {
|
|
487
|
+
throw new ToolRequiresGptException_1.ToolRequiresGptException('locate');
|
|
488
|
+
}
|
|
489
|
+
const envData = await resolveEnvData();
|
|
490
|
+
const { locator, result } = await (0, locateElement_1.locateElement)(page, description, gptClient, { signal: abortController.signal, envData });
|
|
393
491
|
// --- Cache the result for future runs ---
|
|
394
492
|
if (useCache) {
|
|
395
493
|
try {
|
|
@@ -87,6 +87,9 @@ class ToolManager {
|
|
|
87
87
|
startedAt: startedAt,
|
|
88
88
|
completedAt: null,
|
|
89
89
|
});
|
|
90
|
+
// Expose the un-interpolated parameters so tools that need to
|
|
91
|
+
// preserve `{{...}}` references (e.g. AssertTool) can read them.
|
|
92
|
+
context.rawParameters = toolParameters;
|
|
90
93
|
// Use the interpolated parameters when calling the tool.
|
|
91
94
|
toolCallResult = isFromGpt
|
|
92
95
|
? await tool.callFromGpt(context, tool.inputSchemaForGpt.parse(interpolatedParameters))
|
|
@@ -22,5 +22,16 @@ export type ToolCallContext = {
|
|
|
22
22
|
readonly invokedToolCalls: ToolCall[];
|
|
23
23
|
readonly metadata: FlowMetadata;
|
|
24
24
|
readonly toolCallId: string;
|
|
25
|
+
/**
|
|
26
|
+
* Original (un-interpolated) parameters supplied to the current tool call.
|
|
27
|
+
*
|
|
28
|
+
* `ToolManager.invokeTool` interpolates `{{...}}` expressions in the tool's
|
|
29
|
+
* parameters before invoking the tool, but a tool may need the raw text to
|
|
30
|
+
* preserve env var references in its output (e.g. `AssertTool` emits
|
|
31
|
+
* Playwright steps that retain `{{$.env.X}}` so cached replays stay correct
|
|
32
|
+
* across env value changes). Set by `ToolManager` immediately before the
|
|
33
|
+
* tool runs; absent for direct/legacy invocation paths.
|
|
34
|
+
*/
|
|
35
|
+
rawParameters?: Record<string, any>;
|
|
25
36
|
};
|
|
26
37
|
//# sourceMappingURL=ToolCallContext.d.ts.map
|
|
@@ -124,6 +124,36 @@ It will use a screenshot of the current viewport of the webpage, the webpage's t
|
|
|
124
124
|
Logger_1.appLogger.warn(msg);
|
|
125
125
|
return msg;
|
|
126
126
|
});
|
|
127
|
+
const rawAssertion = typeof context.rawParameters?.assertionToTestFor === 'string'
|
|
128
|
+
? context.rawParameters.assertionToTestFor
|
|
129
|
+
: parameters.assertionToTestFor;
|
|
130
|
+
const envEntries = Object.entries(context.envData ?? {});
|
|
131
|
+
// Only treat env vars as "in play" when the raw assertion actually
|
|
132
|
+
// references one — keeps the prompt small for the common case.
|
|
133
|
+
const referencedEnvEntries = envEntries.filter(([name]) => rawAssertion.includes(`{{$.env.${name}}}`));
|
|
134
|
+
const hasEnvRefs = referencedEnvEntries.length > 0;
|
|
135
|
+
const envBlock = hasEnvRefs
|
|
136
|
+
? `
|
|
137
|
+
|
|
138
|
+
The user's original assertion contains environment variable references using the
|
|
139
|
+
syntax \`{{$.env.NAME}}\`. To keep cached Playwright steps valid across runs with
|
|
140
|
+
different env values, you MUST emit those same placeholders in any
|
|
141
|
+
playwrightAssertionStep \`value\`/\`attributeValue\` field whose contents come from
|
|
142
|
+
an env var. Do NOT bake the literal current value into the step.
|
|
143
|
+
|
|
144
|
+
Original (uninterpolated) assertion: ${rawAssertion}
|
|
145
|
+
|
|
146
|
+
Current env mapping (use these to identify which substrings on the page came
|
|
147
|
+
from which env var, then emit the placeholder rather than the literal):
|
|
148
|
+
${referencedEnvEntries.map(([name, value]) => ` - {{$.env.${name}}} = ${JSON.stringify(value)}`).join('\n')}
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
- Raw assertion "Welcome banner says hello {{$.env.USERNAME}}", USERNAME="alice", page shows "Welcome alice" →
|
|
152
|
+
[{ locator: "text", role: null, value: "{{$.env.USERNAME}}", valueIsRegex: false, assertion: "toBeVisible", attributeValue: null }]
|
|
153
|
+
- Raw assertion "The username field shows {{$.env.USERNAME}}", USERNAME="alice", page input value is "alice" →
|
|
154
|
+
[{ locator: "label", role: null, value: "Username", valueIsRegex: false, assertion: "toHaveValue", attributeValue: "{{$.env.USERNAME}}" }]
|
|
155
|
+
- For literal page text unrelated to env vars, keep the literal value as usual.`
|
|
156
|
+
: '';
|
|
127
157
|
const promptMessages = [
|
|
128
158
|
{
|
|
129
159
|
type: 'system',
|
|
@@ -142,7 +172,7 @@ CRITICAL RULES for generating structured steps — follow these precisely:
|
|
|
142
172
|
- Text input / textarea content: use 'toHaveValue' with locator='label' and set attributeValue to the expected text. Do NOT use 'toBeVisible' on the textbox.
|
|
143
173
|
- Selected tabs, pills, or items with aria-selected: use 'toHaveAttribute' with value='aria-selected' and attributeValue='true', NOT 'toBeVisible' on the text.
|
|
144
174
|
- Text content within an element: use 'toContainText' with attributeValue set to the substring, NOT 'toBeVisible'.
|
|
145
|
-
- Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks
|
|
175
|
+
- Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks.${envBlock}`,
|
|
146
176
|
},
|
|
147
177
|
{
|
|
148
178
|
type: 'user',
|
|
@@ -184,7 +214,7 @@ careful positioning lost, etc. A screenshot of the webpage has also been provide
|
|
|
184
214
|
verifiedSteps.length > 0) {
|
|
185
215
|
try {
|
|
186
216
|
const executor = (0, assertCache_1.buildAssertExecutor)(verifiedSteps);
|
|
187
|
-
await executor({ page: page });
|
|
217
|
+
await executor({ page: page, envData: context.envData });
|
|
188
218
|
}
|
|
189
219
|
catch (error) {
|
|
190
220
|
Logger_1.appLogger.debug(`Structured assertion steps failed verification for: "${parameters.assertionToTestFor}" — discarding steps. Error: ${error.message}`);
|
|
@@ -33,6 +33,13 @@ export declare const PlaywrightAssertionStepSchema: z.ZodObject<{
|
|
|
33
33
|
export type PlaywrightAssertionStep = z.infer<typeof PlaywrightAssertionStepSchema>;
|
|
34
34
|
export type AssertCacheExecutor = (context: {
|
|
35
35
|
page: Page;
|
|
36
|
+
/**
|
|
37
|
+
* Optional env mapping used to interpolate `{{$.env.X}}` placeholders that
|
|
38
|
+
* the AI may have embedded into step `value`/`attributeValue` fields. When
|
|
39
|
+
* absent, steps run unchanged (backwards compatible with cache entries
|
|
40
|
+
* recorded before env-aware caching).
|
|
41
|
+
*/
|
|
42
|
+
envData?: Record<string, string>;
|
|
36
43
|
}) => Promise<void>;
|
|
37
44
|
/**
|
|
38
45
|
* Builds an executor function from structured assertion steps.
|
|
@@ -85,6 +92,13 @@ export type LocateCacheEntryWithRunner = LocateCacheEntry & {
|
|
|
85
92
|
};
|
|
86
93
|
export type LocateCacheExecutor = (context: {
|
|
87
94
|
page: DonobuExtendedPage;
|
|
95
|
+
/**
|
|
96
|
+
* Optional env mapping used to interpolate `{{$.env.X}}` placeholders that
|
|
97
|
+
* the AI may have embedded into `LocatorStep.text`/`name`/`testId` fields.
|
|
98
|
+
* Absent → steps run unchanged (backwards compatible with cache entries
|
|
99
|
+
* recorded before env-aware caching).
|
|
100
|
+
*/
|
|
101
|
+
envData?: Record<string, string>;
|
|
88
102
|
}) => Locator;
|
|
89
103
|
/**
|
|
90
104
|
* Builds a cache executor that mechanically reconstructs a Playwright
|
|
@@ -5,6 +5,7 @@ exports.buildAssertExecutor = buildAssertExecutor;
|
|
|
5
5
|
exports.buildLocateExecutor = buildLocateExecutor;
|
|
6
6
|
const test_1 = require("@playwright/test");
|
|
7
7
|
const v4_1 = require("zod/v4");
|
|
8
|
+
const TemplateInterpolator_1 = require("../../../utils/TemplateInterpolator");
|
|
8
9
|
const buildLocator_1 = require("../locate/buildLocator");
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Structured assertion step schema
|
|
@@ -84,17 +85,33 @@ Common roles: 'heading', 'button', 'link', 'tab', 'tabpanel', 'dialog', 'navigat
|
|
|
84
85
|
- toContainText: set to the text substring to match within the element
|
|
85
86
|
- All other assertions: set to null`),
|
|
86
87
|
});
|
|
88
|
+
/**
|
|
89
|
+
* Resolves any `{{$.env.X}}` placeholders in a step field against the
|
|
90
|
+
* supplied env data. Returns the input verbatim when no env data is given,
|
|
91
|
+
* preserving backwards compatibility with cached entries that contain
|
|
92
|
+
* literal values only.
|
|
93
|
+
*/
|
|
94
|
+
function resolveStepField(value, envData) {
|
|
95
|
+
if (!envData || !value.includes('{{')) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return (0, TemplateInterpolator_1.interpolateString)(value, { env: envData, calls: [] });
|
|
99
|
+
}
|
|
87
100
|
/**
|
|
88
101
|
* Builds an executor function from structured assertion steps.
|
|
89
102
|
* Each step maps to exactly one Playwright `expect` call — no string
|
|
90
103
|
* evaluation, no VM contexts.
|
|
91
104
|
*/
|
|
92
105
|
function buildAssertExecutor(steps) {
|
|
93
|
-
return async ({ page }) => {
|
|
106
|
+
return async ({ page, envData }) => {
|
|
94
107
|
for (const step of steps) {
|
|
108
|
+
const resolvedValue = resolveStepField(step.value, envData);
|
|
109
|
+
const resolvedAttrValue = step.attributeValue === null
|
|
110
|
+
? null
|
|
111
|
+
: resolveStepField(step.attributeValue, envData);
|
|
95
112
|
const matcher = step.valueIsRegex
|
|
96
|
-
? new RegExp(
|
|
97
|
-
:
|
|
113
|
+
? new RegExp(resolvedValue)
|
|
114
|
+
: resolvedValue;
|
|
98
115
|
// Page-level assertions (no element locator needed)
|
|
99
116
|
if (step.assertion === 'toHaveTitle') {
|
|
100
117
|
await (0, test_1.expect)(page).toHaveTitle(matcher);
|
|
@@ -138,13 +155,13 @@ function buildAssertExecutor(steps) {
|
|
|
138
155
|
await (0, test_1.expect)(locator).toBeChecked();
|
|
139
156
|
break;
|
|
140
157
|
case 'toHaveValue':
|
|
141
|
-
await (0, test_1.expect)(locator).toHaveValue(
|
|
158
|
+
await (0, test_1.expect)(locator).toHaveValue(resolvedAttrValue ?? '');
|
|
142
159
|
break;
|
|
143
160
|
case 'toContainText':
|
|
144
|
-
await (0, test_1.expect)(locator).toContainText(
|
|
161
|
+
await (0, test_1.expect)(locator).toContainText(resolvedAttrValue ?? '');
|
|
145
162
|
break;
|
|
146
163
|
case 'toHaveAttribute':
|
|
147
|
-
await (0, test_1.expect)(locator).toHaveAttribute(
|
|
164
|
+
await (0, test_1.expect)(locator).toHaveAttribute(resolvedValue, resolvedAttrValue ?? '');
|
|
148
165
|
break;
|
|
149
166
|
}
|
|
150
167
|
}
|
|
@@ -155,6 +172,6 @@ function buildAssertExecutor(steps) {
|
|
|
155
172
|
* {@link Locator} from a cached {@link LocateResult}.
|
|
156
173
|
*/
|
|
157
174
|
function buildLocateExecutor(result) {
|
|
158
|
-
return ({ page }) => (0, buildLocator_1.buildLocator)(page, result);
|
|
175
|
+
return ({ page, envData }) => (0, buildLocator_1.buildLocator)(page, result, envData);
|
|
159
176
|
}
|
|
160
177
|
//# sourceMappingURL=assertCache.js.map
|
|
@@ -4,6 +4,10 @@ import type { LocateResult } from './locateTypes';
|
|
|
4
4
|
* Mechanically construct a Playwright {@link Locator} from a structured
|
|
5
5
|
* {@link LocateResult}. No `eval` or string parsing — every branch maps to a
|
|
6
6
|
* direct Playwright API call.
|
|
7
|
+
*
|
|
8
|
+
* When `envData` is supplied, `{{$.env.X}}` placeholders inside `text`,
|
|
9
|
+
* `name`, and `testId` step fields are resolved against it before being
|
|
10
|
+
* applied. `selector` and `frames[]` are left untouched.
|
|
7
11
|
*/
|
|
8
|
-
export declare function buildLocator(page: Page, result: LocateResult): Locator;
|
|
12
|
+
export declare function buildLocator(page: Page, result: LocateResult, envData?: Record<string, string>): Locator;
|
|
9
13
|
//# sourceMappingURL=buildLocator.d.ts.map
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildLocator = buildLocator;
|
|
4
|
+
const TemplateInterpolator_1 = require("../../../utils/TemplateInterpolator");
|
|
5
|
+
/**
|
|
6
|
+
* Resolves any `{{$.env.X}}` placeholders in a step field against the
|
|
7
|
+
* supplied env data. Returns the input verbatim when no env data is given,
|
|
8
|
+
* or when the field has no placeholder syntax — backwards compatible with
|
|
9
|
+
* cached entries that contain literal values only.
|
|
10
|
+
*
|
|
11
|
+
* Only applied to `text`, `name`, and `testId` step fields. `selector`
|
|
12
|
+
* (CSS/XPath) and `frames[]` entries are left literal because raw env
|
|
13
|
+
* values cannot be safely embedded into a CSS selector without escaping.
|
|
14
|
+
*/
|
|
15
|
+
function resolveStepField(value, envData) {
|
|
16
|
+
if (!envData || !value.includes('{{')) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return (0, TemplateInterpolator_1.interpolateString)(value, { env: envData, calls: [] });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interpolate env placeholders, then optionally compile the result as a
|
|
23
|
+
* regex. Mirrors the order used by `buildAssertExecutor` so env-var × regex
|
|
24
|
+
* semantics stay consistent across cache executors.
|
|
25
|
+
*
|
|
26
|
+
* On `new RegExp(...)` failure (invalid pattern) the original string is
|
|
27
|
+
* returned, letting Playwright apply literal substring matching rather than
|
|
28
|
+
* throwing inside the cache replay path.
|
|
29
|
+
*/
|
|
30
|
+
function resolveAndCompile(value, isRegex, envData) {
|
|
31
|
+
const resolved = resolveStepField(value, envData);
|
|
32
|
+
if (!isRegex) {
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return new RegExp(resolved);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
4
42
|
/**
|
|
5
43
|
* Mechanically construct a Playwright {@link Locator} from a structured
|
|
6
44
|
* {@link LocateResult}. No `eval` or string parsing — every branch maps to a
|
|
7
45
|
* direct Playwright API call.
|
|
46
|
+
*
|
|
47
|
+
* When `envData` is supplied, `{{$.env.X}}` placeholders inside `text`,
|
|
48
|
+
* `name`, and `testId` step fields are resolved against it before being
|
|
49
|
+
* applied. `selector` and `frames[]` are left untouched.
|
|
8
50
|
*/
|
|
9
|
-
function buildLocator(page, result) {
|
|
51
|
+
function buildLocator(page, result, envData) {
|
|
10
52
|
// 1. Resolve frame chain (if any)
|
|
11
53
|
let frameScope;
|
|
12
54
|
if (result.frames && result.frames.length > 0) {
|
|
@@ -16,9 +58,9 @@ function buildLocator(page, result) {
|
|
|
16
58
|
}
|
|
17
59
|
// 2. Apply locator steps
|
|
18
60
|
const base = frameScope ?? page;
|
|
19
|
-
let locator = applyStep(base, result.steps[0]);
|
|
61
|
+
let locator = applyStep(base, result.steps[0], envData);
|
|
20
62
|
for (let i = 1; i < result.steps.length; i++) {
|
|
21
|
-
locator = applyStepToLocator(locator, result.steps[i]);
|
|
63
|
+
locator = applyStepToLocator(locator, result.steps[i], envData);
|
|
22
64
|
}
|
|
23
65
|
// 3. nth disambiguation
|
|
24
66
|
if (result.nth !== undefined) {
|
|
@@ -39,34 +81,41 @@ function applyFrameStep(parent, step) {
|
|
|
39
81
|
throw new Error(`Unknown frame method: ${step.method}`);
|
|
40
82
|
}
|
|
41
83
|
}
|
|
42
|
-
function applyStep(base, step) {
|
|
43
|
-
return applyStepTo(base, step);
|
|
84
|
+
function applyStep(base, step, envData) {
|
|
85
|
+
return applyStepTo(base, step, envData);
|
|
44
86
|
}
|
|
45
|
-
function applyStepToLocator(parent, step) {
|
|
46
|
-
return applyStepTo(parent, step);
|
|
87
|
+
function applyStepToLocator(parent, step, envData) {
|
|
88
|
+
return applyStepTo(parent, step, envData);
|
|
47
89
|
}
|
|
48
|
-
function applyStepTo(parent, step) {
|
|
90
|
+
function applyStepTo(parent, step, envData) {
|
|
91
|
+
// `exact` and `*IsRegex` are mutually exclusive. If the AI emits both
|
|
92
|
+
// (shouldn't happen — the prompt forbids it), regex wins because passing
|
|
93
|
+
// `exact: true` with a `RegExp` matcher to Playwright is meaningless.
|
|
49
94
|
const exactOpt = step.exact !== undefined ? { exact: step.exact } : undefined;
|
|
50
95
|
switch (step.method) {
|
|
51
96
|
case 'getByRole': {
|
|
52
97
|
const roleOpts = {};
|
|
53
98
|
if (step.name !== undefined) {
|
|
54
|
-
roleOpts.name = step.name;
|
|
99
|
+
roleOpts.name = resolveAndCompile(step.name, step.nameIsRegex, envData);
|
|
55
100
|
}
|
|
56
|
-
if (step.exact !== undefined) {
|
|
101
|
+
if (step.exact !== undefined && !step.nameIsRegex) {
|
|
57
102
|
roleOpts.exact = step.exact;
|
|
58
103
|
}
|
|
59
104
|
return parent.getByRole((step.role ?? 'generic'), Object.keys(roleOpts).length > 0 ? roleOpts : undefined);
|
|
60
105
|
}
|
|
61
106
|
case 'getByText':
|
|
62
|
-
return parent.getByText(step.text ?? '', exactOpt);
|
|
107
|
+
return parent.getByText(resolveAndCompile(step.text ?? '', step.textIsRegex, envData), step.textIsRegex ? undefined : exactOpt);
|
|
63
108
|
case 'getByLabel':
|
|
64
|
-
return parent.getByLabel(step.text ?? '', exactOpt);
|
|
109
|
+
return parent.getByLabel(resolveAndCompile(step.text ?? '', step.textIsRegex, envData), step.textIsRegex ? undefined : exactOpt);
|
|
65
110
|
case 'getByPlaceholder':
|
|
66
|
-
return parent.getByPlaceholder(step.text ?? '', exactOpt);
|
|
111
|
+
return parent.getByPlaceholder(resolveAndCompile(step.text ?? '', step.textIsRegex, envData), step.textIsRegex ? undefined : exactOpt);
|
|
67
112
|
case 'getByTestId':
|
|
68
|
-
return parent.getByTestId(step.testId ?? '');
|
|
113
|
+
return parent.getByTestId(resolveStepField(step.testId ?? '', envData));
|
|
69
114
|
case 'locator':
|
|
115
|
+
// `selector` is a raw CSS/XPath string — interpolating env values into
|
|
116
|
+
// it can produce invalid syntax silently. The locate prompt steers the
|
|
117
|
+
// AI toward semantic locators when env values are involved; cached
|
|
118
|
+
// selectors stay literal.
|
|
70
119
|
return parent.locator(step.selector ?? '*');
|
|
71
120
|
default:
|
|
72
121
|
throw new Error(`Unknown locator method: ${step.method}`);
|
|
@@ -17,6 +17,7 @@ import type { LocateResult } from './locateTypes';
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function locateElement(page: Page, description: string, gptClient: GptClient, options?: {
|
|
19
19
|
signal?: AbortSignal;
|
|
20
|
+
envData?: Record<string, string>;
|
|
20
21
|
}): Promise<{
|
|
21
22
|
locator: Locator;
|
|
22
23
|
result: LocateResult;
|