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.
@@ -1,4 +1,4 @@
1
- import { loadTestDefinition } from './chunk-PDU3AEXH.js';
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, ChatMessage, runAgent } from 'blazen';
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 screenshot. Always call this exactly once.",
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 screenshot meets the expected criteria."
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-2CW6OEXJ.js');
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
- baseUrl,
3205
- context,
3206
- screenshotDir,
3207
- debugMode,
3208
- interactive,
3209
- aiConfig,
3210
- browserName
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
- if (action.type === "saveStorageState") {
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, path6) => {
3984
- if (path6.includes(".") && !path6.includes(":")) {
3985
- const [testId, _varName] = path6.split(".", 2);
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 {{${path6}}} not yet fully implemented`);
4301
+ console.warn(`Cross-test variable interpolation {{${path7}}} not yet fully implemented`);
3988
4302
  return match;
3989
4303
  }
3990
- const result = interpolateVariables(`{{${path6}}}`, currentVariables);
4304
+ const result = interpolateVariables(`{{${path7}}}`, currentVariables);
3991
4305
  return result;
3992
4306
  });
3993
4307
  }
3994
- async function runTestInWorkflow(test, page, context, options, workflowDir, testFilePath, workflowBaseUrl) {
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 resolveUrl2 = (value, baseUrl) => {
3999
- if (!baseUrl) return value;
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
- lastStepEndTs = Date.now();
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
- switch (action.type) {
4051
- case "navigate": {
4052
- const interpolated = interpolate(action.value);
4053
- const baseUrl = test.config?.web?.baseUrl || workflowBaseUrl;
4054
- const target = resolveUrl2(interpolated, baseUrl);
4055
- if (debugMode) {
4056
- console.log(` [DEBUG] Navigate step:`);
4057
- console.log(` [DEBUG] - action.value: ${action.value}`);
4058
- console.log(` [DEBUG] - interpolated: ${interpolated}`);
4059
- console.log(` [DEBUG] - test.config?.web?.baseUrl: ${test.config?.web?.baseUrl ?? "(undefined)"}`);
4060
- console.log(` [DEBUG] - workflowBaseUrl: ${workflowBaseUrl ?? "(undefined)"}`);
4061
- console.log(` [DEBUG] - effective baseUrl: ${baseUrl ?? "(undefined)"}`);
4062
- console.log(` [DEBUG] - target: ${target}`);
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
- if (workflow.config?.appwrite) {
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: workflow.config?.appwrite ? {
4763
+ providerConfig: trackingAppwrite ? {
5171
4764
  provider: "appwrite",
5172
- endpoint: workflow.config.appwrite.endpoint,
5173
- projectId: workflow.config.appwrite.projectId,
5174
- apiKey: workflow.config.appwrite.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 = options.browser ?? workflow.config?.web?.browser ?? "chromium";
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 testSizes = options.testSizes && options.testSizes.length > 0 ? options.testSizes : ["1920x1080"];
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 cliStorageState = typeof options.storageState === "string" ? path5__default.isAbsolute(options.storageState) ? options.storageState : path5__default.resolve(process.cwd(), options.storageState) : options.storageState;
5241
- const storageState = cliStorageState ?? resolveStorageStatePath(
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
- } : void 0
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-VWXGXFIM.js.map
5435
- //# sourceMappingURL=chunk-VWXGXFIM.js.map
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