intellitester 0.5.2 → 0.5.4
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/{chunk-2L45UPE7.cjs → chunk-7TRKXEK4.cjs} +7 -5
- package/dist/chunk-7TRKXEK4.cjs.map +1 -0
- package/dist/{chunk-VWXGXFIM.js → chunk-KRXZC5J5.js} +581 -986
- package/dist/chunk-KRXZC5J5.js.map +1 -0
- package/dist/{chunk-XF2UENRQ.cjs → chunk-QIORMASV.cjs} +581 -985
- package/dist/chunk-QIORMASV.cjs.map +1 -0
- package/dist/{chunk-PDU3AEXH.js → chunk-VP7KGC7O.js} +7 -5
- package/dist/chunk-VP7KGC7O.js.map +1 -0
- package/dist/cli/index.cjs +243 -51
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +206 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +42 -38
- package/dist/index.d.cts +100 -2
- package/dist/index.d.ts +100 -2
- package/dist/index.js +2 -2
- package/dist/{loader-2CW6OEXJ.js → loader-FDWG62M2.js} +3 -3
- package/dist/{loader-2CW6OEXJ.js.map → loader-FDWG62M2.js.map} +1 -1
- package/dist/{loader-74NFO66L.cjs → loader-STM22G7Y.cjs} +16 -16
- package/dist/{loader-74NFO66L.cjs.map → loader-STM22G7Y.cjs.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-2L45UPE7.cjs.map +0 -1
- package/dist/chunk-PDU3AEXH.js.map +0 -1
- package/dist/chunk-VWXGXFIM.js.map +0 -1
- package/dist/chunk-XF2UENRQ.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadTestDefinition } from './chunk-
|
|
1
|
+
import { loadTestDefinition } from './chunk-VP7KGC7O.js';
|
|
2
2
|
import { track } from './chunk-VZ5S7MN6.js';
|
|
3
3
|
import { loadCleanupHandlers, executeCleanup, saveFailedCleanup } from './chunk-R6R55OTZ.js';
|
|
4
4
|
import { init_esm_shims, __require } from './chunk-RXXJEYXO.js';
|
|
@@ -14,10 +14,11 @@ import { chromium, webkit, firefox } from 'playwright';
|
|
|
14
14
|
import prompts from 'prompts';
|
|
15
15
|
import { Client, Users, TablesDB, Storage, Teams } from 'node-appwrite';
|
|
16
16
|
import os from 'os';
|
|
17
|
-
import { CompletionModel, Workflow,
|
|
17
|
+
import { CompletionModel, Workflow, runAgent, ChatMessage } from 'blazen';
|
|
18
18
|
import { z } from 'zod';
|
|
19
19
|
import { spawn } from 'child_process';
|
|
20
20
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
21
|
+
import { pathToFileURL } from 'url';
|
|
21
22
|
import { createWorker } from 'tesseract.js';
|
|
22
23
|
|
|
23
24
|
// src/core/randomUsername.ts
|
|
@@ -2187,17 +2188,17 @@ var AIEvaluationResponseSchema = z.object({
|
|
|
2187
2188
|
});
|
|
2188
2189
|
var SUBMIT_EVALUATION_TOOL = {
|
|
2189
2190
|
name: "submit_evaluation",
|
|
2190
|
-
description: "Submit the structured pass/fail evaluation of the
|
|
2191
|
+
description: "Submit the structured pass/fail evaluation of the page. Call this exactly once when you are confident in your verdict \u2014 it is terminal.",
|
|
2191
2192
|
parameters: {
|
|
2192
2193
|
type: "object",
|
|
2193
2194
|
properties: {
|
|
2194
2195
|
passed: {
|
|
2195
2196
|
type: "boolean",
|
|
2196
|
-
description: "Whether the
|
|
2197
|
+
description: "Whether the page meets the expected criteria."
|
|
2197
2198
|
},
|
|
2198
2199
|
reason: {
|
|
2199
2200
|
type: "string",
|
|
2200
|
-
description: "Concise explanation of the decision."
|
|
2201
|
+
description: "Concise explanation of the decision, citing the evidence you observed."
|
|
2201
2202
|
}
|
|
2202
2203
|
},
|
|
2203
2204
|
required: ["passed", "reason"]
|
|
@@ -2237,8 +2238,382 @@ Does the screenshot contain all of the expected content or meet the specified co
|
|
|
2237
2238
|
const result = await wf.run({});
|
|
2238
2239
|
return result.data;
|
|
2239
2240
|
}
|
|
2241
|
+
var TAKE_SCREENSHOT_TOOL = {
|
|
2242
|
+
name: "take_screenshot",
|
|
2243
|
+
description: "Capture a fresh screenshot (saved as a test artifact). NOTE: pixels from this call are NOT shown back to you on the next turn \u2014 only the initial screenshot in the user message is visible. For verifying content after scrolling/waiting, use accessibility_snapshot or query_dom (those return real readable data).",
|
|
2244
|
+
parameters: {
|
|
2245
|
+
type: "object",
|
|
2246
|
+
properties: {
|
|
2247
|
+
fullPage: {
|
|
2248
|
+
type: "boolean",
|
|
2249
|
+
description: "Capture full scrollable page (true) or just the current viewport (false). Default: false."
|
|
2250
|
+
},
|
|
2251
|
+
selector: {
|
|
2252
|
+
type: "string",
|
|
2253
|
+
description: "Optional CSS selector. If provided, screenshots just that element."
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
var SCROLL_TOOL = {
|
|
2259
|
+
name: "scroll",
|
|
2260
|
+
description: "Scroll the page or a specific element. Use to bring off-screen content into the viewport before re-screenshotting.",
|
|
2261
|
+
parameters: {
|
|
2262
|
+
type: "object",
|
|
2263
|
+
properties: {
|
|
2264
|
+
direction: {
|
|
2265
|
+
type: "string",
|
|
2266
|
+
enum: ["up", "down"],
|
|
2267
|
+
description: "Direction to scroll the page."
|
|
2268
|
+
},
|
|
2269
|
+
pixels: {
|
|
2270
|
+
type: "number",
|
|
2271
|
+
description: "Pixels to scroll (default 800). Used with `direction`."
|
|
2272
|
+
},
|
|
2273
|
+
toBottom: {
|
|
2274
|
+
type: "boolean",
|
|
2275
|
+
description: "Scroll all the way to the bottom of the page."
|
|
2276
|
+
},
|
|
2277
|
+
toTop: {
|
|
2278
|
+
type: "boolean",
|
|
2279
|
+
description: "Scroll all the way to the top of the page."
|
|
2280
|
+
},
|
|
2281
|
+
selector: {
|
|
2282
|
+
type: "string",
|
|
2283
|
+
description: "CSS selector. If provided, scrolls that element into view instead of moving the page."
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
var WAIT_TOOL = {
|
|
2289
|
+
name: "wait",
|
|
2290
|
+
description: "Pause briefly to let animations or lazy-loaded UI settle before re-checking. Capped at 3000ms.",
|
|
2291
|
+
parameters: {
|
|
2292
|
+
type: "object",
|
|
2293
|
+
properties: {
|
|
2294
|
+
ms: {
|
|
2295
|
+
type: "number",
|
|
2296
|
+
description: "Milliseconds to wait (1-3000)."
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
required: ["ms"]
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
var QUERY_DOM_TOOL = {
|
|
2303
|
+
name: "query_dom",
|
|
2304
|
+
description: "Look up an element by CSS selector and report whether it exists, is visible, its text content, and its attributes. Useful when pixels are ambiguous (e.g., for <video>, <canvas>, or off-screen content).",
|
|
2305
|
+
parameters: {
|
|
2306
|
+
type: "object",
|
|
2307
|
+
properties: {
|
|
2308
|
+
selector: {
|
|
2309
|
+
type: "string",
|
|
2310
|
+
description: "CSS selector to query."
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
required: ["selector"]
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
var ACCESSIBILITY_SNAPSHOT_TOOL = {
|
|
2317
|
+
name: "accessibility_snapshot",
|
|
2318
|
+
description: "Get the ARIA snapshot (YAML accessibility tree) of the page or a sub-tree. The truth-source when pixels are ambiguous \u2014 lists roles, names, and structure as screen readers see them.",
|
|
2319
|
+
parameters: {
|
|
2320
|
+
type: "object",
|
|
2321
|
+
properties: {
|
|
2322
|
+
selector: {
|
|
2323
|
+
type: "string",
|
|
2324
|
+
description: "Optional CSS selector to scope the snapshot to a sub-tree. Default: full body."
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
var AGENT_BUILTIN_TOOLS = [
|
|
2330
|
+
TAKE_SCREENSHOT_TOOL,
|
|
2331
|
+
SCROLL_TOOL,
|
|
2332
|
+
WAIT_TOOL,
|
|
2333
|
+
QUERY_DOM_TOOL,
|
|
2334
|
+
ACCESSIBILITY_SNAPSHOT_TOOL
|
|
2335
|
+
];
|
|
2336
|
+
var AGENT_BUILTIN_NAMES = new Set(AGENT_BUILTIN_TOOLS.map((t) => t.name));
|
|
2337
|
+
var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set([...AGENT_BUILTIN_NAMES, "submit_evaluation"]);
|
|
2338
|
+
function truncate(s, limit) {
|
|
2339
|
+
return s.length <= limit ? s : `${s.slice(0, limit)}
|
|
2340
|
+
\u2026[truncated ${s.length - limit} chars]`;
|
|
2341
|
+
}
|
|
2342
|
+
async function execTakeScreenshot(args, page) {
|
|
2343
|
+
const fullPage = args.fullPage === true;
|
|
2344
|
+
const selector = typeof args.selector === "string" ? args.selector : void 0;
|
|
2345
|
+
try {
|
|
2346
|
+
if (selector) {
|
|
2347
|
+
await page.locator(selector).first().screenshot();
|
|
2348
|
+
} else {
|
|
2349
|
+
await page.screenshot({ fullPage });
|
|
2350
|
+
}
|
|
2351
|
+
const url = page.url();
|
|
2352
|
+
const scope = selector ? `selector="${selector}"` : fullPage ? "fullPage" : "viewport";
|
|
2353
|
+
return {
|
|
2354
|
+
kind: "text",
|
|
2355
|
+
text: `Screenshot captured (${scope}) at ${url}. Note: re-screenshots are not surfaced visually in this loop \u2014 use accessibility_snapshot or query_dom to verify the contents you'd want to inspect.`
|
|
2356
|
+
};
|
|
2357
|
+
} catch (e) {
|
|
2358
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2359
|
+
return { kind: "text", text: `take_screenshot error: ${msg}` };
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
async function execScroll(args, page) {
|
|
2363
|
+
const selector = typeof args.selector === "string" ? args.selector : void 0;
|
|
2364
|
+
const toBottom = args.toBottom === true;
|
|
2365
|
+
const toTop = args.toTop === true;
|
|
2366
|
+
const direction = args.direction === "up" ? "up" : args.direction === "down" ? "down" : null;
|
|
2367
|
+
const pixels = typeof args.pixels === "number" ? args.pixels : 800;
|
|
2368
|
+
if (selector) {
|
|
2369
|
+
await page.locator(selector).first().scrollIntoViewIfNeeded();
|
|
2370
|
+
return { kind: "text", text: `Scrolled "${selector}" into view.` };
|
|
2371
|
+
}
|
|
2372
|
+
if (toBottom) {
|
|
2373
|
+
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
|
|
2374
|
+
return { kind: "text", text: "Scrolled to bottom of page." };
|
|
2375
|
+
}
|
|
2376
|
+
if (toTop) {
|
|
2377
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2378
|
+
return { kind: "text", text: "Scrolled to top of page." };
|
|
2379
|
+
}
|
|
2380
|
+
const dy = direction === "up" ? -pixels : pixels;
|
|
2381
|
+
await page.evaluate((y) => window.scrollBy(0, y), dy);
|
|
2382
|
+
const pos = await page.evaluate(() => ({ y: window.scrollY, max: document.documentElement.scrollHeight - window.innerHeight }));
|
|
2383
|
+
return { kind: "text", text: `Scrolled ${dy}px. Now at y=${Math.round(pos.y)} / ${Math.round(pos.max)} (max).` };
|
|
2384
|
+
}
|
|
2385
|
+
async function execWait(args, page) {
|
|
2386
|
+
const requested = typeof args.ms === "number" && args.ms > 0 ? args.ms : 500;
|
|
2387
|
+
const ms = Math.min(Math.floor(requested), 3e3);
|
|
2388
|
+
await page.waitForTimeout(ms);
|
|
2389
|
+
return { kind: "text", text: `Waited ${ms}ms${requested !== ms ? ` (capped from requested ${requested}ms)` : ""}.` };
|
|
2390
|
+
}
|
|
2391
|
+
async function execQueryDom(args, page) {
|
|
2392
|
+
const selector = typeof args.selector === "string" ? args.selector : "";
|
|
2393
|
+
if (!selector) return { kind: "text", text: "query_dom error: selector is required." };
|
|
2394
|
+
try {
|
|
2395
|
+
const locator = page.locator(selector);
|
|
2396
|
+
const count = await locator.count();
|
|
2397
|
+
if (count === 0) {
|
|
2398
|
+
return { kind: "text", text: `query_dom: no elements match "${selector}".` };
|
|
2399
|
+
}
|
|
2400
|
+
const first = locator.first();
|
|
2401
|
+
const visible = await first.isVisible().catch(() => false);
|
|
2402
|
+
const text = await first.innerText({ timeout: 1e3 }).catch(() => "");
|
|
2403
|
+
const attributes = await first.evaluate((el) => {
|
|
2404
|
+
const out = {};
|
|
2405
|
+
const target = el;
|
|
2406
|
+
for (const name of target.getAttributeNames()) {
|
|
2407
|
+
out[name] = target.getAttribute(name) ?? "";
|
|
2408
|
+
}
|
|
2409
|
+
return out;
|
|
2410
|
+
}).catch(() => ({}));
|
|
2411
|
+
const payload = {
|
|
2412
|
+
selector,
|
|
2413
|
+
count,
|
|
2414
|
+
visible,
|
|
2415
|
+
text: truncate(text, 2e3),
|
|
2416
|
+
attributes
|
|
2417
|
+
};
|
|
2418
|
+
return { kind: "text", text: JSON.stringify(payload, null, 2) };
|
|
2419
|
+
} catch (e) {
|
|
2420
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2421
|
+
return { kind: "text", text: `query_dom error: ${msg}` };
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
async function execAccessibilitySnapshot(args, page) {
|
|
2425
|
+
const selector = typeof args.selector === "string" && args.selector.trim() ? args.selector : "body";
|
|
2426
|
+
try {
|
|
2427
|
+
const locator = page.locator(selector).first();
|
|
2428
|
+
const count = await locator.count();
|
|
2429
|
+
if (count === 0) {
|
|
2430
|
+
return { kind: "text", text: `accessibility_snapshot: selector "${selector}" did not match any element.` };
|
|
2431
|
+
}
|
|
2432
|
+
const snap = await locator.ariaSnapshot({ timeout: 5e3 });
|
|
2433
|
+
return { kind: "text", text: truncate(snap, 8e3) };
|
|
2434
|
+
} catch (e) {
|
|
2435
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2436
|
+
return { kind: "text", text: `accessibility_snapshot error: ${msg}` };
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
async function dispatchBuiltinTool(name, args, page) {
|
|
2440
|
+
switch (name) {
|
|
2441
|
+
case "take_screenshot":
|
|
2442
|
+
return execTakeScreenshot(args, page);
|
|
2443
|
+
case "scroll":
|
|
2444
|
+
return execScroll(args, page);
|
|
2445
|
+
case "wait":
|
|
2446
|
+
return execWait(args, page);
|
|
2447
|
+
case "query_dom":
|
|
2448
|
+
return execQueryDom(args, page);
|
|
2449
|
+
case "accessibility_snapshot":
|
|
2450
|
+
return execAccessibilitySnapshot(args, page);
|
|
2451
|
+
default:
|
|
2452
|
+
throw new Error(`Unknown built-in tool: ${name}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
async function loadCustomTools(toolsPath, workflowDir) {
|
|
2456
|
+
const resolved = path5__default.isAbsolute(toolsPath) ? toolsPath : path5__default.resolve(workflowDir, toolsPath);
|
|
2457
|
+
const href = pathToFileURL(resolved).href;
|
|
2458
|
+
const mod = await import(href);
|
|
2459
|
+
const candidate = (mod && typeof mod === "object" && "default" in mod ? mod.default : mod) ?? mod;
|
|
2460
|
+
if (!candidate || typeof candidate !== "object" || !Array.isArray(candidate.tools) || typeof candidate.handler !== "function") {
|
|
2461
|
+
throw new Error(
|
|
2462
|
+
`Custom tools module at ${resolved} must export { tools: JsToolDef[], handler: (name, args, ctx) => AgentToolResult }`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
const out = candidate;
|
|
2466
|
+
for (const tool of out.tools) {
|
|
2467
|
+
if (!tool.name || typeof tool.name !== "string") {
|
|
2468
|
+
throw new Error(`Custom tools module at ${resolved}: every tool must have a string \`name\`.`);
|
|
2469
|
+
}
|
|
2470
|
+
if (RESERVED_TOOL_NAMES.has(tool.name)) {
|
|
2471
|
+
throw new Error(
|
|
2472
|
+
`Custom tools module at ${resolved}: tool name "${tool.name}" collides with a reserved built-in tool. Rename it.`
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return out;
|
|
2477
|
+
}
|
|
2478
|
+
var SubmitEvaluationSignal = class extends Error {
|
|
2479
|
+
constructor(passed, reason) {
|
|
2480
|
+
super("SubmitEvaluationSignal");
|
|
2481
|
+
this.passed = passed;
|
|
2482
|
+
this.reason = reason;
|
|
2483
|
+
this.name = "SubmitEvaluationSignal";
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
async function runAgentEvaluation(initialScreenshot, expectedArray, customPrompt, aiConfig, page, opts) {
|
|
2487
|
+
const model = buildModel(aiConfig);
|
|
2488
|
+
const completionOptions = buildCompletionOptions(aiConfig);
|
|
2489
|
+
let customTools = [];
|
|
2490
|
+
let customHandler;
|
|
2491
|
+
if (opts.customToolsPath) {
|
|
2492
|
+
const mod = await loadCustomTools(opts.customToolsPath, opts.workflowDir);
|
|
2493
|
+
customTools = mod.tools;
|
|
2494
|
+
customHandler = mod.handler;
|
|
2495
|
+
}
|
|
2496
|
+
const allTools = [
|
|
2497
|
+
...AGENT_BUILTIN_TOOLS,
|
|
2498
|
+
...customTools,
|
|
2499
|
+
SUBMIT_EVALUATION_TOOL
|
|
2500
|
+
];
|
|
2501
|
+
const systemPrompt = `You are auditing a web page to decide whether the user's expected condition holds.
|
|
2502
|
+
|
|
2503
|
+
The initial screenshot of the page is attached to the user message \u2014 that is the ONLY image you can see. Re-screenshots are not shown back to you (they are saved as artifacts only). After scrolling/waiting, verify content by reading the DOM, NOT by re-screenshotting.
|
|
2504
|
+
|
|
2505
|
+
Tools:
|
|
2506
|
+
- accessibility_snapshot: returns the ARIA tree (YAML) of a region \u2014 the truth source for what is rendered. Use this to confirm structure (lists, regions, roles, accessible names).
|
|
2507
|
+
- query_dom: returns count/visibility/text/attributes for a CSS selector.
|
|
2508
|
+
- scroll: move the page (by pixels, to-bottom/top, or scroll a selector into view).
|
|
2509
|
+
- wait: pause up to 3000ms for animations/lazy loads.
|
|
2510
|
+
- take_screenshot: captures an artifact (you do not see the result image \u2014 informational only).
|
|
2511
|
+
- submit_evaluation: terminal \u2014 call exactly once with { passed, reason } when confident.
|
|
2512
|
+
|
|
2513
|
+
Workflow: examine the initial screenshot. If the expected content isn't in view, scroll to where it should live, then use accessibility_snapshot or query_dom to confirm. Be efficient. Call submit_evaluation once you have enough evidence.`;
|
|
2514
|
+
const defaultUserPrompt = `Expected content or conditions:
|
|
2515
|
+
${expectedArray.map((exp) => `- ${exp}`).join("\n")}
|
|
2516
|
+
|
|
2517
|
+
Investigate the page using the available tools. When confident, call submit_evaluation with your verdict.`;
|
|
2518
|
+
const userPromptText = customPrompt || defaultUserPrompt;
|
|
2519
|
+
const imageBase64 = initialScreenshot.toString("base64");
|
|
2520
|
+
const messages = [
|
|
2521
|
+
ChatMessage.system(systemPrompt),
|
|
2522
|
+
ChatMessage.userImageBase64(userPromptText, imageBase64, "image/png")
|
|
2523
|
+
];
|
|
2524
|
+
let submission;
|
|
2525
|
+
const toolHandler = async (name, rawArgs) => {
|
|
2526
|
+
const args = rawArgs && typeof rawArgs === "object" ? rawArgs : {};
|
|
2527
|
+
if (name === "submit_evaluation") {
|
|
2528
|
+
const parsed = AIEvaluationResponseSchema.parse(args);
|
|
2529
|
+
submission = parsed;
|
|
2530
|
+
throw new SubmitEvaluationSignal(parsed.passed, parsed.reason);
|
|
2531
|
+
}
|
|
2532
|
+
let result;
|
|
2533
|
+
if (AGENT_BUILTIN_NAMES.has(name)) {
|
|
2534
|
+
result = await dispatchBuiltinTool(name, args, page);
|
|
2535
|
+
} else if (customHandler) {
|
|
2536
|
+
result = await customHandler(name, args, { page });
|
|
2537
|
+
} else {
|
|
2538
|
+
result = { kind: "text", text: `Unknown tool: ${name}` };
|
|
2539
|
+
}
|
|
2540
|
+
return result.text;
|
|
2541
|
+
};
|
|
2542
|
+
let iterations = 0;
|
|
2543
|
+
try {
|
|
2544
|
+
const agentResult = await runAgent(model, messages, allTools, toolHandler, {
|
|
2545
|
+
maxIterations: opts.maxSteps,
|
|
2546
|
+
temperature: completionOptions.temperature ?? void 0,
|
|
2547
|
+
maxTokens: completionOptions.maxTokens ?? void 0,
|
|
2548
|
+
noFinishTool: true
|
|
2549
|
+
});
|
|
2550
|
+
iterations = agentResult.iterations;
|
|
2551
|
+
} catch (e) {
|
|
2552
|
+
if (!submission) {
|
|
2553
|
+
throw e;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (!submission) {
|
|
2557
|
+
throw new Error(
|
|
2558
|
+
`Agent exhausted ${opts.maxSteps} iterations without calling submit_evaluation.`
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
passed: submission.passed,
|
|
2563
|
+
reason: submission.reason,
|
|
2564
|
+
iterations: iterations || opts.maxSteps
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2240
2567
|
async function evaluate(options) {
|
|
2241
2568
|
const expectedArray = Array.isArray(options.expected) ? options.expected : [options.expected];
|
|
2569
|
+
if (options.mode === "agent") {
|
|
2570
|
+
if (!options.aiConfig) {
|
|
2571
|
+
return {
|
|
2572
|
+
passed: false,
|
|
2573
|
+
mode: "agent",
|
|
2574
|
+
reason: "Agent evaluation requested but no AI configuration provided",
|
|
2575
|
+
screenshotPath: options.screenshotPath
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
if (!options.page) {
|
|
2579
|
+
return {
|
|
2580
|
+
passed: false,
|
|
2581
|
+
mode: "agent",
|
|
2582
|
+
reason: "Agent evaluation requires a live Page (internal wiring error)",
|
|
2583
|
+
screenshotPath: options.screenshotPath
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
try {
|
|
2587
|
+
const agentResult = await runAgentEvaluation(
|
|
2588
|
+
options.screenshotBuffer,
|
|
2589
|
+
expectedArray,
|
|
2590
|
+
options.prompt,
|
|
2591
|
+
options.aiConfig,
|
|
2592
|
+
options.page,
|
|
2593
|
+
{
|
|
2594
|
+
maxSteps: options.maxSteps ?? 6,
|
|
2595
|
+
customToolsPath: options.customToolsPath,
|
|
2596
|
+
workflowDir: options.workflowDir ?? process.cwd()
|
|
2597
|
+
}
|
|
2598
|
+
);
|
|
2599
|
+
return {
|
|
2600
|
+
passed: agentResult.passed,
|
|
2601
|
+
mode: "agent",
|
|
2602
|
+
reason: agentResult.reason,
|
|
2603
|
+
aiReason: agentResult.reason,
|
|
2604
|
+
agentIterations: agentResult.iterations,
|
|
2605
|
+
screenshotPath: options.screenshotPath
|
|
2606
|
+
};
|
|
2607
|
+
} catch (e) {
|
|
2608
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2609
|
+
return {
|
|
2610
|
+
passed: false,
|
|
2611
|
+
mode: "agent",
|
|
2612
|
+
reason: `Agent evaluation failed: ${msg}`,
|
|
2613
|
+
screenshotPath: options.screenshotPath
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2242
2617
|
let ocrFailReason;
|
|
2243
2618
|
if (options.mode === "ocr" || options.mode === "auto") {
|
|
2244
2619
|
try {
|
|
@@ -2600,7 +2975,7 @@ async function handleInteractiveError(page, action, error, screenshotDir, stepIn
|
|
|
2600
2975
|
return response.action || "abort";
|
|
2601
2976
|
}
|
|
2602
2977
|
async function executeActionWithRetry(page, action, index, options) {
|
|
2603
|
-
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName, healing, testFilePath } = options;
|
|
2978
|
+
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName, healing, testFilePath, responseLog, stepStartTs } = options;
|
|
2604
2979
|
const extras = {};
|
|
2605
2980
|
const buildTrackPayload = (stepExtras) => {
|
|
2606
2981
|
if (!("track" in action)) return null;
|
|
@@ -3053,15 +3428,7 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3053
3428
|
if (debugMode) {
|
|
3054
3429
|
console.log(`[DEBUG] Executing nested step ${nestedIdx + 1}: ${nestedAction.type}`);
|
|
3055
3430
|
}
|
|
3056
|
-
await executeActionWithRetry(page, nestedAction, index,
|
|
3057
|
-
baseUrl,
|
|
3058
|
-
context,
|
|
3059
|
-
screenshotDir,
|
|
3060
|
-
debugMode,
|
|
3061
|
-
interactive,
|
|
3062
|
-
aiConfig,
|
|
3063
|
-
browserName
|
|
3064
|
-
});
|
|
3431
|
+
await executeActionWithRetry(page, nestedAction, index, options);
|
|
3065
3432
|
}
|
|
3066
3433
|
break;
|
|
3067
3434
|
}
|
|
@@ -3163,18 +3530,10 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3163
3530
|
if (debugMode) {
|
|
3164
3531
|
console.log(`[DEBUG] Executing branch step ${nestedIdx + 1}: ${nestedAction.type}`);
|
|
3165
3532
|
}
|
|
3166
|
-
await executeActionWithRetry(page, nestedAction, index,
|
|
3167
|
-
baseUrl,
|
|
3168
|
-
context,
|
|
3169
|
-
screenshotDir,
|
|
3170
|
-
debugMode,
|
|
3171
|
-
interactive,
|
|
3172
|
-
aiConfig,
|
|
3173
|
-
browserName
|
|
3174
|
-
});
|
|
3533
|
+
await executeActionWithRetry(page, nestedAction, index, options);
|
|
3175
3534
|
}
|
|
3176
3535
|
} else {
|
|
3177
|
-
const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-
|
|
3536
|
+
const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-FDWG62M2.js');
|
|
3178
3537
|
const workflowPath = path5__default.resolve(process.cwd(), branchToExecute.workflow);
|
|
3179
3538
|
const workflowDir = path5__default.dirname(workflowPath);
|
|
3180
3539
|
if (debugMode) {
|
|
@@ -3200,17 +3559,147 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3200
3559
|
}
|
|
3201
3560
|
}
|
|
3202
3561
|
for (const [testStepIdx, testAction] of test.steps.entries()) {
|
|
3203
|
-
await executeActionWithRetry(page, testAction, testStepIdx,
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3562
|
+
await executeActionWithRetry(page, testAction, testStepIdx, options);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
break;
|
|
3567
|
+
}
|
|
3568
|
+
case "saveStorageState": {
|
|
3569
|
+
const saveAction = action;
|
|
3570
|
+
if (saveAction.path) {
|
|
3571
|
+
const resolvedPath = interpolateVariables(saveAction.path, context.variables);
|
|
3572
|
+
const baseDir = testFilePath ? path5__default.dirname(testFilePath) : process.cwd();
|
|
3573
|
+
const absPath = path5__default.isAbsolute(resolvedPath) ? resolvedPath : path5__default.resolve(baseDir, resolvedPath);
|
|
3574
|
+
await page.context().storageState({ path: absPath });
|
|
3575
|
+
if (debugMode) {
|
|
3576
|
+
console.log(`[DEBUG] Saved storage state to ${absPath}`);
|
|
3577
|
+
}
|
|
3578
|
+
} else if (saveAction.handler) {
|
|
3579
|
+
const resolvedHandler = interpolateVariables(saveAction.handler, context.variables);
|
|
3580
|
+
const baseDir = testFilePath ? path5__default.dirname(testFilePath) : process.cwd();
|
|
3581
|
+
const absPath = path5__default.isAbsolute(resolvedHandler) ? resolvedHandler : path5__default.resolve(baseDir, resolvedHandler);
|
|
3582
|
+
let loadPath = absPath;
|
|
3583
|
+
if (absPath.endsWith(".ts")) {
|
|
3584
|
+
const jsPath = absPath.replace(/\.ts$/, ".js");
|
|
3585
|
+
try {
|
|
3586
|
+
await fs3__default.access(jsPath);
|
|
3587
|
+
loadPath = jsPath;
|
|
3588
|
+
} catch {
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
const mod = await import(`${loadPath}?t=${Date.now()}`);
|
|
3592
|
+
const fn = mod.default ?? mod;
|
|
3593
|
+
if (typeof fn !== "function") {
|
|
3594
|
+
throw new Error(`saveStorageState handler at ${resolvedHandler} did not export a default function`);
|
|
3595
|
+
}
|
|
3596
|
+
await fn({
|
|
3597
|
+
page,
|
|
3598
|
+
context: page.context(),
|
|
3599
|
+
variables: context.variables
|
|
3600
|
+
});
|
|
3601
|
+
if (debugMode) {
|
|
3602
|
+
console.log(`[DEBUG] Ran custom saveStorageState handler: ${resolvedHandler}`);
|
|
3603
|
+
}
|
|
3604
|
+
} else {
|
|
3605
|
+
throw new Error("saveStorageState requires either `path` or `handler` (schema should have caught this)");
|
|
3606
|
+
}
|
|
3607
|
+
break;
|
|
3608
|
+
}
|
|
3609
|
+
case "assertCookies": {
|
|
3610
|
+
const cookieAction = action;
|
|
3611
|
+
const filterUrl = cookieAction.url ? interpolateVariables(cookieAction.url, context.variables) : void 0;
|
|
3612
|
+
const jar = await page.context().cookies(filterUrl);
|
|
3613
|
+
const names = new Set(jar.map((c) => c.name));
|
|
3614
|
+
const problems = [];
|
|
3615
|
+
if (cookieAction.has) {
|
|
3616
|
+
for (const name of cookieAction.has) {
|
|
3617
|
+
if (!names.has(name)) problems.push(`expected cookie "${name}" to be present`);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
if (cookieAction.not) {
|
|
3621
|
+
for (const name of cookieAction.not) {
|
|
3622
|
+
if (names.has(name)) problems.push(`expected cookie "${name}" to be absent`);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
if (cookieAction.match) {
|
|
3626
|
+
for (const [name, pattern] of Object.entries(cookieAction.match)) {
|
|
3627
|
+
const c = jar.find((entry) => entry.name === name);
|
|
3628
|
+
if (!c) {
|
|
3629
|
+
problems.push(`expected cookie "${name}" to be present (for value match)`);
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
const matcher = compileMatcher(interpolateVariables(pattern, context.variables), "substr");
|
|
3633
|
+
if (!matcher(c.value)) {
|
|
3634
|
+
problems.push(`cookie "${name}" value "${c.value}" did not match pattern "${pattern}"`);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
if (problems.length > 0) {
|
|
3639
|
+
throw new Error(`assertCookies failed:
|
|
3640
|
+
- ${problems.join("\n - ")}
|
|
3641
|
+
(cookies seen: ${[...names].join(", ") || "<none>"})`);
|
|
3642
|
+
}
|
|
3643
|
+
break;
|
|
3644
|
+
}
|
|
3645
|
+
case "expectResponse": {
|
|
3646
|
+
if (!responseLog) {
|
|
3647
|
+
throw new Error("expectResponse requires a responseLog to be attached (executor wiring issue)");
|
|
3648
|
+
}
|
|
3649
|
+
const respAction = action;
|
|
3650
|
+
const urlPattern = interpolateVariables(respAction.url, context.variables);
|
|
3651
|
+
const urlMatch = compileMatcher(urlPattern, "url");
|
|
3652
|
+
const headerMatchers = respAction.headers ? Object.entries(respAction.headers).map(([name, pattern]) => ({
|
|
3653
|
+
name: name.toLowerCase(),
|
|
3654
|
+
test: compileMatcher(interpolateVariables(pattern, context.variables), "substr"),
|
|
3655
|
+
pattern
|
|
3656
|
+
})) : [];
|
|
3657
|
+
const expectedStatus = respAction.status;
|
|
3658
|
+
let sinceTs;
|
|
3659
|
+
if (respAction.since === "testStart") {
|
|
3660
|
+
sinceTs = 0;
|
|
3661
|
+
} else if (typeof respAction.since === "number") {
|
|
3662
|
+
sinceTs = respAction.since;
|
|
3663
|
+
} else {
|
|
3664
|
+
sinceTs = stepStartTs ?? 0;
|
|
3665
|
+
}
|
|
3666
|
+
const timeout = respAction.timeout ?? 5e3;
|
|
3667
|
+
const deadline = Date.now() + timeout;
|
|
3668
|
+
const findMatch = () => {
|
|
3669
|
+
for (const entry of responseLog.snapshot()) {
|
|
3670
|
+
if (entry.ts < sinceTs) continue;
|
|
3671
|
+
if (!urlMatch(entry.url)) continue;
|
|
3672
|
+
if (expectedStatus !== void 0 && entry.status !== expectedStatus) continue;
|
|
3673
|
+
let headersOk = true;
|
|
3674
|
+
for (const h of headerMatchers) {
|
|
3675
|
+
const value = entry.headers[h.name];
|
|
3676
|
+
if (value === void 0 || !h.test(value)) {
|
|
3677
|
+
headersOk = false;
|
|
3678
|
+
break;
|
|
3679
|
+
}
|
|
3212
3680
|
}
|
|
3681
|
+
if (headersOk) return entry;
|
|
3213
3682
|
}
|
|
3683
|
+
return null;
|
|
3684
|
+
};
|
|
3685
|
+
let match = findMatch();
|
|
3686
|
+
while (!match && Date.now() < deadline) {
|
|
3687
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3688
|
+
match = findMatch();
|
|
3689
|
+
}
|
|
3690
|
+
if (!match) {
|
|
3691
|
+
const recent = responseLog.snapshot().slice(-5).map((e) => `${e.status} ${e.url}`).join("\n ");
|
|
3692
|
+
throw new Error(
|
|
3693
|
+
`expectResponse timed out after ${timeout}ms.
|
|
3694
|
+
url pattern: ${urlPattern}
|
|
3695
|
+
` + (expectedStatus !== void 0 ? ` expected status: ${expectedStatus}
|
|
3696
|
+
` : "") + (headerMatchers.length > 0 ? ` expected headers: ${headerMatchers.map((h) => `${h.name}=${h.pattern}`).join(", ")}
|
|
3697
|
+
` : "") + ` recent responses:
|
|
3698
|
+
${recent || "<none>"}`
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
if (debugMode) {
|
|
3702
|
+
console.log(`[DEBUG] expectResponse matched: ${match.status} ${match.url}`);
|
|
3214
3703
|
}
|
|
3215
3704
|
break;
|
|
3216
3705
|
}
|
|
@@ -3670,6 +4159,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3670
4159
|
});
|
|
3671
4160
|
const expectedRaw = evalAction.expected;
|
|
3672
4161
|
const expectedArray = Array.isArray(expectedRaw) ? expectedRaw.map((e) => interpolateVariables(e, executionContext.variables)) : [interpolateVariables(expectedRaw, executionContext.variables)];
|
|
4162
|
+
const workflowDir = options.testFilePath ? path5__default.dirname(options.testFilePath) : process.cwd();
|
|
3673
4163
|
const evalResult = await evaluate({
|
|
3674
4164
|
expected: expectedArray,
|
|
3675
4165
|
mode: evalAction.mode ?? "auto",
|
|
@@ -3678,7 +4168,11 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3678
4168
|
confidence: evalAction.confidence ?? 60,
|
|
3679
4169
|
screenshotBuffer,
|
|
3680
4170
|
screenshotPath: evalScreenshotPath,
|
|
3681
|
-
aiConfig: options.aiConfig
|
|
4171
|
+
aiConfig: options.aiConfig,
|
|
4172
|
+
page,
|
|
4173
|
+
maxSteps: evalAction.maxSteps,
|
|
4174
|
+
customToolsPath: evalAction.tools,
|
|
4175
|
+
workflowDir
|
|
3682
4176
|
});
|
|
3683
4177
|
if (debugMode) {
|
|
3684
4178
|
console.log(`[DEBUG] Evaluate result: ${evalResult.passed ? "PASSED" : "FAILED"} (${evalResult.mode})`);
|
|
@@ -3699,172 +4193,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3699
4193
|
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3700
4194
|
continue;
|
|
3701
4195
|
}
|
|
3702
|
-
|
|
3703
|
-
const saveAction = action;
|
|
3704
|
-
try {
|
|
3705
|
-
if (saveAction.path) {
|
|
3706
|
-
const resolvedPath = interpolateVariables(saveAction.path, executionContext.variables);
|
|
3707
|
-
const baseDir = options.testFilePath ? path5__default.dirname(options.testFilePath) : process.cwd();
|
|
3708
|
-
const absPath = path5__default.isAbsolute(resolvedPath) ? resolvedPath : path5__default.resolve(baseDir, resolvedPath);
|
|
3709
|
-
await page.context().storageState({ path: absPath });
|
|
3710
|
-
if (debugMode) {
|
|
3711
|
-
console.log(`[DEBUG] Saved storage state to ${absPath}`);
|
|
3712
|
-
}
|
|
3713
|
-
} else if (saveAction.handler) {
|
|
3714
|
-
const resolvedHandler = interpolateVariables(saveAction.handler, executionContext.variables);
|
|
3715
|
-
const baseDir = options.testFilePath ? path5__default.dirname(options.testFilePath) : process.cwd();
|
|
3716
|
-
const absPath = path5__default.isAbsolute(resolvedHandler) ? resolvedHandler : path5__default.resolve(baseDir, resolvedHandler);
|
|
3717
|
-
let loadPath = absPath;
|
|
3718
|
-
if (absPath.endsWith(".ts")) {
|
|
3719
|
-
const jsPath = absPath.replace(/\.ts$/, ".js");
|
|
3720
|
-
try {
|
|
3721
|
-
await fs3__default.access(jsPath);
|
|
3722
|
-
loadPath = jsPath;
|
|
3723
|
-
} catch {
|
|
3724
|
-
}
|
|
3725
|
-
}
|
|
3726
|
-
const mod = await import(`${loadPath}?t=${Date.now()}`);
|
|
3727
|
-
const fn = mod.default ?? mod;
|
|
3728
|
-
if (typeof fn !== "function") {
|
|
3729
|
-
throw new Error(`saveStorageState handler at ${resolvedHandler} did not export a default function`);
|
|
3730
|
-
}
|
|
3731
|
-
await fn({
|
|
3732
|
-
page,
|
|
3733
|
-
context: page.context(),
|
|
3734
|
-
variables: executionContext.variables
|
|
3735
|
-
});
|
|
3736
|
-
if (debugMode) {
|
|
3737
|
-
console.log(`[DEBUG] Ran custom saveStorageState handler: ${resolvedHandler}`);
|
|
3738
|
-
}
|
|
3739
|
-
} else {
|
|
3740
|
-
throw new Error("saveStorageState requires either `path` or `handler` (schema should have caught this)");
|
|
3741
|
-
}
|
|
3742
|
-
sizeResults.push({ action, status: "passed" });
|
|
3743
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3744
|
-
const trackedPayload = buildTrackPayload(action, index);
|
|
3745
|
-
if (trackedPayload) {
|
|
3746
|
-
await track(trackedPayload);
|
|
3747
|
-
}
|
|
3748
|
-
} catch (e) {
|
|
3749
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
3750
|
-
sizeResults.push({ action, status: "failed", error: errMsg });
|
|
3751
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3752
|
-
throw e;
|
|
3753
|
-
}
|
|
3754
|
-
continue;
|
|
3755
|
-
}
|
|
3756
|
-
if (action.type === "assertCookies") {
|
|
3757
|
-
const cookieAction = action;
|
|
3758
|
-
try {
|
|
3759
|
-
const filterUrl = cookieAction.url ? interpolateVariables(cookieAction.url, executionContext.variables) : void 0;
|
|
3760
|
-
const jar = await page.context().cookies(filterUrl);
|
|
3761
|
-
const names = new Set(jar.map((c) => c.name));
|
|
3762
|
-
const problems = [];
|
|
3763
|
-
if (cookieAction.has) {
|
|
3764
|
-
for (const name of cookieAction.has) {
|
|
3765
|
-
if (!names.has(name)) problems.push(`expected cookie "${name}" to be present`);
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
if (cookieAction.not) {
|
|
3769
|
-
for (const name of cookieAction.not) {
|
|
3770
|
-
if (names.has(name)) problems.push(`expected cookie "${name}" to be absent`);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
if (cookieAction.match) {
|
|
3774
|
-
for (const [name, pattern] of Object.entries(cookieAction.match)) {
|
|
3775
|
-
const c = jar.find((entry) => entry.name === name);
|
|
3776
|
-
if (!c) {
|
|
3777
|
-
problems.push(`expected cookie "${name}" to be present (for value match)`);
|
|
3778
|
-
continue;
|
|
3779
|
-
}
|
|
3780
|
-
const matcher = compileMatcher(interpolateVariables(pattern, executionContext.variables), "substr");
|
|
3781
|
-
if (!matcher(c.value)) {
|
|
3782
|
-
problems.push(`cookie "${name}" value "${c.value}" did not match pattern "${pattern}"`);
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
if (problems.length > 0) {
|
|
3787
|
-
throw new Error(`assertCookies failed:
|
|
3788
|
-
- ${problems.join("\n - ")}
|
|
3789
|
-
(cookies seen: ${[...names].join(", ") || "<none>"})`);
|
|
3790
|
-
}
|
|
3791
|
-
sizeResults.push({ action, status: "passed" });
|
|
3792
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3793
|
-
} catch (e) {
|
|
3794
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
3795
|
-
sizeResults.push({ action, status: "failed", error: errMsg });
|
|
3796
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3797
|
-
throw e;
|
|
3798
|
-
}
|
|
3799
|
-
continue;
|
|
3800
|
-
}
|
|
3801
|
-
if (action.type === "expectResponse") {
|
|
3802
|
-
const respAction = action;
|
|
3803
|
-
try {
|
|
3804
|
-
const urlPattern = interpolateVariables(respAction.url, executionContext.variables);
|
|
3805
|
-
const urlMatch = compileMatcher(urlPattern, "url");
|
|
3806
|
-
const headerMatchers = respAction.headers ? Object.entries(respAction.headers).map(([name, pattern]) => ({
|
|
3807
|
-
name: name.toLowerCase(),
|
|
3808
|
-
test: compileMatcher(interpolateVariables(pattern, executionContext.variables), "substr"),
|
|
3809
|
-
pattern
|
|
3810
|
-
})) : [];
|
|
3811
|
-
const expectedStatus = respAction.status;
|
|
3812
|
-
let sinceTs;
|
|
3813
|
-
if (respAction.since === "testStart") {
|
|
3814
|
-
sinceTs = 0;
|
|
3815
|
-
} else if (typeof respAction.since === "number") {
|
|
3816
|
-
sinceTs = respAction.since;
|
|
3817
|
-
} else {
|
|
3818
|
-
sinceTs = lastStepEndTs;
|
|
3819
|
-
}
|
|
3820
|
-
const timeout = respAction.timeout ?? 5e3;
|
|
3821
|
-
const deadline = Date.now() + timeout;
|
|
3822
|
-
const findMatch = () => {
|
|
3823
|
-
for (const entry of responseLog.snapshot()) {
|
|
3824
|
-
if (entry.ts < sinceTs) continue;
|
|
3825
|
-
if (!urlMatch(entry.url)) continue;
|
|
3826
|
-
if (expectedStatus !== void 0 && entry.status !== expectedStatus) continue;
|
|
3827
|
-
let headersOk = true;
|
|
3828
|
-
for (const h of headerMatchers) {
|
|
3829
|
-
const value = entry.headers[h.name];
|
|
3830
|
-
if (value === void 0 || !h.test(value)) {
|
|
3831
|
-
headersOk = false;
|
|
3832
|
-
break;
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
if (headersOk) return entry;
|
|
3836
|
-
}
|
|
3837
|
-
return null;
|
|
3838
|
-
};
|
|
3839
|
-
let match = findMatch();
|
|
3840
|
-
while (!match && Date.now() < deadline) {
|
|
3841
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3842
|
-
match = findMatch();
|
|
3843
|
-
}
|
|
3844
|
-
if (!match) {
|
|
3845
|
-
const recent = responseLog.snapshot().slice(-5).map((e) => `${e.status} ${e.url}`).join("\n ");
|
|
3846
|
-
throw new Error(
|
|
3847
|
-
`expectResponse timed out after ${timeout}ms.
|
|
3848
|
-
url pattern: ${urlPattern}
|
|
3849
|
-
` + (expectedStatus !== void 0 ? ` expected status: ${expectedStatus}
|
|
3850
|
-
` : "") + (headerMatchers.length > 0 ? ` expected headers: ${headerMatchers.map((h) => `${h.name}=${h.pattern}`).join(", ")}
|
|
3851
|
-
` : "") + ` recent responses:
|
|
3852
|
-
${recent || "<none>"}`
|
|
3853
|
-
);
|
|
3854
|
-
}
|
|
3855
|
-
if (debugMode) {
|
|
3856
|
-
console.log(`[DEBUG] expectResponse matched: ${match.status} ${match.url}`);
|
|
3857
|
-
}
|
|
3858
|
-
sizeResults.push({ action, status: "passed" });
|
|
3859
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3860
|
-
} catch (e) {
|
|
3861
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
3862
|
-
sizeResults.push({ action, status: "failed", error: errMsg });
|
|
3863
|
-
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3864
|
-
throw e;
|
|
3865
|
-
}
|
|
3866
|
-
continue;
|
|
3867
|
-
}
|
|
4196
|
+
const stepStartTs = lastStepEndTs;
|
|
3868
4197
|
const actionExtras = await executeActionWithRetry(page, action, index, {
|
|
3869
4198
|
baseUrl: options.baseUrl ?? test.config?.web?.baseUrl,
|
|
3870
4199
|
context: executionContext,
|
|
@@ -3874,7 +4203,9 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3874
4203
|
aiConfig: options.aiConfig,
|
|
3875
4204
|
browserName,
|
|
3876
4205
|
healing: options.healing,
|
|
3877
|
-
testFilePath: options.testFilePath
|
|
4206
|
+
testFilePath: options.testFilePath,
|
|
4207
|
+
responseLog,
|
|
4208
|
+
stepStartTs
|
|
3878
4209
|
});
|
|
3879
4210
|
sizeResults.push({ action, status: "passed", logOutput: actionExtras.logOutput });
|
|
3880
4211
|
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
@@ -3952,23 +4283,6 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3952
4283
|
// src/executors/web/workflowExecutor.ts
|
|
3953
4284
|
init_esm_shims();
|
|
3954
4285
|
var defaultScreenshotDir2 = path5__default.join(process.cwd(), "artifacts", "screenshots");
|
|
3955
|
-
var interpolateTrackMetadata2 = (value, variables) => {
|
|
3956
|
-
if (typeof value === "string") {
|
|
3957
|
-
return interpolateVariables(value, variables);
|
|
3958
|
-
}
|
|
3959
|
-
if (Array.isArray(value)) {
|
|
3960
|
-
return value.map((entry) => interpolateTrackMetadata2(entry, variables));
|
|
3961
|
-
}
|
|
3962
|
-
if (value && typeof value === "object") {
|
|
3963
|
-
return Object.fromEntries(
|
|
3964
|
-
Object.entries(value).map(([key, entry]) => [
|
|
3965
|
-
key,
|
|
3966
|
-
interpolateTrackMetadata2(entry, variables)
|
|
3967
|
-
])
|
|
3968
|
-
);
|
|
3969
|
-
}
|
|
3970
|
-
return value;
|
|
3971
|
-
};
|
|
3972
4286
|
var getBrowser2 = (browser) => {
|
|
3973
4287
|
switch (browser) {
|
|
3974
4288
|
case "firefox":
|
|
@@ -3980,772 +4294,45 @@ var getBrowser2 = (browser) => {
|
|
|
3980
4294
|
}
|
|
3981
4295
|
};
|
|
3982
4296
|
function interpolateWorkflowVariables(value, currentVariables, testResults) {
|
|
3983
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (match,
|
|
3984
|
-
if (
|
|
3985
|
-
const [testId, _varName] =
|
|
4297
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, path7) => {
|
|
4298
|
+
if (path7.includes(".") && !path7.includes(":")) {
|
|
4299
|
+
const [testId, _varName] = path7.split(".", 2);
|
|
3986
4300
|
testResults.find((t) => t.id === testId);
|
|
3987
|
-
console.warn(`Cross-test variable interpolation {{${
|
|
4301
|
+
console.warn(`Cross-test variable interpolation {{${path7}}} not yet fully implemented`);
|
|
3988
4302
|
return match;
|
|
3989
4303
|
}
|
|
3990
|
-
const result = interpolateVariables(`{{${
|
|
4304
|
+
const result = interpolateVariables(`{{${path7}}}`, currentVariables);
|
|
3991
4305
|
return result;
|
|
3992
4306
|
});
|
|
3993
4307
|
}
|
|
3994
|
-
async function runTestInWorkflow(test, page, context, options,
|
|
4308
|
+
async function runTestInWorkflow(test, page, context, options, _workflowDir, testFilePath, workflowBaseUrl) {
|
|
3995
4309
|
const results = [];
|
|
3996
4310
|
const debugMode = options.debug ?? false;
|
|
3997
4311
|
const screenshotDir = defaultScreenshotDir2;
|
|
3998
|
-
const
|
|
3999
|
-
|
|
4000
|
-
try {
|
|
4001
|
-
const url = new URL(value, baseUrl);
|
|
4002
|
-
return url.toString();
|
|
4003
|
-
} catch {
|
|
4004
|
-
return value;
|
|
4005
|
-
}
|
|
4006
|
-
};
|
|
4007
|
-
const interpolate = (value) => {
|
|
4008
|
-
return interpolateVariables(value, context.variables);
|
|
4009
|
-
};
|
|
4312
|
+
const browserName = options.browser ?? "chromium";
|
|
4313
|
+
const baseUrl = test.config?.web?.baseUrl || workflowBaseUrl;
|
|
4010
4314
|
const responseLog = new ResponseLog();
|
|
4011
4315
|
responseLog.attach(page);
|
|
4012
|
-
let lastStepEndTs = Date.now();
|
|
4013
|
-
const resolveLocator2 = (locator) => {
|
|
4014
|
-
if (locator.testId) return page.getByTestId(locator.testId);
|
|
4015
|
-
if (locator.text) return page.getByText(locator.text);
|
|
4016
|
-
if (locator.css) return page.locator(locator.css);
|
|
4017
|
-
if (locator.xpath) return page.locator(`xpath=${locator.xpath}`);
|
|
4018
|
-
if (locator.role) {
|
|
4019
|
-
const options2 = {};
|
|
4020
|
-
if (locator.name) options2.name = locator.name;
|
|
4021
|
-
return page.getByRole(locator.role, options2);
|
|
4022
|
-
}
|
|
4023
|
-
if (locator.description) return page.getByText(locator.description);
|
|
4024
|
-
throw new Error("No usable selector found for locator");
|
|
4025
|
-
};
|
|
4026
|
-
const buildTrackPayload = (action, index, stepExtras) => {
|
|
4027
|
-
if (!("track" in action)) return null;
|
|
4028
|
-
const rawTrack = action.track;
|
|
4029
|
-
if (!rawTrack || typeof rawTrack !== "object") return null;
|
|
4030
|
-
const track2 = interpolateTrackMetadata2(rawTrack, context.variables);
|
|
4031
|
-
if (typeof track2.type !== "string" || typeof track2.id !== "string") return null;
|
|
4032
|
-
const { includeStepContext, ...rest } = track2;
|
|
4033
|
-
const payload = {
|
|
4034
|
-
type: track2.type,
|
|
4035
|
-
id: track2.id,
|
|
4036
|
-
...rest
|
|
4037
|
-
};
|
|
4038
|
-
if (includeStepContext) {
|
|
4039
|
-
payload.step = { index, ...action, ...stepExtras };
|
|
4040
|
-
}
|
|
4041
|
-
return payload;
|
|
4042
|
-
};
|
|
4043
4316
|
try {
|
|
4044
4317
|
for (const [index, action] of test.steps.entries()) {
|
|
4045
|
-
|
|
4318
|
+
const stepStartTs = Date.now();
|
|
4046
4319
|
if (debugMode) {
|
|
4047
4320
|
console.log(` [DEBUG] Step ${index + 1}: ${action.type}`);
|
|
4048
4321
|
}
|
|
4049
4322
|
try {
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
}
|
|
4064
|
-
await page.goto(target);
|
|
4065
|
-
break;
|
|
4066
|
-
}
|
|
4067
|
-
case "tap": {
|
|
4068
|
-
if (debugMode) console.log(` [DEBUG] Tapping element:`, action.target);
|
|
4069
|
-
const handle = resolveLocator2(action.target);
|
|
4070
|
-
await handle.click();
|
|
4071
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4072
|
-
});
|
|
4073
|
-
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
|
|
4074
|
-
});
|
|
4075
|
-
break;
|
|
4076
|
-
}
|
|
4077
|
-
case "input": {
|
|
4078
|
-
const interpolated = interpolate(action.value);
|
|
4079
|
-
if (debugMode) console.log(` [DEBUG] Input: ${interpolated}`);
|
|
4080
|
-
const handle = resolveLocator2(action.target);
|
|
4081
|
-
await handle.fill(interpolated);
|
|
4082
|
-
break;
|
|
4083
|
-
}
|
|
4084
|
-
case "clear": {
|
|
4085
|
-
if (debugMode) console.log(` [DEBUG] Clearing element:`, action.target);
|
|
4086
|
-
const handle = resolveLocator2(action.target);
|
|
4087
|
-
await handle.clear();
|
|
4088
|
-
break;
|
|
4089
|
-
}
|
|
4090
|
-
case "hover": {
|
|
4091
|
-
if (debugMode) console.log(` [DEBUG] Hovering element:`, action.target);
|
|
4092
|
-
const handle = resolveLocator2(action.target);
|
|
4093
|
-
await handle.hover();
|
|
4094
|
-
break;
|
|
4095
|
-
}
|
|
4096
|
-
case "select": {
|
|
4097
|
-
const interpolated = interpolate(action.value);
|
|
4098
|
-
if (debugMode) console.log(` [DEBUG] Selecting: ${interpolated}`);
|
|
4099
|
-
const handle = resolveLocator2(action.target);
|
|
4100
|
-
await handle.selectOption(interpolated);
|
|
4101
|
-
break;
|
|
4102
|
-
}
|
|
4103
|
-
case "check": {
|
|
4104
|
-
if (debugMode) console.log(` [DEBUG] Checking:`, action.target);
|
|
4105
|
-
const handle = resolveLocator2(action.target);
|
|
4106
|
-
await handle.check();
|
|
4107
|
-
break;
|
|
4108
|
-
}
|
|
4109
|
-
case "uncheck": {
|
|
4110
|
-
if (debugMode) console.log(` [DEBUG] Unchecking:`, action.target);
|
|
4111
|
-
const handle = resolveLocator2(action.target);
|
|
4112
|
-
await handle.uncheck();
|
|
4113
|
-
break;
|
|
4114
|
-
}
|
|
4115
|
-
case "press": {
|
|
4116
|
-
if (debugMode) console.log(` [DEBUG] Pressing key: ${action.key}`);
|
|
4117
|
-
if (action.target) {
|
|
4118
|
-
const handle = resolveLocator2(action.target);
|
|
4119
|
-
await handle.press(action.key);
|
|
4120
|
-
} else {
|
|
4121
|
-
await page.keyboard.press(action.key);
|
|
4122
|
-
}
|
|
4123
|
-
break;
|
|
4124
|
-
}
|
|
4125
|
-
case "focus": {
|
|
4126
|
-
if (debugMode) console.log(` [DEBUG] Focusing:`, action.target);
|
|
4127
|
-
const handle = resolveLocator2(action.target);
|
|
4128
|
-
await handle.focus();
|
|
4129
|
-
break;
|
|
4130
|
-
}
|
|
4131
|
-
case "assert": {
|
|
4132
|
-
if (debugMode) console.log(` [DEBUG] Assert:`, action.target);
|
|
4133
|
-
const handle = resolveLocator2(action.target);
|
|
4134
|
-
await handle.waitFor({ state: "visible" });
|
|
4135
|
-
if (action.value) {
|
|
4136
|
-
const interpolated = interpolate(action.value);
|
|
4137
|
-
const text = (await handle.textContent())?.trim() ?? "";
|
|
4138
|
-
if (!text.includes(interpolated)) {
|
|
4139
|
-
throw new Error(
|
|
4140
|
-
`Assertion failed: expected "${interpolated}", got "${text}"`
|
|
4141
|
-
);
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
break;
|
|
4145
|
-
}
|
|
4146
|
-
case "wait": {
|
|
4147
|
-
if (action.target) {
|
|
4148
|
-
const handle = resolveLocator2(action.target);
|
|
4149
|
-
await handle.waitFor({ state: "visible", timeout: action.timeout });
|
|
4150
|
-
} else {
|
|
4151
|
-
await page.waitForTimeout(action.timeout ?? 1e3);
|
|
4152
|
-
}
|
|
4153
|
-
break;
|
|
4154
|
-
}
|
|
4155
|
-
case "scroll": {
|
|
4156
|
-
if (action.target) {
|
|
4157
|
-
const handle = resolveLocator2(action.target);
|
|
4158
|
-
await handle.scrollIntoViewIfNeeded();
|
|
4159
|
-
} else {
|
|
4160
|
-
const amount = action.amount ?? 500;
|
|
4161
|
-
const direction = action.direction ?? "down";
|
|
4162
|
-
const deltaY = direction === "up" ? -amount : amount;
|
|
4163
|
-
await page.evaluate((value) => window.scrollBy(0, value), deltaY);
|
|
4164
|
-
}
|
|
4165
|
-
break;
|
|
4166
|
-
}
|
|
4167
|
-
case "screenshot": {
|
|
4168
|
-
const ssAction = action;
|
|
4169
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4170
|
-
});
|
|
4171
|
-
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
4172
|
-
});
|
|
4173
|
-
const waitBefore = ssAction.waitBefore ?? 500;
|
|
4174
|
-
if (waitBefore > 0) {
|
|
4175
|
-
await page.waitForTimeout(waitBefore);
|
|
4176
|
-
}
|
|
4177
|
-
const filename = ssAction.name ?? `step-${index + 1}.png`;
|
|
4178
|
-
const filePath = path5__default.join(screenshotDir, filename);
|
|
4179
|
-
await page.screenshot({ path: filePath, fullPage: true });
|
|
4180
|
-
results.push({ action, status: "passed", screenshotPath: filePath });
|
|
4181
|
-
const trackedPayload2 = buildTrackPayload(action, index, { screenshotPath: filePath });
|
|
4182
|
-
if (trackedPayload2) {
|
|
4183
|
-
await track(trackedPayload2);
|
|
4184
|
-
}
|
|
4185
|
-
continue;
|
|
4186
|
-
}
|
|
4187
|
-
case "saveStorageState": {
|
|
4188
|
-
const saveAction = action;
|
|
4189
|
-
if (saveAction.path) {
|
|
4190
|
-
const resolvedPath = interpolate(saveAction.path);
|
|
4191
|
-
const baseDir = path5__default.dirname(testFilePath);
|
|
4192
|
-
const absPath = path5__default.isAbsolute(resolvedPath) ? resolvedPath : path5__default.resolve(baseDir, resolvedPath);
|
|
4193
|
-
await page.context().storageState({ path: absPath });
|
|
4194
|
-
if (debugMode) {
|
|
4195
|
-
console.log(` [DEBUG] Saved storage state to ${absPath}`);
|
|
4196
|
-
}
|
|
4197
|
-
} else if (saveAction.handler) {
|
|
4198
|
-
const resolvedHandler = interpolate(saveAction.handler);
|
|
4199
|
-
const baseDir = path5__default.dirname(testFilePath);
|
|
4200
|
-
const absPath = path5__default.isAbsolute(resolvedHandler) ? resolvedHandler : path5__default.resolve(baseDir, resolvedHandler);
|
|
4201
|
-
let loadPath = absPath;
|
|
4202
|
-
if (absPath.endsWith(".ts")) {
|
|
4203
|
-
const jsPath = absPath.replace(/\.ts$/, ".js");
|
|
4204
|
-
try {
|
|
4205
|
-
await fs3__default.access(jsPath);
|
|
4206
|
-
loadPath = jsPath;
|
|
4207
|
-
} catch {
|
|
4208
|
-
}
|
|
4209
|
-
}
|
|
4210
|
-
const mod = await import(`${loadPath}?t=${Date.now()}`);
|
|
4211
|
-
const fn = mod.default ?? mod;
|
|
4212
|
-
if (typeof fn !== "function") {
|
|
4213
|
-
throw new Error(`saveStorageState handler at ${resolvedHandler} did not export a default function`);
|
|
4214
|
-
}
|
|
4215
|
-
await fn({
|
|
4216
|
-
page,
|
|
4217
|
-
context: page.context(),
|
|
4218
|
-
variables: context.variables
|
|
4219
|
-
});
|
|
4220
|
-
if (debugMode) {
|
|
4221
|
-
console.log(` [DEBUG] Ran custom saveStorageState handler: ${resolvedHandler}`);
|
|
4222
|
-
}
|
|
4223
|
-
} else {
|
|
4224
|
-
throw new Error("saveStorageState requires either `path` or `handler` (schema should have caught this)");
|
|
4225
|
-
}
|
|
4226
|
-
results.push({ action, status: "passed" });
|
|
4227
|
-
const trackedPayload2 = buildTrackPayload(action, index);
|
|
4228
|
-
if (trackedPayload2) {
|
|
4229
|
-
await track(trackedPayload2);
|
|
4230
|
-
}
|
|
4231
|
-
break;
|
|
4232
|
-
}
|
|
4233
|
-
case "assertCookies": {
|
|
4234
|
-
const cookieAction = action;
|
|
4235
|
-
const filterUrl = cookieAction.url ? interpolate(cookieAction.url) : void 0;
|
|
4236
|
-
const jar = await page.context().cookies(filterUrl);
|
|
4237
|
-
const names = new Set(jar.map((c) => c.name));
|
|
4238
|
-
const problems = [];
|
|
4239
|
-
if (cookieAction.has) {
|
|
4240
|
-
for (const name of cookieAction.has) {
|
|
4241
|
-
if (!names.has(name)) problems.push(`expected cookie "${name}" to be present`);
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
if (cookieAction.not) {
|
|
4245
|
-
for (const name of cookieAction.not) {
|
|
4246
|
-
if (names.has(name)) problems.push(`expected cookie "${name}" to be absent`);
|
|
4247
|
-
}
|
|
4248
|
-
}
|
|
4249
|
-
if (cookieAction.match) {
|
|
4250
|
-
for (const [name, pattern] of Object.entries(cookieAction.match)) {
|
|
4251
|
-
const c = jar.find((entry) => entry.name === name);
|
|
4252
|
-
if (!c) {
|
|
4253
|
-
problems.push(`expected cookie "${name}" to be present (for value match)`);
|
|
4254
|
-
continue;
|
|
4255
|
-
}
|
|
4256
|
-
const matcher = compileMatcher(interpolate(pattern), "substr");
|
|
4257
|
-
if (!matcher(c.value)) {
|
|
4258
|
-
problems.push(`cookie "${name}" value "${c.value}" did not match pattern "${pattern}"`);
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
if (problems.length > 0) {
|
|
4263
|
-
throw new Error(`assertCookies failed:
|
|
4264
|
-
- ${problems.join("\n - ")}
|
|
4265
|
-
(cookies seen: ${[...names].join(", ") || "<none>"})`);
|
|
4266
|
-
}
|
|
4267
|
-
results.push({ action, status: "passed" });
|
|
4268
|
-
break;
|
|
4269
|
-
}
|
|
4270
|
-
case "expectResponse": {
|
|
4271
|
-
const respAction = action;
|
|
4272
|
-
const urlPattern = interpolate(respAction.url);
|
|
4273
|
-
const urlMatch = compileMatcher(urlPattern, "url");
|
|
4274
|
-
const headerMatchers = respAction.headers ? Object.entries(respAction.headers).map(([name, pattern]) => ({
|
|
4275
|
-
name: name.toLowerCase(),
|
|
4276
|
-
test: compileMatcher(interpolate(pattern), "substr"),
|
|
4277
|
-
pattern
|
|
4278
|
-
})) : [];
|
|
4279
|
-
const expectedStatus = respAction.status;
|
|
4280
|
-
let sinceTs;
|
|
4281
|
-
if (respAction.since === "testStart") {
|
|
4282
|
-
sinceTs = 0;
|
|
4283
|
-
} else if (typeof respAction.since === "number") {
|
|
4284
|
-
sinceTs = respAction.since;
|
|
4285
|
-
} else {
|
|
4286
|
-
sinceTs = lastStepEndTs;
|
|
4287
|
-
}
|
|
4288
|
-
const timeout = respAction.timeout ?? 5e3;
|
|
4289
|
-
const deadline = Date.now() + timeout;
|
|
4290
|
-
const findMatch = () => {
|
|
4291
|
-
for (const entry of responseLog.snapshot()) {
|
|
4292
|
-
if (entry.ts < sinceTs) continue;
|
|
4293
|
-
if (!urlMatch(entry.url)) continue;
|
|
4294
|
-
if (expectedStatus !== void 0 && entry.status !== expectedStatus) continue;
|
|
4295
|
-
let headersOk = true;
|
|
4296
|
-
for (const h of headerMatchers) {
|
|
4297
|
-
const value = entry.headers[h.name];
|
|
4298
|
-
if (value === void 0 || !h.test(value)) {
|
|
4299
|
-
headersOk = false;
|
|
4300
|
-
break;
|
|
4301
|
-
}
|
|
4302
|
-
}
|
|
4303
|
-
if (headersOk) return entry;
|
|
4304
|
-
}
|
|
4305
|
-
return null;
|
|
4306
|
-
};
|
|
4307
|
-
let match = findMatch();
|
|
4308
|
-
while (!match && Date.now() < deadline) {
|
|
4309
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4310
|
-
match = findMatch();
|
|
4311
|
-
}
|
|
4312
|
-
if (!match) {
|
|
4313
|
-
const recent = responseLog.snapshot().slice(-5).map((e) => `${e.status} ${e.url}`).join("\n ");
|
|
4314
|
-
throw new Error(
|
|
4315
|
-
`expectResponse timed out after ${timeout}ms.
|
|
4316
|
-
url pattern: ${urlPattern}
|
|
4317
|
-
` + (expectedStatus !== void 0 ? ` expected status: ${expectedStatus}
|
|
4318
|
-
` : "") + (headerMatchers.length > 0 ? ` expected headers: ${headerMatchers.map((h) => `${h.name}=${h.pattern}`).join(", ")}
|
|
4319
|
-
` : "") + ` recent responses:
|
|
4320
|
-
${recent || "<none>"}`
|
|
4321
|
-
);
|
|
4322
|
-
}
|
|
4323
|
-
if (debugMode) {
|
|
4324
|
-
console.log(` [DEBUG] expectResponse matched: ${match.status} ${match.url}`);
|
|
4325
|
-
}
|
|
4326
|
-
results.push({ action, status: "passed" });
|
|
4327
|
-
break;
|
|
4328
|
-
}
|
|
4329
|
-
case "setVar": {
|
|
4330
|
-
let value;
|
|
4331
|
-
if (action.value) {
|
|
4332
|
-
value = interpolate(action.value);
|
|
4333
|
-
} else if (action.from === "response") {
|
|
4334
|
-
throw new Error("setVar from response not yet implemented");
|
|
4335
|
-
} else if (action.from === "element") {
|
|
4336
|
-
throw new Error("setVar from element not yet implemented");
|
|
4337
|
-
} else if (action.from === "email") {
|
|
4338
|
-
throw new Error("Use email.extractCode or email.extractLink instead");
|
|
4339
|
-
} else {
|
|
4340
|
-
throw new Error("setVar requires value or from");
|
|
4341
|
-
}
|
|
4342
|
-
context.variables.set(action.name, value);
|
|
4343
|
-
if (debugMode) console.log(` [DEBUG] Set variable ${action.name} = ${value}`);
|
|
4344
|
-
break;
|
|
4345
|
-
}
|
|
4346
|
-
case "email.waitFor": {
|
|
4347
|
-
if (!context.emailClient) {
|
|
4348
|
-
throw new Error("Email client not configured");
|
|
4349
|
-
}
|
|
4350
|
-
const mailbox = interpolate(action.mailbox);
|
|
4351
|
-
context.lastEmail = await context.emailClient.waitForEmail(mailbox, {
|
|
4352
|
-
timeout: action.timeout,
|
|
4353
|
-
subjectContains: action.subjectContains
|
|
4354
|
-
});
|
|
4355
|
-
break;
|
|
4356
|
-
}
|
|
4357
|
-
case "email.extractCode": {
|
|
4358
|
-
if (!context.emailClient) {
|
|
4359
|
-
throw new Error("Email client not configured");
|
|
4360
|
-
}
|
|
4361
|
-
if (!context.lastEmail) {
|
|
4362
|
-
throw new Error("No email loaded - call email.waitFor first");
|
|
4363
|
-
}
|
|
4364
|
-
const code = context.emailClient.extractCode(
|
|
4365
|
-
context.lastEmail,
|
|
4366
|
-
action.pattern ? new RegExp(action.pattern) : void 0
|
|
4367
|
-
);
|
|
4368
|
-
if (!code) {
|
|
4369
|
-
throw new Error("No code found in email");
|
|
4370
|
-
}
|
|
4371
|
-
context.variables.set(action.saveTo, code);
|
|
4372
|
-
break;
|
|
4373
|
-
}
|
|
4374
|
-
case "email.extractLink": {
|
|
4375
|
-
if (!context.emailClient) {
|
|
4376
|
-
throw new Error("Email client not configured");
|
|
4377
|
-
}
|
|
4378
|
-
if (!context.lastEmail) {
|
|
4379
|
-
throw new Error("No email loaded - call email.waitFor first");
|
|
4380
|
-
}
|
|
4381
|
-
const link = context.emailClient.extractLink(
|
|
4382
|
-
context.lastEmail,
|
|
4383
|
-
action.pattern ? new RegExp(action.pattern) : void 0
|
|
4384
|
-
);
|
|
4385
|
-
if (!link) {
|
|
4386
|
-
throw new Error("No link found in email");
|
|
4387
|
-
}
|
|
4388
|
-
context.variables.set(action.saveTo, link);
|
|
4389
|
-
break;
|
|
4390
|
-
}
|
|
4391
|
-
case "email.clear": {
|
|
4392
|
-
if (!context.emailClient) {
|
|
4393
|
-
throw new Error("Email client not configured");
|
|
4394
|
-
}
|
|
4395
|
-
const mailbox = interpolate(action.mailbox);
|
|
4396
|
-
await context.emailClient.clearMailbox(mailbox);
|
|
4397
|
-
break;
|
|
4398
|
-
}
|
|
4399
|
-
case "appwrite.verifyEmail": {
|
|
4400
|
-
if (!context.appwriteContext.userId) {
|
|
4401
|
-
throw new Error("No user tracked. appwrite.verifyEmail requires a user signup first.");
|
|
4402
|
-
}
|
|
4403
|
-
if (!context.appwriteConfig?.apiKey) {
|
|
4404
|
-
throw new Error("appwrite.verifyEmail requires appwrite.apiKey in config");
|
|
4405
|
-
}
|
|
4406
|
-
const { Client: Client2, Users: Users2 } = await import('node-appwrite');
|
|
4407
|
-
const client = new Client2().setEndpoint(context.appwriteConfig.endpoint).setProject(context.appwriteConfig.projectId).setKey(context.appwriteConfig.apiKey);
|
|
4408
|
-
const users = new Users2(client);
|
|
4409
|
-
await users.updateEmailVerification(context.appwriteContext.userId, true);
|
|
4410
|
-
if (debugMode) console.log(` [DEBUG] Verified email for user ${context.appwriteContext.userId}`);
|
|
4411
|
-
break;
|
|
4412
|
-
}
|
|
4413
|
-
case "debug": {
|
|
4414
|
-
console.log(" [DEBUG] Pausing execution - Playwright Inspector will open");
|
|
4415
|
-
await page.pause();
|
|
4416
|
-
break;
|
|
4417
|
-
}
|
|
4418
|
-
case "waitForSelector": {
|
|
4419
|
-
const handle = resolveLocator2(action.target);
|
|
4420
|
-
const timeout = action.timeout ?? 3e4;
|
|
4421
|
-
if (debugMode) {
|
|
4422
|
-
console.log(` [DEBUG] Waiting for element to be ${action.state}:`, action.target);
|
|
4423
|
-
}
|
|
4424
|
-
const waitForCondition2 = async (checkFn, timeoutMs, errorMessage) => {
|
|
4425
|
-
const start = Date.now();
|
|
4426
|
-
while (Date.now() - start < timeoutMs) {
|
|
4427
|
-
if (await checkFn()) return;
|
|
4428
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
4429
|
-
}
|
|
4430
|
-
throw new Error(errorMessage);
|
|
4431
|
-
};
|
|
4432
|
-
switch (action.state) {
|
|
4433
|
-
case "visible":
|
|
4434
|
-
case "hidden":
|
|
4435
|
-
case "attached":
|
|
4436
|
-
case "detached":
|
|
4437
|
-
await handle.waitFor({ state: action.state, timeout });
|
|
4438
|
-
break;
|
|
4439
|
-
case "enabled":
|
|
4440
|
-
await waitForCondition2(
|
|
4441
|
-
() => handle.isEnabled(),
|
|
4442
|
-
timeout,
|
|
4443
|
-
`Element did not become enabled within ${timeout}ms`
|
|
4444
|
-
);
|
|
4445
|
-
break;
|
|
4446
|
-
case "disabled":
|
|
4447
|
-
await waitForCondition2(
|
|
4448
|
-
() => handle.isDisabled(),
|
|
4449
|
-
timeout,
|
|
4450
|
-
`Element did not become disabled within ${timeout}ms`
|
|
4451
|
-
);
|
|
4452
|
-
break;
|
|
4453
|
-
}
|
|
4454
|
-
break;
|
|
4455
|
-
}
|
|
4456
|
-
case "conditional": {
|
|
4457
|
-
const handle = resolveLocator2(action.condition.target);
|
|
4458
|
-
let conditionMet = false;
|
|
4459
|
-
if (debugMode) {
|
|
4460
|
-
console.log(` [DEBUG] Checking condition ${action.condition.type}:`, action.condition.target);
|
|
4461
|
-
}
|
|
4462
|
-
try {
|
|
4463
|
-
switch (action.condition.type) {
|
|
4464
|
-
case "exists":
|
|
4465
|
-
await handle.waitFor({ state: "attached", timeout: 500 });
|
|
4466
|
-
conditionMet = true;
|
|
4467
|
-
break;
|
|
4468
|
-
case "notExists":
|
|
4469
|
-
try {
|
|
4470
|
-
await handle.waitFor({ state: "detached", timeout: 500 });
|
|
4471
|
-
conditionMet = true;
|
|
4472
|
-
} catch {
|
|
4473
|
-
conditionMet = false;
|
|
4474
|
-
}
|
|
4475
|
-
break;
|
|
4476
|
-
case "visible":
|
|
4477
|
-
conditionMet = await handle.isVisible();
|
|
4478
|
-
break;
|
|
4479
|
-
case "hidden":
|
|
4480
|
-
conditionMet = !await handle.isVisible();
|
|
4481
|
-
break;
|
|
4482
|
-
}
|
|
4483
|
-
} catch {
|
|
4484
|
-
conditionMet = action.condition.type === "notExists";
|
|
4485
|
-
}
|
|
4486
|
-
if (debugMode) {
|
|
4487
|
-
console.log(` [DEBUG] Condition result: ${conditionMet}`);
|
|
4488
|
-
}
|
|
4489
|
-
const stepsToRun = conditionMet ? action.then : action.else ?? [];
|
|
4490
|
-
for (const nestedAction of stepsToRun) {
|
|
4491
|
-
switch (nestedAction.type) {
|
|
4492
|
-
case "screenshot": {
|
|
4493
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4494
|
-
});
|
|
4495
|
-
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
4496
|
-
});
|
|
4497
|
-
const filename = nestedAction.name ?? `conditional-step.png`;
|
|
4498
|
-
const filePath = path5__default.join(screenshotDir, filename);
|
|
4499
|
-
await page.screenshot({ path: filePath, fullPage: true });
|
|
4500
|
-
results.push({ action: nestedAction, status: "passed", screenshotPath: filePath });
|
|
4501
|
-
const trackedPayload2 = buildTrackPayload(nestedAction, index, { screenshotPath: filePath });
|
|
4502
|
-
if (trackedPayload2) {
|
|
4503
|
-
await track(trackedPayload2);
|
|
4504
|
-
}
|
|
4505
|
-
break;
|
|
4506
|
-
}
|
|
4507
|
-
case "evaluate":
|
|
4508
|
-
throw new Error("Evaluate action in nested context (conditional/waitForBranch) is not yet supported");
|
|
4509
|
-
case "fail": {
|
|
4510
|
-
throw new Error(nestedAction.message);
|
|
4511
|
-
}
|
|
4512
|
-
default:
|
|
4513
|
-
throw new Error(`Nested action type ${nestedAction.type} in conditional not yet supported`);
|
|
4514
|
-
}
|
|
4515
|
-
}
|
|
4516
|
-
break;
|
|
4517
|
-
}
|
|
4518
|
-
case "evaluate": {
|
|
4519
|
-
const evalAction = action;
|
|
4520
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4521
|
-
});
|
|
4522
|
-
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
4523
|
-
});
|
|
4524
|
-
const waitBefore = evalAction.waitBefore ?? 500;
|
|
4525
|
-
if (waitBefore > 0) {
|
|
4526
|
-
await page.waitForTimeout(waitBefore);
|
|
4527
|
-
}
|
|
4528
|
-
const evalScreenshotPath = path5__default.join(screenshotDir, `evaluate-step-${index + 1}.png`);
|
|
4529
|
-
const screenshotBuffer = await page.screenshot({
|
|
4530
|
-
path: evalScreenshotPath,
|
|
4531
|
-
fullPage: evalAction.fullPage ?? true
|
|
4532
|
-
});
|
|
4533
|
-
const expectedRaw = evalAction.expected;
|
|
4534
|
-
const expectedArray = Array.isArray(expectedRaw) ? expectedRaw.map((e) => interpolate(e)) : [interpolate(expectedRaw)];
|
|
4535
|
-
const evalResult = await evaluate({
|
|
4536
|
-
expected: expectedArray,
|
|
4537
|
-
mode: evalAction.mode ?? "auto",
|
|
4538
|
-
regex: evalAction.regex ?? false,
|
|
4539
|
-
prompt: evalAction.prompt,
|
|
4540
|
-
confidence: evalAction.confidence ?? 60,
|
|
4541
|
-
screenshotBuffer,
|
|
4542
|
-
screenshotPath: evalScreenshotPath,
|
|
4543
|
-
aiConfig: options.aiConfig
|
|
4544
|
-
});
|
|
4545
|
-
if (debugMode) {
|
|
4546
|
-
console.log(` [DEBUG] Evaluate result: ${evalResult.passed ? "PASSED" : "FAILED"} (${evalResult.mode})`);
|
|
4547
|
-
console.log(` [DEBUG] Reason: ${evalResult.reason}`);
|
|
4548
|
-
}
|
|
4549
|
-
if (!evalResult.passed) {
|
|
4550
|
-
throw new Error(`Evaluate failed (${evalResult.mode} mode): ${evalResult.reason}`);
|
|
4551
|
-
}
|
|
4552
|
-
results.push({
|
|
4553
|
-
action,
|
|
4554
|
-
status: "passed",
|
|
4555
|
-
screenshotPath: evalScreenshotPath,
|
|
4556
|
-
logOutput: `Evaluate passed (${evalResult.mode}): ${evalResult.reason}`
|
|
4557
|
-
});
|
|
4558
|
-
continue;
|
|
4559
|
-
}
|
|
4560
|
-
case "fail": {
|
|
4561
|
-
throw new Error(action.message);
|
|
4562
|
-
}
|
|
4563
|
-
case "waitForBranch": {
|
|
4564
|
-
const wfbAction = action;
|
|
4565
|
-
const handle = resolveLocator2(wfbAction.target);
|
|
4566
|
-
const timeout = wfbAction.timeout ?? 3e4;
|
|
4567
|
-
const state = wfbAction.state ?? "visible";
|
|
4568
|
-
const pollInterval = wfbAction.pollInterval ?? 100;
|
|
4569
|
-
if (debugMode) {
|
|
4570
|
-
console.log(` [DEBUG] waitForBranch: waiting for element to be ${state}:`, wfbAction.target);
|
|
4571
|
-
}
|
|
4572
|
-
const startTime = Date.now();
|
|
4573
|
-
let elementAppeared = false;
|
|
4574
|
-
while (Date.now() - startTime < timeout) {
|
|
4575
|
-
try {
|
|
4576
|
-
let conditionMet = false;
|
|
4577
|
-
switch (state) {
|
|
4578
|
-
case "visible":
|
|
4579
|
-
conditionMet = await handle.isVisible();
|
|
4580
|
-
break;
|
|
4581
|
-
case "attached":
|
|
4582
|
-
conditionMet = await handle.count() > 0;
|
|
4583
|
-
break;
|
|
4584
|
-
case "enabled":
|
|
4585
|
-
conditionMet = await handle.isEnabled().catch(() => false);
|
|
4586
|
-
break;
|
|
4587
|
-
}
|
|
4588
|
-
if (conditionMet) {
|
|
4589
|
-
elementAppeared = true;
|
|
4590
|
-
break;
|
|
4591
|
-
}
|
|
4592
|
-
} catch {
|
|
4593
|
-
}
|
|
4594
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
4595
|
-
}
|
|
4596
|
-
if (debugMode) {
|
|
4597
|
-
console.log(` [DEBUG] waitForBranch: element ${elementAppeared ? "appeared" : "timed out"}`);
|
|
4598
|
-
}
|
|
4599
|
-
const branch = elementAppeared ? wfbAction.onAppear : wfbAction.onTimeout;
|
|
4600
|
-
if (branch) {
|
|
4601
|
-
if (Array.isArray(branch)) {
|
|
4602
|
-
for (const nestedAction of branch) {
|
|
4603
|
-
if (debugMode) {
|
|
4604
|
-
console.log(` [DEBUG] waitForBranch: executing nested action ${nestedAction.type}`);
|
|
4605
|
-
}
|
|
4606
|
-
switch (nestedAction.type) {
|
|
4607
|
-
case "navigate": {
|
|
4608
|
-
const interpolated = interpolate(nestedAction.value);
|
|
4609
|
-
const baseUrl = test.config?.web?.baseUrl || workflowBaseUrl;
|
|
4610
|
-
const target = resolveUrl2(interpolated, baseUrl);
|
|
4611
|
-
await page.goto(target);
|
|
4612
|
-
break;
|
|
4613
|
-
}
|
|
4614
|
-
case "tap": {
|
|
4615
|
-
const nestedHandle = resolveLocator2(nestedAction.target);
|
|
4616
|
-
await nestedHandle.click();
|
|
4617
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4618
|
-
});
|
|
4619
|
-
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
|
|
4620
|
-
});
|
|
4621
|
-
break;
|
|
4622
|
-
}
|
|
4623
|
-
case "input": {
|
|
4624
|
-
const interpolated = interpolate(nestedAction.value);
|
|
4625
|
-
const nestedHandle = resolveLocator2(nestedAction.target);
|
|
4626
|
-
await nestedHandle.fill(interpolated);
|
|
4627
|
-
break;
|
|
4628
|
-
}
|
|
4629
|
-
case "screenshot": {
|
|
4630
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4631
|
-
});
|
|
4632
|
-
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
4633
|
-
});
|
|
4634
|
-
const nestedSsAction = nestedAction;
|
|
4635
|
-
const nestedWaitBefore = nestedSsAction.waitBefore ?? 500;
|
|
4636
|
-
if (nestedWaitBefore > 0) {
|
|
4637
|
-
await page.waitForTimeout(nestedWaitBefore);
|
|
4638
|
-
}
|
|
4639
|
-
const filename = nestedSsAction.name ?? `waitForBranch-step.png`;
|
|
4640
|
-
const filePath = path5__default.join(screenshotDir, filename);
|
|
4641
|
-
await page.screenshot({ path: filePath, fullPage: true });
|
|
4642
|
-
results.push({ action: nestedAction, status: "passed", screenshotPath: filePath });
|
|
4643
|
-
const trackedPayload2 = buildTrackPayload(nestedAction, index, { screenshotPath: filePath });
|
|
4644
|
-
if (trackedPayload2) {
|
|
4645
|
-
await track(trackedPayload2);
|
|
4646
|
-
}
|
|
4647
|
-
break;
|
|
4648
|
-
}
|
|
4649
|
-
case "wait": {
|
|
4650
|
-
if (nestedAction.target) {
|
|
4651
|
-
const nestedHandle = resolveLocator2(nestedAction.target);
|
|
4652
|
-
await nestedHandle.waitFor({ state: "visible", timeout: nestedAction.timeout });
|
|
4653
|
-
} else {
|
|
4654
|
-
await page.waitForTimeout(nestedAction.timeout ?? 1e3);
|
|
4655
|
-
}
|
|
4656
|
-
break;
|
|
4657
|
-
}
|
|
4658
|
-
case "evaluate":
|
|
4659
|
-
throw new Error("Evaluate action in nested context (conditional/waitForBranch) is not yet supported");
|
|
4660
|
-
case "fail": {
|
|
4661
|
-
throw new Error(nestedAction.message);
|
|
4662
|
-
}
|
|
4663
|
-
case "setVar": {
|
|
4664
|
-
let value;
|
|
4665
|
-
if (nestedAction.value) {
|
|
4666
|
-
value = interpolate(nestedAction.value);
|
|
4667
|
-
} else {
|
|
4668
|
-
throw new Error("setVar in waitForBranch requires value");
|
|
4669
|
-
}
|
|
4670
|
-
context.variables.set(nestedAction.name, value);
|
|
4671
|
-
if (debugMode) console.log(` [DEBUG] Set variable ${nestedAction.name} = ${value}`);
|
|
4672
|
-
break;
|
|
4673
|
-
}
|
|
4674
|
-
case "assert": {
|
|
4675
|
-
const nestedHandle = resolveLocator2(nestedAction.target);
|
|
4676
|
-
await nestedHandle.waitFor({ state: "visible" });
|
|
4677
|
-
if (nestedAction.value) {
|
|
4678
|
-
const interpolated = interpolate(nestedAction.value);
|
|
4679
|
-
const text = (await nestedHandle.textContent())?.trim() ?? "";
|
|
4680
|
-
if (!text.includes(interpolated)) {
|
|
4681
|
-
throw new Error(
|
|
4682
|
-
`Assertion failed: expected "${interpolated}", got "${text}"`
|
|
4683
|
-
);
|
|
4684
|
-
}
|
|
4685
|
-
}
|
|
4686
|
-
break;
|
|
4687
|
-
}
|
|
4688
|
-
default:
|
|
4689
|
-
throw new Error(`Nested action type ${nestedAction.type} in waitForBranch not yet supported`);
|
|
4690
|
-
}
|
|
4691
|
-
if (nestedAction.type !== "screenshot") {
|
|
4692
|
-
const trackedPayload2 = buildTrackPayload(nestedAction, index);
|
|
4693
|
-
if (trackedPayload2) {
|
|
4694
|
-
await track(trackedPayload2);
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
4697
|
-
results.push({ action: nestedAction, status: "passed" });
|
|
4698
|
-
}
|
|
4699
|
-
} else if (typeof branch === "object" && "workflow" in branch) {
|
|
4700
|
-
const workflowPath = path5__default.resolve(workflowDir, branch.workflow);
|
|
4701
|
-
if (debugMode) {
|
|
4702
|
-
console.log(` [DEBUG] waitForBranch: loading workflow from ${workflowPath}`);
|
|
4703
|
-
}
|
|
4704
|
-
const { loadWorkflowDefinition } = await import('./loader-2CW6OEXJ.js');
|
|
4705
|
-
const nestedWorkflow = await loadWorkflowDefinition(workflowPath);
|
|
4706
|
-
if (branch.variables) {
|
|
4707
|
-
for (const [key, value] of Object.entries(branch.variables)) {
|
|
4708
|
-
const interpolated = interpolate(value);
|
|
4709
|
-
context.variables.set(key, interpolated);
|
|
4710
|
-
}
|
|
4711
|
-
}
|
|
4712
|
-
for (const testRef of nestedWorkflow.tests) {
|
|
4713
|
-
const testFilePath2 = path5__default.resolve(path5__default.dirname(workflowPath), testRef.file);
|
|
4714
|
-
const nestedTest = await loadTestDefinition(testFilePath2);
|
|
4715
|
-
if (nestedTest.variables) {
|
|
4716
|
-
for (const [key, value] of Object.entries(nestedTest.variables)) {
|
|
4717
|
-
const interpolated = interpolateVariables(value, context.variables);
|
|
4718
|
-
context.variables.set(key, interpolated);
|
|
4719
|
-
}
|
|
4720
|
-
}
|
|
4721
|
-
const nestedResult = await runTestInWorkflow(
|
|
4722
|
-
nestedTest,
|
|
4723
|
-
page,
|
|
4724
|
-
context,
|
|
4725
|
-
options,
|
|
4726
|
-
path5__default.dirname(workflowPath),
|
|
4727
|
-
testFilePath2,
|
|
4728
|
-
nestedWorkflow.config?.web?.baseUrl ?? workflowBaseUrl
|
|
4729
|
-
);
|
|
4730
|
-
results.push(...nestedResult.steps);
|
|
4731
|
-
if (nestedResult.status === "failed") {
|
|
4732
|
-
throw new Error(`Nested workflow test failed in waitForBranch`);
|
|
4733
|
-
}
|
|
4734
|
-
}
|
|
4735
|
-
}
|
|
4736
|
-
} else if (!elementAppeared && debugMode) {
|
|
4737
|
-
console.log(` [DEBUG] waitForBranch: timeout occurred but no onTimeout branch defined, continuing silently`);
|
|
4738
|
-
}
|
|
4739
|
-
break;
|
|
4740
|
-
}
|
|
4741
|
-
default:
|
|
4742
|
-
throw new Error(`Unsupported action type: ${action.type}`);
|
|
4743
|
-
}
|
|
4744
|
-
const trackedPayload = buildTrackPayload(action, index);
|
|
4745
|
-
if (trackedPayload) {
|
|
4746
|
-
await track(trackedPayload);
|
|
4747
|
-
}
|
|
4748
|
-
results.push({ action, status: "passed" });
|
|
4323
|
+
const actionExtras = await executeActionWithRetry(page, action, index, {
|
|
4324
|
+
baseUrl,
|
|
4325
|
+
context,
|
|
4326
|
+
screenshotDir,
|
|
4327
|
+
debugMode,
|
|
4328
|
+
interactive: false,
|
|
4329
|
+
aiConfig: options.aiConfig,
|
|
4330
|
+
browserName,
|
|
4331
|
+
testFilePath,
|
|
4332
|
+
responseLog,
|
|
4333
|
+
stepStartTs
|
|
4334
|
+
});
|
|
4335
|
+
results.push({ action, status: "passed", logOutput: actionExtras.logOutput });
|
|
4749
4336
|
} catch (error) {
|
|
4750
4337
|
const message = error instanceof Error ? error.message : String(error);
|
|
4751
4338
|
results.push({ action, status: "failed", error: message });
|
|
@@ -4955,13 +4542,14 @@ async function runWorkflowWithContext(workflow, workflowFilePath, options) {
|
|
|
4955
4542
|
Starting workflow: ${workflow.name}`);
|
|
4956
4543
|
console.log(`Session ID: ${sessionId}
|
|
4957
4544
|
`);
|
|
4958
|
-
|
|
4545
|
+
const effectiveAppwriteConfig = workflow.config?.appwrite ? {
|
|
4546
|
+
endpoint: workflow.config.appwrite.endpoint,
|
|
4547
|
+
projectId: workflow.config.appwrite.projectId,
|
|
4548
|
+
apiKey: workflow.config.appwrite.apiKey
|
|
4549
|
+
} : options.appwriteConfig;
|
|
4550
|
+
if (effectiveAppwriteConfig) {
|
|
4959
4551
|
if (!executionContext.appwriteConfig) {
|
|
4960
|
-
executionContext.appwriteConfig =
|
|
4961
|
-
endpoint: workflow.config.appwrite.endpoint,
|
|
4962
|
-
projectId: workflow.config.appwrite.projectId,
|
|
4963
|
-
apiKey: workflow.config.appwrite.apiKey
|
|
4964
|
-
};
|
|
4552
|
+
executionContext.appwriteConfig = effectiveAppwriteConfig;
|
|
4965
4553
|
}
|
|
4966
4554
|
setupAppwriteTracking(page, executionContext);
|
|
4967
4555
|
}
|
|
@@ -5163,15 +4751,20 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5163
4751
|
process.env.INTELLITESTER_SESSION_ID = sessionId;
|
|
5164
4752
|
process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
|
|
5165
4753
|
}
|
|
4754
|
+
const trackingAppwrite = workflow.config?.appwrite ? {
|
|
4755
|
+
endpoint: workflow.config.appwrite.endpoint,
|
|
4756
|
+
projectId: workflow.config.appwrite.projectId,
|
|
4757
|
+
apiKey: workflow.config.appwrite.apiKey
|
|
4758
|
+
} : options.appwriteConfig;
|
|
5166
4759
|
fileTracking = await initFileTracking({
|
|
5167
4760
|
sessionId,
|
|
5168
4761
|
cleanupConfig,
|
|
5169
4762
|
trackDir: options.trackDir,
|
|
5170
|
-
providerConfig:
|
|
4763
|
+
providerConfig: trackingAppwrite ? {
|
|
5171
4764
|
provider: "appwrite",
|
|
5172
|
-
endpoint:
|
|
5173
|
-
projectId:
|
|
5174
|
-
apiKey:
|
|
4765
|
+
endpoint: trackingAppwrite.endpoint,
|
|
4766
|
+
projectId: trackingAppwrite.projectId,
|
|
4767
|
+
apiKey: trackingAppwrite.apiKey
|
|
5175
4768
|
} : void 0
|
|
5176
4769
|
});
|
|
5177
4770
|
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
@@ -5219,12 +4812,13 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5219
4812
|
};
|
|
5220
4813
|
process.on("SIGINT", signalCleanup);
|
|
5221
4814
|
process.on("SIGTERM", signalCleanup);
|
|
5222
|
-
const browserName =
|
|
4815
|
+
const browserName = workflow.config?.web?.browser ?? options.browser ?? "chromium";
|
|
5223
4816
|
const headless = options.headed === true ? false : workflow.config?.web?.headless ?? true;
|
|
5224
4817
|
console.log(`Launching ${browserName}${headless ? " (headless)" : " (visible)"}...`);
|
|
5225
4818
|
const browser = await getBrowser2(browserName).launch(getBrowserLaunchOptions({ headless, browser: browserName }));
|
|
5226
4819
|
console.log(`Browser launched successfully`);
|
|
5227
|
-
const
|
|
4820
|
+
const workflowTestSizes = workflow.config?.web?.testSizes;
|
|
4821
|
+
const testSizes = workflowTestSizes && workflowTestSizes.length > 0 ? workflowTestSizes : options.testSizes && options.testSizes.length > 0 ? options.testSizes : ["1920x1080"];
|
|
5228
4822
|
const viewportSizes = [];
|
|
5229
4823
|
for (const size of testSizes) {
|
|
5230
4824
|
const viewport = parseViewportSize(size);
|
|
@@ -5237,11 +4831,12 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5237
4831
|
}
|
|
5238
4832
|
const allTestResults = [];
|
|
5239
4833
|
let anyFailed = false;
|
|
5240
|
-
const
|
|
5241
|
-
const
|
|
4834
|
+
const optionsStorageState = typeof options.storageState === "string" ? path5__default.isAbsolute(options.storageState) ? options.storageState : path5__default.resolve(process.cwd(), options.storageState) : options.storageState;
|
|
4835
|
+
const workflowStorageState = resolveStorageStatePath(
|
|
5242
4836
|
workflow.config?.web?.storageState,
|
|
5243
4837
|
workflowDir
|
|
5244
4838
|
);
|
|
4839
|
+
const storageState = workflowStorageState ?? optionsStorageState;
|
|
5245
4840
|
let browserContext = await browser.newContext({
|
|
5246
4841
|
viewport: viewportSizes[0].viewport,
|
|
5247
4842
|
...storageState ? { storageState } : {}
|
|
@@ -5257,7 +4852,7 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5257
4852
|
endpoint: workflow.config.appwrite.endpoint,
|
|
5258
4853
|
projectId: workflow.config.appwrite.projectId,
|
|
5259
4854
|
apiKey: workflow.config.appwrite.apiKey
|
|
5260
|
-
} :
|
|
4855
|
+
} : options.appwriteConfig
|
|
5261
4856
|
};
|
|
5262
4857
|
if (workflow.variables) {
|
|
5263
4858
|
for (const [key, value] of Object.entries(workflow.variables)) {
|
|
@@ -5430,6 +5025,6 @@ Collected ${serverResources.length} server-tracked resources`);
|
|
|
5430
5025
|
// src/executors/web/index.ts
|
|
5431
5026
|
init_esm_shims();
|
|
5432
5027
|
|
|
5433
|
-
export { buildCompletionOptions, buildModel, createTestContext, generateFillerText, generateRandomEmail, generateRandomPhone, generateRandomPhoto, generateRandomUsername, getBrowserLaunchOptions, initFileTracking, interpolateVariables, mergeFileTrackedResources, parseViewportSize, resolveStorageStatePath, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, webServerManager };
|
|
5434
|
-
//# sourceMappingURL=chunk-
|
|
5435
|
-
//# sourceMappingURL=chunk-
|
|
5028
|
+
export { buildCompletionOptions, buildModel, createTestContext, executeActionWithRetry, generateFillerText, generateRandomEmail, generateRandomPhone, generateRandomPhoto, generateRandomUsername, getBrowserLaunchOptions, initFileTracking, interpolateVariables, mergeFileTrackedResources, parseViewportSize, resolveStorageStatePath, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, webServerManager };
|
|
5029
|
+
//# sourceMappingURL=chunk-KRXZC5J5.js.map
|
|
5030
|
+
//# sourceMappingURL=chunk-KRXZC5J5.js.map
|