intellitester 0.5.2 → 0.5.3
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-VWXGXFIM.js → chunk-7UYD5Q7D.js} +226 -1011
- package/dist/chunk-7UYD5Q7D.js.map +1 -0
- package/dist/{chunk-XF2UENRQ.cjs → chunk-TBMJMXJK.cjs} +225 -1009
- package/dist/chunk-TBMJMXJK.cjs.map +1 -0
- package/dist/cli/index.cjs +206 -31
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +188 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +16 -12
- package/dist/index.d.cts +70 -2
- package/dist/index.d.ts +70 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VWXGXFIM.js.map +0 -1
- package/dist/chunk-XF2UENRQ.cjs.map +0 -1
|
@@ -8,13 +8,13 @@ import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
|
|
|
8
8
|
import { createServer } from 'http';
|
|
9
9
|
import * as fs3 from 'fs/promises';
|
|
10
10
|
import fs3__default from 'fs/promises';
|
|
11
|
-
import * as
|
|
12
|
-
import
|
|
11
|
+
import * as path4 from 'path';
|
|
12
|
+
import path4__default from 'path';
|
|
13
13
|
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';
|
|
@@ -441,8 +441,8 @@ var TRACK_DIR = ".intellitester/track";
|
|
|
441
441
|
var ACTIVE_TESTS_FILE = "ACTIVE_TESTS.json";
|
|
442
442
|
var DEFAULT_STALE_MS = 2 * 60 * 60 * 1e3;
|
|
443
443
|
var HEARTBEAT_MS = 15e3;
|
|
444
|
-
var getTrackDir = (cwd, trackDir) => trackDir ?
|
|
445
|
-
var getActiveTestsPath = (cwd, trackDir) =>
|
|
444
|
+
var getTrackDir = (cwd, trackDir) => trackDir ? path4__default.resolve(cwd, trackDir) : path4__default.join(cwd, TRACK_DIR);
|
|
445
|
+
var getActiveTestsPath = (cwd, trackDir) => path4__default.join(getTrackDir(cwd, trackDir), ACTIVE_TESTS_FILE);
|
|
446
446
|
var loadActiveTests = async (cwd, trackDir) => {
|
|
447
447
|
const filePath = getActiveTestsPath(cwd, trackDir);
|
|
448
448
|
try {
|
|
@@ -509,11 +509,11 @@ var cleanupOrphanedTrackFiles = async (cwd, state, trackDir) => {
|
|
|
509
509
|
const dir = getTrackDir(cwd, trackDir);
|
|
510
510
|
try {
|
|
511
511
|
const files = await fs3__default.readdir(dir);
|
|
512
|
-
const activeFiles = new Set(Object.values(state.sessions).map((s) =>
|
|
512
|
+
const activeFiles = new Set(Object.values(state.sessions).map((s) => path4__default.basename(s.trackFile)));
|
|
513
513
|
for (const file of files) {
|
|
514
514
|
if (!file.endsWith(".jsonl")) continue;
|
|
515
515
|
if (activeFiles.has(file)) continue;
|
|
516
|
-
const filePath =
|
|
516
|
+
const filePath = path4__default.join(dir, file);
|
|
517
517
|
try {
|
|
518
518
|
const stat2 = await fs3__default.stat(filePath);
|
|
519
519
|
const staleMs = Number(process.env.INTELLITESTER_STALE_TEST_MS ?? DEFAULT_STALE_MS);
|
|
@@ -560,7 +560,7 @@ async function initFileTracking(options) {
|
|
|
560
560
|
const trackDir = options.trackDir;
|
|
561
561
|
await fs3__default.mkdir(getTrackDir(cwd, trackDir), { recursive: true });
|
|
562
562
|
await pruneStaleTests(cwd, options.cleanupConfig, trackDir);
|
|
563
|
-
const trackFile =
|
|
563
|
+
const trackFile = path4__default.join(getTrackDir(cwd, trackDir), `TEST_SESSION_${options.sessionId}.jsonl`);
|
|
564
564
|
await fs3__default.writeFile(trackFile, "", "utf8");
|
|
565
565
|
const state = await loadActiveTests(cwd, trackDir);
|
|
566
566
|
state.sessions[options.sessionId] = {
|
|
@@ -1080,7 +1080,7 @@ var MIME_BY_EXTENSION = {
|
|
|
1080
1080
|
".webm": "video/webm"
|
|
1081
1081
|
};
|
|
1082
1082
|
function mimeFromName(name) {
|
|
1083
|
-
const ext =
|
|
1083
|
+
const ext = path4__default.extname(name).toLowerCase();
|
|
1084
1084
|
return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
|
|
1085
1085
|
}
|
|
1086
1086
|
function deriveFilenameFromUrl(url) {
|
|
@@ -1100,8 +1100,8 @@ async function downloadUrl(url) {
|
|
|
1100
1100
|
const arrayBuffer = await response.arrayBuffer();
|
|
1101
1101
|
const buffer = Buffer.from(arrayBuffer);
|
|
1102
1102
|
const name = deriveFilenameFromUrl(url);
|
|
1103
|
-
const tempDir = await fs3__default.mkdtemp(
|
|
1104
|
-
const tempPath =
|
|
1103
|
+
const tempDir = await fs3__default.mkdtemp(path4__default.join(os.tmpdir(), "intellitester-upload-"));
|
|
1104
|
+
const tempPath = path4__default.join(tempDir, name);
|
|
1105
1105
|
await fs3__default.writeFile(tempPath, buffer);
|
|
1106
1106
|
return { path: tempPath, tempDir };
|
|
1107
1107
|
}
|
|
@@ -1112,8 +1112,8 @@ async function resolveOne(entry, context) {
|
|
|
1112
1112
|
const downloaded = await downloadUrl(interpolated);
|
|
1113
1113
|
return { kind: "path", path: downloaded.path, tempDir: downloaded.tempDir };
|
|
1114
1114
|
}
|
|
1115
|
-
const absolute =
|
|
1116
|
-
context.testFilePath ?
|
|
1115
|
+
const absolute = path4__default.isAbsolute(interpolated) ? interpolated : path4__default.resolve(
|
|
1116
|
+
context.testFilePath ? path4__default.dirname(context.testFilePath) : process.cwd(),
|
|
1117
1117
|
interpolated
|
|
1118
1118
|
);
|
|
1119
1119
|
try {
|
|
@@ -1156,7 +1156,7 @@ async function toPlaywrightFiles(resolved) {
|
|
|
1156
1156
|
}
|
|
1157
1157
|
const buffer = await fs3__default.readFile(r.path);
|
|
1158
1158
|
return {
|
|
1159
|
-
name:
|
|
1159
|
+
name: path4__default.basename(r.path),
|
|
1160
1160
|
mimeType: mimeFromName(r.path),
|
|
1161
1161
|
buffer
|
|
1162
1162
|
};
|
|
@@ -1635,9 +1635,9 @@ init_esm_shims();
|
|
|
1635
1635
|
var INTELLITESTER_DIR = ".intellitester";
|
|
1636
1636
|
var SERVERS_MARKER_FILE = "servers.json";
|
|
1637
1637
|
var LEGACY_SINGLE_MARKER_FILE = "server.json";
|
|
1638
|
-
var getMarkerDir = (cwd) =>
|
|
1639
|
-
var getMarkerPath = (cwd) =>
|
|
1640
|
-
var getLegacyMarkerPath = (cwd) =>
|
|
1638
|
+
var getMarkerDir = (cwd) => path4.join(cwd, INTELLITESTER_DIR);
|
|
1639
|
+
var getMarkerPath = (cwd) => path4.join(getMarkerDir(cwd), SERVERS_MARKER_FILE);
|
|
1640
|
+
var getLegacyMarkerPath = (cwd) => path4.join(getMarkerDir(cwd), LEGACY_SINGLE_MARKER_FILE);
|
|
1641
1641
|
async function readMarkers(cwd) {
|
|
1642
1642
|
try {
|
|
1643
1643
|
const content = await fs3.readFile(getMarkerPath(cwd), "utf-8");
|
|
@@ -1729,7 +1729,7 @@ async function isServerRunning(url) {
|
|
|
1729
1729
|
}
|
|
1730
1730
|
async function readPackageJson(cwd) {
|
|
1731
1731
|
try {
|
|
1732
|
-
const packagePath =
|
|
1732
|
+
const packagePath = path4.join(cwd, "package.json");
|
|
1733
1733
|
const content = await fs3.readFile(packagePath, "utf-8");
|
|
1734
1734
|
return JSON.parse(content);
|
|
1735
1735
|
} catch {
|
|
@@ -1754,11 +1754,11 @@ function detectFramework(pkg) {
|
|
|
1754
1754
|
return null;
|
|
1755
1755
|
}
|
|
1756
1756
|
async function detectPackageManager(cwd) {
|
|
1757
|
-
const hasDenoLock = await fs3.stat(
|
|
1758
|
-
const hasBunLockb = await fs3.stat(
|
|
1759
|
-
const hasBunLock = await fs3.stat(
|
|
1760
|
-
const hasPnpmLock = await fs3.stat(
|
|
1761
|
-
const hasYarnLock = await fs3.stat(
|
|
1757
|
+
const hasDenoLock = await fs3.stat(path4.join(cwd, "deno.lock")).catch(() => null);
|
|
1758
|
+
const hasBunLockb = await fs3.stat(path4.join(cwd, "bun.lockb")).catch(() => null);
|
|
1759
|
+
const hasBunLock = await fs3.stat(path4.join(cwd, "bun.lock")).catch(() => null);
|
|
1760
|
+
const hasPnpmLock = await fs3.stat(path4.join(cwd, "pnpm-lock.yaml")).catch(() => null);
|
|
1761
|
+
const hasYarnLock = await fs3.stat(path4.join(cwd, "yarn.lock")).catch(() => null);
|
|
1762
1762
|
if (hasDenoLock) return "deno";
|
|
1763
1763
|
if (hasBunLockb || hasBunLock) return "bun";
|
|
1764
1764
|
if (hasPnpmLock) return "pnpm";
|
|
@@ -1782,7 +1782,7 @@ function getDevCommand(pm, script) {
|
|
|
1782
1782
|
async function detectBuildDirectory(cwd) {
|
|
1783
1783
|
const commonDirs = [".next", ".output", ".svelte-kit", "dist", "build", "out"];
|
|
1784
1784
|
for (const dir of commonDirs) {
|
|
1785
|
-
const fullPath =
|
|
1785
|
+
const fullPath = path4.join(cwd, dir);
|
|
1786
1786
|
try {
|
|
1787
1787
|
const stat2 = await fs3.stat(fullPath);
|
|
1788
1788
|
if (stat2.isDirectory()) return dir;
|
|
@@ -2330,10 +2330,10 @@ function safeCallback(label, fn) {
|
|
|
2330
2330
|
console.error(`[intellitester] ${label} callback threw:`, e);
|
|
2331
2331
|
}
|
|
2332
2332
|
}
|
|
2333
|
-
var defaultScreenshotDir =
|
|
2333
|
+
var defaultScreenshotDir = path4__default.join(process.cwd(), "artifacts", "screenshots");
|
|
2334
2334
|
var resolveStorageStatePath = (value, baseDir) => {
|
|
2335
2335
|
if (typeof value !== "string") return value;
|
|
2336
|
-
return
|
|
2336
|
+
return path4__default.isAbsolute(value) ? value : path4__default.resolve(baseDir, value);
|
|
2337
2337
|
};
|
|
2338
2338
|
var interpolateTrackMetadata = (value, variables) => {
|
|
2339
2339
|
if (typeof value === "string") {
|
|
@@ -2531,7 +2531,7 @@ var runScreenshot = async (page, name, screenshotDir, stepIndex, browserName) =>
|
|
|
2531
2531
|
const timing = getBrowserTimingConfig(browserName ?? "chromium");
|
|
2532
2532
|
await waitForPageStable(page, timing.screenshotNetworkIdleTimeout);
|
|
2533
2533
|
const filename = name ?? `step-${stepIndex + 1}.png`;
|
|
2534
|
-
const filePath =
|
|
2534
|
+
const filePath = path4__default.join(screenshotDir, filename);
|
|
2535
2535
|
await page.screenshot({ path: filePath, fullPage: true });
|
|
2536
2536
|
return filePath;
|
|
2537
2537
|
};
|
|
@@ -2551,7 +2551,7 @@ async function handleInteractiveError(page, action, error, screenshotDir, stepIn
|
|
|
2551
2551
|
console.error(` Error: ${error.message}
|
|
2552
2552
|
`);
|
|
2553
2553
|
await ensureScreenshotDir(screenshotDir);
|
|
2554
|
-
const screenshotPath =
|
|
2554
|
+
const screenshotPath = path4__default.join(screenshotDir, `error-step-${stepIndex + 1}.png`);
|
|
2555
2555
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
2556
2556
|
const pageContent = await page.content();
|
|
2557
2557
|
if (aiConfig) {
|
|
@@ -2600,7 +2600,7 @@ async function handleInteractiveError(page, action, error, screenshotDir, stepIn
|
|
|
2600
2600
|
return response.action || "abort";
|
|
2601
2601
|
}
|
|
2602
2602
|
async function executeActionWithRetry(page, action, index, options) {
|
|
2603
|
-
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName, healing, testFilePath } = options;
|
|
2603
|
+
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName, healing, testFilePath, responseLog, stepStartTs } = options;
|
|
2604
2604
|
const extras = {};
|
|
2605
2605
|
const buildTrackPayload = (stepExtras) => {
|
|
2606
2606
|
if (!("track" in action)) return null;
|
|
@@ -3053,15 +3053,7 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3053
3053
|
if (debugMode) {
|
|
3054
3054
|
console.log(`[DEBUG] Executing nested step ${nestedIdx + 1}: ${nestedAction.type}`);
|
|
3055
3055
|
}
|
|
3056
|
-
await executeActionWithRetry(page, nestedAction, index,
|
|
3057
|
-
baseUrl,
|
|
3058
|
-
context,
|
|
3059
|
-
screenshotDir,
|
|
3060
|
-
debugMode,
|
|
3061
|
-
interactive,
|
|
3062
|
-
aiConfig,
|
|
3063
|
-
browserName
|
|
3064
|
-
});
|
|
3056
|
+
await executeActionWithRetry(page, nestedAction, index, options);
|
|
3065
3057
|
}
|
|
3066
3058
|
break;
|
|
3067
3059
|
}
|
|
@@ -3163,20 +3155,12 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3163
3155
|
if (debugMode) {
|
|
3164
3156
|
console.log(`[DEBUG] Executing branch step ${nestedIdx + 1}: ${nestedAction.type}`);
|
|
3165
3157
|
}
|
|
3166
|
-
await executeActionWithRetry(page, nestedAction, index,
|
|
3167
|
-
baseUrl,
|
|
3168
|
-
context,
|
|
3169
|
-
screenshotDir,
|
|
3170
|
-
debugMode,
|
|
3171
|
-
interactive,
|
|
3172
|
-
aiConfig,
|
|
3173
|
-
browserName
|
|
3174
|
-
});
|
|
3158
|
+
await executeActionWithRetry(page, nestedAction, index, options);
|
|
3175
3159
|
}
|
|
3176
3160
|
} else {
|
|
3177
3161
|
const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-2CW6OEXJ.js');
|
|
3178
|
-
const workflowPath =
|
|
3179
|
-
const workflowDir =
|
|
3162
|
+
const workflowPath = path4__default.resolve(process.cwd(), branchToExecute.workflow);
|
|
3163
|
+
const workflowDir = path4__default.dirname(workflowPath);
|
|
3180
3164
|
if (debugMode) {
|
|
3181
3165
|
console.log(`[DEBUG] Executing workflow: ${workflowPath}`);
|
|
3182
3166
|
}
|
|
@@ -3188,7 +3172,7 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3188
3172
|
}
|
|
3189
3173
|
}
|
|
3190
3174
|
for (const testRef of workflow.tests) {
|
|
3191
|
-
const testFilePath2 =
|
|
3175
|
+
const testFilePath2 = path4__default.resolve(workflowDir, testRef.file);
|
|
3192
3176
|
if (debugMode) {
|
|
3193
3177
|
console.log(`[DEBUG] Loading test from workflow: ${testFilePath2}`);
|
|
3194
3178
|
}
|
|
@@ -3200,18 +3184,148 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
3200
3184
|
}
|
|
3201
3185
|
}
|
|
3202
3186
|
for (const [testStepIdx, testAction] of test.steps.entries()) {
|
|
3203
|
-
await executeActionWithRetry(page, testAction, testStepIdx,
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3187
|
+
await executeActionWithRetry(page, testAction, testStepIdx, options);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
break;
|
|
3192
|
+
}
|
|
3193
|
+
case "saveStorageState": {
|
|
3194
|
+
const saveAction = action;
|
|
3195
|
+
if (saveAction.path) {
|
|
3196
|
+
const resolvedPath = interpolateVariables(saveAction.path, context.variables);
|
|
3197
|
+
const baseDir = testFilePath ? path4__default.dirname(testFilePath) : process.cwd();
|
|
3198
|
+
const absPath = path4__default.isAbsolute(resolvedPath) ? resolvedPath : path4__default.resolve(baseDir, resolvedPath);
|
|
3199
|
+
await page.context().storageState({ path: absPath });
|
|
3200
|
+
if (debugMode) {
|
|
3201
|
+
console.log(`[DEBUG] Saved storage state to ${absPath}`);
|
|
3202
|
+
}
|
|
3203
|
+
} else if (saveAction.handler) {
|
|
3204
|
+
const resolvedHandler = interpolateVariables(saveAction.handler, context.variables);
|
|
3205
|
+
const baseDir = testFilePath ? path4__default.dirname(testFilePath) : process.cwd();
|
|
3206
|
+
const absPath = path4__default.isAbsolute(resolvedHandler) ? resolvedHandler : path4__default.resolve(baseDir, resolvedHandler);
|
|
3207
|
+
let loadPath = absPath;
|
|
3208
|
+
if (absPath.endsWith(".ts")) {
|
|
3209
|
+
const jsPath = absPath.replace(/\.ts$/, ".js");
|
|
3210
|
+
try {
|
|
3211
|
+
await fs3__default.access(jsPath);
|
|
3212
|
+
loadPath = jsPath;
|
|
3213
|
+
} catch {
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
const mod = await import(`${loadPath}?t=${Date.now()}`);
|
|
3217
|
+
const fn = mod.default ?? mod;
|
|
3218
|
+
if (typeof fn !== "function") {
|
|
3219
|
+
throw new Error(`saveStorageState handler at ${resolvedHandler} did not export a default function`);
|
|
3220
|
+
}
|
|
3221
|
+
await fn({
|
|
3222
|
+
page,
|
|
3223
|
+
context: page.context(),
|
|
3224
|
+
variables: context.variables
|
|
3225
|
+
});
|
|
3226
|
+
if (debugMode) {
|
|
3227
|
+
console.log(`[DEBUG] Ran custom saveStorageState handler: ${resolvedHandler}`);
|
|
3228
|
+
}
|
|
3229
|
+
} else {
|
|
3230
|
+
throw new Error("saveStorageState requires either `path` or `handler` (schema should have caught this)");
|
|
3231
|
+
}
|
|
3232
|
+
break;
|
|
3233
|
+
}
|
|
3234
|
+
case "assertCookies": {
|
|
3235
|
+
const cookieAction = action;
|
|
3236
|
+
const filterUrl = cookieAction.url ? interpolateVariables(cookieAction.url, context.variables) : void 0;
|
|
3237
|
+
const jar = await page.context().cookies(filterUrl);
|
|
3238
|
+
const names = new Set(jar.map((c) => c.name));
|
|
3239
|
+
const problems = [];
|
|
3240
|
+
if (cookieAction.has) {
|
|
3241
|
+
for (const name of cookieAction.has) {
|
|
3242
|
+
if (!names.has(name)) problems.push(`expected cookie "${name}" to be present`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if (cookieAction.not) {
|
|
3246
|
+
for (const name of cookieAction.not) {
|
|
3247
|
+
if (names.has(name)) problems.push(`expected cookie "${name}" to be absent`);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (cookieAction.match) {
|
|
3251
|
+
for (const [name, pattern] of Object.entries(cookieAction.match)) {
|
|
3252
|
+
const c = jar.find((entry) => entry.name === name);
|
|
3253
|
+
if (!c) {
|
|
3254
|
+
problems.push(`expected cookie "${name}" to be present (for value match)`);
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
const matcher = compileMatcher(interpolateVariables(pattern, context.variables), "substr");
|
|
3258
|
+
if (!matcher(c.value)) {
|
|
3259
|
+
problems.push(`cookie "${name}" value "${c.value}" did not match pattern "${pattern}"`);
|
|
3212
3260
|
}
|
|
3213
3261
|
}
|
|
3214
3262
|
}
|
|
3263
|
+
if (problems.length > 0) {
|
|
3264
|
+
throw new Error(`assertCookies failed:
|
|
3265
|
+
- ${problems.join("\n - ")}
|
|
3266
|
+
(cookies seen: ${[...names].join(", ") || "<none>"})`);
|
|
3267
|
+
}
|
|
3268
|
+
break;
|
|
3269
|
+
}
|
|
3270
|
+
case "expectResponse": {
|
|
3271
|
+
if (!responseLog) {
|
|
3272
|
+
throw new Error("expectResponse requires a responseLog to be attached (executor wiring issue)");
|
|
3273
|
+
}
|
|
3274
|
+
const respAction = action;
|
|
3275
|
+
const urlPattern = interpolateVariables(respAction.url, context.variables);
|
|
3276
|
+
const urlMatch = compileMatcher(urlPattern, "url");
|
|
3277
|
+
const headerMatchers = respAction.headers ? Object.entries(respAction.headers).map(([name, pattern]) => ({
|
|
3278
|
+
name: name.toLowerCase(),
|
|
3279
|
+
test: compileMatcher(interpolateVariables(pattern, context.variables), "substr"),
|
|
3280
|
+
pattern
|
|
3281
|
+
})) : [];
|
|
3282
|
+
const expectedStatus = respAction.status;
|
|
3283
|
+
let sinceTs;
|
|
3284
|
+
if (respAction.since === "testStart") {
|
|
3285
|
+
sinceTs = 0;
|
|
3286
|
+
} else if (typeof respAction.since === "number") {
|
|
3287
|
+
sinceTs = respAction.since;
|
|
3288
|
+
} else {
|
|
3289
|
+
sinceTs = stepStartTs ?? 0;
|
|
3290
|
+
}
|
|
3291
|
+
const timeout = respAction.timeout ?? 5e3;
|
|
3292
|
+
const deadline = Date.now() + timeout;
|
|
3293
|
+
const findMatch = () => {
|
|
3294
|
+
for (const entry of responseLog.snapshot()) {
|
|
3295
|
+
if (entry.ts < sinceTs) continue;
|
|
3296
|
+
if (!urlMatch(entry.url)) continue;
|
|
3297
|
+
if (expectedStatus !== void 0 && entry.status !== expectedStatus) continue;
|
|
3298
|
+
let headersOk = true;
|
|
3299
|
+
for (const h of headerMatchers) {
|
|
3300
|
+
const value = entry.headers[h.name];
|
|
3301
|
+
if (value === void 0 || !h.test(value)) {
|
|
3302
|
+
headersOk = false;
|
|
3303
|
+
break;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (headersOk) return entry;
|
|
3307
|
+
}
|
|
3308
|
+
return null;
|
|
3309
|
+
};
|
|
3310
|
+
let match = findMatch();
|
|
3311
|
+
while (!match && Date.now() < deadline) {
|
|
3312
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3313
|
+
match = findMatch();
|
|
3314
|
+
}
|
|
3315
|
+
if (!match) {
|
|
3316
|
+
const recent = responseLog.snapshot().slice(-5).map((e) => `${e.status} ${e.url}`).join("\n ");
|
|
3317
|
+
throw new Error(
|
|
3318
|
+
`expectResponse timed out after ${timeout}ms.
|
|
3319
|
+
url pattern: ${urlPattern}
|
|
3320
|
+
` + (expectedStatus !== void 0 ? ` expected status: ${expectedStatus}
|
|
3321
|
+
` : "") + (headerMatchers.length > 0 ? ` expected headers: ${headerMatchers.map((h) => `${h.name}=${h.pattern}`).join(", ")}
|
|
3322
|
+
` : "") + ` recent responses:
|
|
3323
|
+
${recent || "<none>"}`
|
|
3324
|
+
);
|
|
3325
|
+
}
|
|
3326
|
+
if (debugMode) {
|
|
3327
|
+
console.log(`[DEBUG] expectResponse matched: ${match.status} ${match.url}`);
|
|
3328
|
+
}
|
|
3215
3329
|
break;
|
|
3216
3330
|
}
|
|
3217
3331
|
default:
|
|
@@ -3663,7 +3777,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3663
3777
|
const timing = getBrowserTimingConfig(browserName ?? "chromium");
|
|
3664
3778
|
await waitForPageStable(page, timing.screenshotNetworkIdleTimeout);
|
|
3665
3779
|
await ensureScreenshotDir(screenshotDir);
|
|
3666
|
-
const evalScreenshotPath =
|
|
3780
|
+
const evalScreenshotPath = path4__default.join(screenshotDir, `evaluate-step-${index + 1}.png`);
|
|
3667
3781
|
const screenshotBuffer = await page.screenshot({
|
|
3668
3782
|
path: evalScreenshotPath,
|
|
3669
3783
|
fullPage: evalAction.fullPage ?? true
|
|
@@ -3699,172 +3813,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3699
3813
|
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
3700
3814
|
continue;
|
|
3701
3815
|
}
|
|
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
|
-
}
|
|
3816
|
+
const stepStartTs = lastStepEndTs;
|
|
3868
3817
|
const actionExtras = await executeActionWithRetry(page, action, index, {
|
|
3869
3818
|
baseUrl: options.baseUrl ?? test.config?.web?.baseUrl,
|
|
3870
3819
|
context: executionContext,
|
|
@@ -3874,7 +3823,9 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3874
3823
|
aiConfig: options.aiConfig,
|
|
3875
3824
|
browserName,
|
|
3876
3825
|
healing: options.healing,
|
|
3877
|
-
testFilePath: options.testFilePath
|
|
3826
|
+
testFilePath: options.testFilePath,
|
|
3827
|
+
responseLog,
|
|
3828
|
+
stepStartTs
|
|
3878
3829
|
});
|
|
3879
3830
|
sizeResults.push({ action, status: "passed", logOutput: actionExtras.logOutput });
|
|
3880
3831
|
safeCallback("onStepComplete", options.onStepComplete ? () => options.onStepComplete(sizeResults[sizeResults.length - 1], index, test.steps.length) : void 0);
|
|
@@ -3951,24 +3902,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
3951
3902
|
|
|
3952
3903
|
// src/executors/web/workflowExecutor.ts
|
|
3953
3904
|
init_esm_shims();
|
|
3954
|
-
var defaultScreenshotDir2 =
|
|
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
|
-
};
|
|
3905
|
+
var defaultScreenshotDir2 = path4__default.join(process.cwd(), "artifacts", "screenshots");
|
|
3972
3906
|
var getBrowser2 = (browser) => {
|
|
3973
3907
|
switch (browser) {
|
|
3974
3908
|
case "firefox":
|
|
@@ -3991,761 +3925,34 @@ function interpolateWorkflowVariables(value, currentVariables, testResults) {
|
|
|
3991
3925
|
return result;
|
|
3992
3926
|
});
|
|
3993
3927
|
}
|
|
3994
|
-
async function runTestInWorkflow(test, page, context, options,
|
|
3928
|
+
async function runTestInWorkflow(test, page, context, options, _workflowDir, testFilePath, workflowBaseUrl) {
|
|
3995
3929
|
const results = [];
|
|
3996
3930
|
const debugMode = options.debug ?? false;
|
|
3997
3931
|
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
|
-
};
|
|
3932
|
+
const browserName = options.browser ?? "chromium";
|
|
3933
|
+
const baseUrl = test.config?.web?.baseUrl || workflowBaseUrl;
|
|
4010
3934
|
const responseLog = new ResponseLog();
|
|
4011
3935
|
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
3936
|
try {
|
|
4044
3937
|
for (const [index, action] of test.steps.entries()) {
|
|
4045
|
-
|
|
3938
|
+
const stepStartTs = Date.now();
|
|
4046
3939
|
if (debugMode) {
|
|
4047
3940
|
console.log(` [DEBUG] Step ${index + 1}: ${action.type}`);
|
|
4048
3941
|
}
|
|
4049
3942
|
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" });
|
|
3943
|
+
const actionExtras = await executeActionWithRetry(page, action, index, {
|
|
3944
|
+
baseUrl,
|
|
3945
|
+
context,
|
|
3946
|
+
screenshotDir,
|
|
3947
|
+
debugMode,
|
|
3948
|
+
interactive: false,
|
|
3949
|
+
aiConfig: options.aiConfig,
|
|
3950
|
+
browserName,
|
|
3951
|
+
testFilePath,
|
|
3952
|
+
responseLog,
|
|
3953
|
+
stepStartTs
|
|
3954
|
+
});
|
|
3955
|
+
results.push({ action, status: "passed", logOutput: actionExtras.logOutput });
|
|
4749
3956
|
} catch (error) {
|
|
4750
3957
|
const message = error instanceof Error ? error.message : String(error);
|
|
4751
3958
|
results.push({ action, status: "failed", error: message });
|
|
@@ -4948,20 +4155,21 @@ function inferCleanupConfig(config) {
|
|
|
4948
4155
|
}
|
|
4949
4156
|
async function runWorkflowWithContext(workflow, workflowFilePath, options) {
|
|
4950
4157
|
const { page, executionContext, skipCleanup = false, sessionId: providedSessionId, testStartTime: providedTestStartTime } = options;
|
|
4951
|
-
const workflowDir =
|
|
4158
|
+
const workflowDir = path4__default.dirname(workflowFilePath);
|
|
4952
4159
|
const sessionId = providedSessionId ?? crypto4.randomUUID();
|
|
4953
4160
|
const testStartTime = providedTestStartTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4954
4161
|
console.log(`
|
|
4955
4162
|
Starting workflow: ${workflow.name}`);
|
|
4956
4163
|
console.log(`Session ID: ${sessionId}
|
|
4957
4164
|
`);
|
|
4958
|
-
|
|
4165
|
+
const effectiveAppwriteConfig = workflow.config?.appwrite ? {
|
|
4166
|
+
endpoint: workflow.config.appwrite.endpoint,
|
|
4167
|
+
projectId: workflow.config.appwrite.projectId,
|
|
4168
|
+
apiKey: workflow.config.appwrite.apiKey
|
|
4169
|
+
} : options.appwriteConfig;
|
|
4170
|
+
if (effectiveAppwriteConfig) {
|
|
4959
4171
|
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
|
-
};
|
|
4172
|
+
executionContext.appwriteConfig = effectiveAppwriteConfig;
|
|
4965
4173
|
}
|
|
4966
4174
|
setupAppwriteTracking(page, executionContext);
|
|
4967
4175
|
}
|
|
@@ -4976,7 +4184,7 @@ Starting workflow: ${workflow.name}`);
|
|
|
4976
4184
|
const testResults = [];
|
|
4977
4185
|
let workflowFailed = false;
|
|
4978
4186
|
for (const [index, testRef] of workflow.tests.entries()) {
|
|
4979
|
-
const testFilePath =
|
|
4187
|
+
const testFilePath = path4__default.resolve(workflowDir, testRef.file);
|
|
4980
4188
|
console.log(`
|
|
4981
4189
|
[${index + 1}/${workflow.tests.length}] Running: ${testRef.file}`);
|
|
4982
4190
|
if (testRef.id) {
|
|
@@ -5143,7 +4351,7 @@ ${"=".repeat(60)}`);
|
|
|
5143
4351
|
};
|
|
5144
4352
|
}
|
|
5145
4353
|
async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
5146
|
-
const workflowDir =
|
|
4354
|
+
const workflowDir = path4__default.dirname(workflowFilePath);
|
|
5147
4355
|
const sessionId = options.sessionId ?? crypto4.randomUUID();
|
|
5148
4356
|
const testStartTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5149
4357
|
const cleanupConfig = inferCleanupConfig(workflow.config);
|
|
@@ -5163,15 +4371,20 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5163
4371
|
process.env.INTELLITESTER_SESSION_ID = sessionId;
|
|
5164
4372
|
process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
|
|
5165
4373
|
}
|
|
4374
|
+
const trackingAppwrite = workflow.config?.appwrite ? {
|
|
4375
|
+
endpoint: workflow.config.appwrite.endpoint,
|
|
4376
|
+
projectId: workflow.config.appwrite.projectId,
|
|
4377
|
+
apiKey: workflow.config.appwrite.apiKey
|
|
4378
|
+
} : options.appwriteConfig;
|
|
5166
4379
|
fileTracking = await initFileTracking({
|
|
5167
4380
|
sessionId,
|
|
5168
4381
|
cleanupConfig,
|
|
5169
4382
|
trackDir: options.trackDir,
|
|
5170
|
-
providerConfig:
|
|
4383
|
+
providerConfig: trackingAppwrite ? {
|
|
5171
4384
|
provider: "appwrite",
|
|
5172
|
-
endpoint:
|
|
5173
|
-
projectId:
|
|
5174
|
-
apiKey:
|
|
4385
|
+
endpoint: trackingAppwrite.endpoint,
|
|
4386
|
+
projectId: trackingAppwrite.projectId,
|
|
4387
|
+
apiKey: trackingAppwrite.apiKey
|
|
5175
4388
|
} : void 0
|
|
5176
4389
|
});
|
|
5177
4390
|
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
@@ -5195,7 +4408,7 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5195
4408
|
}
|
|
5196
4409
|
const normalized = wsEntries.map((entry) => ({
|
|
5197
4410
|
...entry,
|
|
5198
|
-
workdir:
|
|
4411
|
+
workdir: path4__default.resolve(serverCwd, entry.workdir ?? entry.cwd ?? "."),
|
|
5199
4412
|
...shouldForceNoReuse ? { reuseExistingServer: false } : {}
|
|
5200
4413
|
}));
|
|
5201
4414
|
await webServerManager.start(normalized.length === 1 ? normalized[0] : normalized);
|
|
@@ -5219,12 +4432,13 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5219
4432
|
};
|
|
5220
4433
|
process.on("SIGINT", signalCleanup);
|
|
5221
4434
|
process.on("SIGTERM", signalCleanup);
|
|
5222
|
-
const browserName =
|
|
4435
|
+
const browserName = workflow.config?.web?.browser ?? options.browser ?? "chromium";
|
|
5223
4436
|
const headless = options.headed === true ? false : workflow.config?.web?.headless ?? true;
|
|
5224
4437
|
console.log(`Launching ${browserName}${headless ? " (headless)" : " (visible)"}...`);
|
|
5225
4438
|
const browser = await getBrowser2(browserName).launch(getBrowserLaunchOptions({ headless, browser: browserName }));
|
|
5226
4439
|
console.log(`Browser launched successfully`);
|
|
5227
|
-
const
|
|
4440
|
+
const workflowTestSizes = workflow.config?.web?.testSizes;
|
|
4441
|
+
const testSizes = workflowTestSizes && workflowTestSizes.length > 0 ? workflowTestSizes : options.testSizes && options.testSizes.length > 0 ? options.testSizes : ["1920x1080"];
|
|
5228
4442
|
const viewportSizes = [];
|
|
5229
4443
|
for (const size of testSizes) {
|
|
5230
4444
|
const viewport = parseViewportSize(size);
|
|
@@ -5237,11 +4451,12 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5237
4451
|
}
|
|
5238
4452
|
const allTestResults = [];
|
|
5239
4453
|
let anyFailed = false;
|
|
5240
|
-
const
|
|
5241
|
-
const
|
|
4454
|
+
const optionsStorageState = typeof options.storageState === "string" ? path4__default.isAbsolute(options.storageState) ? options.storageState : path4__default.resolve(process.cwd(), options.storageState) : options.storageState;
|
|
4455
|
+
const workflowStorageState = resolveStorageStatePath(
|
|
5242
4456
|
workflow.config?.web?.storageState,
|
|
5243
4457
|
workflowDir
|
|
5244
4458
|
);
|
|
4459
|
+
const storageState = workflowStorageState ?? optionsStorageState;
|
|
5245
4460
|
let browserContext = await browser.newContext({
|
|
5246
4461
|
viewport: viewportSizes[0].viewport,
|
|
5247
4462
|
...storageState ? { storageState } : {}
|
|
@@ -5257,7 +4472,7 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
5257
4472
|
endpoint: workflow.config.appwrite.endpoint,
|
|
5258
4473
|
projectId: workflow.config.appwrite.projectId,
|
|
5259
4474
|
apiKey: workflow.config.appwrite.apiKey
|
|
5260
|
-
} :
|
|
4475
|
+
} : options.appwriteConfig
|
|
5261
4476
|
};
|
|
5262
4477
|
if (workflow.variables) {
|
|
5263
4478
|
for (const [key, value] of Object.entries(workflow.variables)) {
|
|
@@ -5430,6 +4645,6 @@ Collected ${serverResources.length} server-tracked resources`);
|
|
|
5430
4645
|
// src/executors/web/index.ts
|
|
5431
4646
|
init_esm_shims();
|
|
5432
4647
|
|
|
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-
|
|
4648
|
+
export { buildCompletionOptions, buildModel, createTestContext, executeActionWithRetry, generateFillerText, generateRandomEmail, generateRandomPhone, generateRandomPhoto, generateRandomUsername, getBrowserLaunchOptions, initFileTracking, interpolateVariables, mergeFileTrackedResources, parseViewportSize, resolveStorageStatePath, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, webServerManager };
|
|
4649
|
+
//# sourceMappingURL=chunk-7UYD5Q7D.js.map
|
|
4650
|
+
//# sourceMappingURL=chunk-7UYD5Q7D.js.map
|