@_deep4wee/agent-lens 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -31
- package/dist/cli.d.mts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +1169 -957
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1268 -974
- package/dist/cli.mjs.map +1 -1
- package/dist/dsl-BIjVN1M0.d.mts +204 -0
- package/dist/dsl-BIjVN1M0.d.ts +204 -0
- package/dist/index.d.mts +131 -155
- package/dist/index.d.ts +131 -155
- package/dist/index.js +1506 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1479 -3
- package/dist/index.mjs.map +1 -1
- package/dist/plugins/a11y-tree/index.d.mts +11 -0
- package/dist/plugins/a11y-tree/index.d.ts +11 -0
- package/dist/plugins/a11y-tree/index.js +190 -0
- package/dist/plugins/a11y-tree/index.js.map +1 -0
- package/dist/plugins/a11y-tree/index.mjs +155 -0
- package/dist/plugins/a11y-tree/index.mjs.map +1 -0
- package/dist/plugins/desktop-webview2/index.d.mts +14 -0
- package/dist/plugins/desktop-webview2/index.d.ts +14 -0
- package/dist/plugins/desktop-webview2/index.js +258 -0
- package/dist/plugins/desktop-webview2/index.js.map +1 -0
- package/dist/plugins/desktop-webview2/index.mjs +221 -0
- package/dist/plugins/desktop-webview2/index.mjs.map +1 -0
- package/dist/plugins/live-controller/index.d.mts +35 -0
- package/dist/plugins/live-controller/index.d.ts +35 -0
- package/dist/plugins/live-controller/index.js +303 -0
- package/dist/plugins/live-controller/index.js.map +1 -0
- package/dist/plugins/live-controller/index.mjs +261 -0
- package/dist/plugins/live-controller/index.mjs.map +1 -0
- package/dist/plugins/mock-ipc/index.d.mts +30 -0
- package/dist/plugins/mock-ipc/index.d.ts +30 -0
- package/dist/plugins/mock-ipc/index.js +210 -0
- package/dist/plugins/mock-ipc/index.js.map +1 -0
- package/dist/plugins/mock-ipc/index.mjs +181 -0
- package/dist/plugins/mock-ipc/index.mjs.map +1 -0
- package/dist/plugins/visual-diff/index.d.mts +30 -0
- package/dist/plugins/visual-diff/index.d.ts +30 -0
- package/dist/plugins/visual-diff/index.js +163 -0
- package/dist/plugins/visual-diff/index.js.map +1 -0
- package/dist/plugins/visual-diff/index.mjs +127 -0
- package/dist/plugins/visual-diff/index.mjs.map +1 -0
- package/docs/plugins.md +415 -0
- package/package.json +40 -2
- package/skills/agent-lens/SKILL.md +142 -37
- package/skills/agent-lens/examples/06-state-testing-with-mock-ipc.md +13 -12
- package/skills/agent-lens/examples/07-live-controller-interactive-loop.md +108 -0
- package/skills/agent-lens/examples/08-accessibility-semantic-inspection.md +80 -0
- package/skills/agent-lens/examples/09-visual-regression-and-pixel-diffing.md +66 -0
- package/skills/agent-lens/examples/10-authoring-custom-agent-plugins.md +85 -0
- package/skills/agent-lens/references/plugin-development.md +165 -0
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -24,18 +23,102 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
23
|
));
|
|
25
24
|
|
|
26
25
|
// src/app/cli.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
26
|
+
var import_path13 = __toESM(require("path"));
|
|
27
|
+
var import_fs12 = __toESM(require("fs"));
|
|
28
|
+
var import_child_process2 = require("child_process");
|
|
29
|
+
var import_jiti2 = require("jiti");
|
|
30
|
+
|
|
31
|
+
// src/shared/lib/config.ts
|
|
32
|
+
var import_path = __toESM(require("path"));
|
|
33
|
+
var import_fs = __toESM(require("fs"));
|
|
34
|
+
function loadConfig(cwd = process.cwd()) {
|
|
35
|
+
const jsonConfigPath = import_path.default.join(cwd, "agent-lens.json");
|
|
36
|
+
if (import_fs.default.existsSync(jsonConfigPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = import_fs.default.readFileSync(jsonConfigPath, "utf-8");
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const packageJsonPath = import_path.default.join(cwd, "package.json");
|
|
45
|
+
if (import_fs.default.existsSync(packageJsonPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const raw = import_fs.default.readFileSync(packageJsonPath, "utf-8");
|
|
48
|
+
const pkg = JSON.parse(raw);
|
|
49
|
+
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
50
|
+
return pkg.agentLens;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
function detectStartCwd(providedCwd) {
|
|
58
|
+
if (providedCwd) {
|
|
59
|
+
const resolved = import_path.default.resolve(process.cwd(), providedCwd);
|
|
60
|
+
if (import_fs.default.existsSync(resolved)) {
|
|
61
|
+
return providedCwd;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const rootPkgPath = import_path.default.join(process.cwd(), "package.json");
|
|
65
|
+
let rootHasDevScript = false;
|
|
66
|
+
if (import_fs.default.existsSync(rootPkgPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const rootPkg = JSON.parse(import_fs.default.readFileSync(rootPkgPath, "utf-8"));
|
|
69
|
+
rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rootHasDevScript) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
const candidatePkg = import_path.default.join(process.cwd(), candidate, "package.json");
|
|
79
|
+
if (import_fs.default.existsSync(candidatePkg)) {
|
|
80
|
+
return `./${candidate}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function resolveWwwrootDir(customDir) {
|
|
86
|
+
if (customDir) {
|
|
87
|
+
return import_path.default.resolve(process.cwd(), customDir);
|
|
88
|
+
}
|
|
89
|
+
const candidates = [
|
|
90
|
+
"dist",
|
|
91
|
+
"build",
|
|
92
|
+
"out",
|
|
93
|
+
"wwwroot",
|
|
94
|
+
"Frontend/dist",
|
|
95
|
+
"frontend/dist",
|
|
96
|
+
"client/dist"
|
|
97
|
+
];
|
|
98
|
+
for (const c of candidates) {
|
|
99
|
+
const candidatePath = import_path.default.resolve(process.cwd(), c);
|
|
100
|
+
if (import_fs.default.existsSync(candidatePath) && import_fs.default.existsSync(import_path.default.join(candidatePath, "index.html"))) {
|
|
101
|
+
return candidatePath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const c of candidates) {
|
|
105
|
+
const candidatePath = import_path.default.resolve(process.cwd(), c);
|
|
106
|
+
if (import_fs.default.existsSync(candidatePath)) {
|
|
107
|
+
return candidatePath;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return import_path.default.resolve(process.cwd(), "dist");
|
|
111
|
+
}
|
|
29
112
|
|
|
30
113
|
// src/features/runner/runner.ts
|
|
31
|
-
var
|
|
32
|
-
var
|
|
114
|
+
var import_path7 = __toESM(require("path"));
|
|
115
|
+
var import_fs7 = __toESM(require("fs"));
|
|
33
116
|
|
|
34
117
|
// src/shared/api/dsl.ts
|
|
35
118
|
var VIEWPORT_PRESETS = {
|
|
36
|
-
/** Minimum supported window size
|
|
119
|
+
/** Minimum supported window size */
|
|
37
120
|
MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
|
|
38
|
-
/** Standard default window size
|
|
121
|
+
/** Standard default window size */
|
|
39
122
|
DEFAULT: { name: "default", width: 1200, height: 800 },
|
|
40
123
|
/** Wide screen for checking grids and tables */
|
|
41
124
|
WIDE: { name: "wide", width: 1600, height: 900 },
|
|
@@ -47,16 +130,16 @@ function defineVisualTest(scenario) {
|
|
|
47
130
|
}
|
|
48
131
|
|
|
49
132
|
// src/features/capture/capture.ts
|
|
50
|
-
var
|
|
51
|
-
var
|
|
133
|
+
var import_fs2 = __toESM(require("fs"));
|
|
134
|
+
var import_path2 = __toESM(require("path"));
|
|
52
135
|
var CaptureEngine = class {
|
|
53
136
|
outputDir;
|
|
54
137
|
currentStepIndex = 0;
|
|
55
138
|
recordedSnapshots = [];
|
|
56
139
|
constructor(outputDir) {
|
|
57
140
|
this.outputDir = outputDir;
|
|
58
|
-
if (!
|
|
59
|
-
|
|
141
|
+
if (!import_fs2.default.existsSync(this.outputDir)) {
|
|
142
|
+
import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
|
|
60
143
|
}
|
|
61
144
|
}
|
|
62
145
|
getSnapshots() {
|
|
@@ -67,17 +150,17 @@ var CaptureEngine = class {
|
|
|
67
150
|
}
|
|
68
151
|
/**
|
|
69
152
|
*/
|
|
70
|
-
async takeSnapshot(page, name, viewport,
|
|
153
|
+
async takeSnapshot(page, name, viewport, options) {
|
|
71
154
|
this.currentStepIndex += 1;
|
|
72
155
|
const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
|
|
73
156
|
const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
74
157
|
const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
|
|
75
|
-
const filePath =
|
|
76
|
-
if (
|
|
77
|
-
const element = await page.waitForSelector(
|
|
158
|
+
const filePath = import_path2.default.join(this.outputDir, fileName);
|
|
159
|
+
if (options?.selector) {
|
|
160
|
+
const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
78
161
|
await element.screenshot({ path: filePath });
|
|
79
162
|
} else {
|
|
80
|
-
await page.screenshot({ path: filePath, fullPage:
|
|
163
|
+
await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
|
|
81
164
|
}
|
|
82
165
|
const metadata = {
|
|
83
166
|
index: this.currentStepIndex,
|
|
@@ -87,20 +170,20 @@ var CaptureEngine = class {
|
|
|
87
170
|
relativeUri: `./${fileName}`,
|
|
88
171
|
viewport,
|
|
89
172
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
-
selector:
|
|
173
|
+
selector: options?.selector
|
|
91
174
|
};
|
|
92
175
|
this.recordedSnapshots.push(metadata);
|
|
93
176
|
return metadata;
|
|
94
177
|
}
|
|
95
178
|
/**
|
|
96
179
|
*/
|
|
97
|
-
async takeBurst(page, name, viewport,
|
|
180
|
+
async takeBurst(page, name, viewport, options) {
|
|
98
181
|
this.currentStepIndex += 1;
|
|
99
182
|
const stepIndex = this.currentStepIndex;
|
|
100
183
|
const paddedIndex = String(stepIndex).padStart(2, "0");
|
|
101
184
|
const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
102
|
-
const duration = Math.max(
|
|
103
|
-
const interval = Math.max(
|
|
185
|
+
const duration = Math.max(options.durationMs, 50);
|
|
186
|
+
const interval = Math.max(options.intervalMs ?? 80, 20);
|
|
104
187
|
const totalFrames = Math.ceil(duration / interval);
|
|
105
188
|
const burstSnapshots = [];
|
|
106
189
|
const burstGroup = sanitizedName;
|
|
@@ -108,9 +191,9 @@ var CaptureEngine = class {
|
|
|
108
191
|
const elapsedMs = frame * interval;
|
|
109
192
|
const paddedFrame = String(frame + 1).padStart(2, "0");
|
|
110
193
|
const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
|
|
111
|
-
const filePath =
|
|
112
|
-
if (
|
|
113
|
-
const element = await page.$(
|
|
194
|
+
const filePath = import_path2.default.join(this.outputDir, fileName);
|
|
195
|
+
if (options.selector) {
|
|
196
|
+
const element = await page.$(options.selector);
|
|
114
197
|
if (element) {
|
|
115
198
|
await element.screenshot({ path: filePath });
|
|
116
199
|
} else {
|
|
@@ -130,7 +213,7 @@ var CaptureEngine = class {
|
|
|
130
213
|
isBurstFrame: true,
|
|
131
214
|
burstGroup,
|
|
132
215
|
frameIndex: frame + 1,
|
|
133
|
-
selector:
|
|
216
|
+
selector: options.selector
|
|
134
217
|
};
|
|
135
218
|
burstSnapshots.push(meta);
|
|
136
219
|
this.recordedSnapshots.push(meta);
|
|
@@ -143,8 +226,8 @@ var CaptureEngine = class {
|
|
|
143
226
|
};
|
|
144
227
|
|
|
145
228
|
// src/features/reporter/reporter.ts
|
|
146
|
-
var
|
|
147
|
-
var
|
|
229
|
+
var import_fs3 = __toESM(require("fs"));
|
|
230
|
+
var import_path3 = __toESM(require("path"));
|
|
148
231
|
var VisualReporter = class {
|
|
149
232
|
static generateReport(data) {
|
|
150
233
|
const {
|
|
@@ -156,8 +239,8 @@ var VisualReporter = class {
|
|
|
156
239
|
targetMode,
|
|
157
240
|
durationMs
|
|
158
241
|
} = data;
|
|
159
|
-
const manifestPath =
|
|
160
|
-
const reportPath =
|
|
242
|
+
const manifestPath = import_path3.default.join(outputDir, "manifest.json");
|
|
243
|
+
const reportPath = import_path3.default.join(outputDir, "report.md");
|
|
161
244
|
const manifest = {
|
|
162
245
|
scenarioId: scenario.id,
|
|
163
246
|
title: scenario.title,
|
|
@@ -172,7 +255,7 @@ var VisualReporter = class {
|
|
|
172
255
|
errors: consoleErrors,
|
|
173
256
|
warnings: consoleWarnings
|
|
174
257
|
};
|
|
175
|
-
|
|
258
|
+
import_fs3.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
176
259
|
const rows = [];
|
|
177
260
|
const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
|
|
178
261
|
const burstGroups = /* @__PURE__ */ new Map();
|
|
@@ -182,9 +265,9 @@ var VisualReporter = class {
|
|
|
182
265
|
burstGroups.set(s.burstGroup, list);
|
|
183
266
|
});
|
|
184
267
|
for (const snap of regularSnapshots) {
|
|
185
|
-
const
|
|
268
|
+
const relativeLink = snap.relativeUri || `./${snap.fileName}`;
|
|
186
269
|
rows.push(
|
|
187
|
-
`| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${
|
|
270
|
+
`| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${relativeLink}) |`
|
|
188
271
|
);
|
|
189
272
|
}
|
|
190
273
|
let burstSections = "";
|
|
@@ -201,8 +284,8 @@ var VisualReporter = class {
|
|
|
201
284
|
| :--- | :--- | :--- | :--- |
|
|
202
285
|
`;
|
|
203
286
|
for (const frame of frames) {
|
|
204
|
-
const
|
|
205
|
-
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${
|
|
287
|
+
const relativeLink = frame.relativeUri || `./${frame.fileName}`;
|
|
288
|
+
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${relativeLink}) |  |
|
|
206
289
|
`;
|
|
207
290
|
}
|
|
208
291
|
burstSections += `
|
|
@@ -265,6 +348,17 @@ ${err.stack}
|
|
|
265
348
|
if (consoleWarnings.length > 20) {
|
|
266
349
|
consoleSections += `
|
|
267
350
|
*...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
let pluginSections = "";
|
|
355
|
+
if (data.customSections && data.customSections.length > 0) {
|
|
356
|
+
for (const sec of data.customSections) {
|
|
357
|
+
pluginSections += `
|
|
358
|
+
## ${sec.title}
|
|
359
|
+
|
|
360
|
+
${sec.content}
|
|
361
|
+
|
|
268
362
|
`;
|
|
269
363
|
}
|
|
270
364
|
}
|
|
@@ -287,18 +381,21 @@ ${rows.join("\n")}
|
|
|
287
381
|
|
|
288
382
|
${burstSections}
|
|
289
383
|
${consoleSections}
|
|
384
|
+
${pluginSections}
|
|
290
385
|
---
|
|
291
386
|
|
|
292
387
|
## \u{1F4CB} AI Agent Verification Checklist:
|
|
293
388
|
- [ ] **Console Errors**: ${consoleErrors.length === 0 ? "\u2705 No errors found" : `\u274C ${consoleErrors.length} errors \u2014 MUST REVIEW`}
|
|
389
|
+
- [ ] **Visual Layout Check**: Inspect snapshots (e.g. view_image on \`./01_${regularSnapshots[0]?.fileName || "quick_snap"}\`) for layout shifts or clipped elements.
|
|
294
390
|
- [ ] **Responsiveness at \`1024x768\`**: Elements do not overflow the screen, no unwanted horizontal scroll.
|
|
391
|
+
|
|
295
392
|
- [ ] **Typography & Spacing**: Spacing matches the design system and layout grids.
|
|
296
393
|
- [ ] **Color Palette & Theme**: Background tints and button accent colors match the concept.
|
|
297
394
|
- [ ] **Component States**: Modals open centered, dropdowns do not overlap with other layers (z-index).
|
|
298
395
|
- [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
|
|
299
396
|
|
|
300
397
|
`;
|
|
301
|
-
|
|
398
|
+
import_fs3.default.writeFileSync(reportPath, reportContent, "utf-8");
|
|
302
399
|
return reportPath;
|
|
303
400
|
}
|
|
304
401
|
static getHealthStatus(errors, warnings) {
|
|
@@ -316,8 +413,6 @@ ${consoleSections}
|
|
|
316
413
|
var ConsoleTracker = class {
|
|
317
414
|
entries = [];
|
|
318
415
|
attached = false;
|
|
319
|
-
/**
|
|
320
|
-
*/
|
|
321
416
|
attach(page) {
|
|
322
417
|
if (this.attached) return;
|
|
323
418
|
this.attached = true;
|
|
@@ -386,8 +481,6 @@ var ConsoleTracker = class {
|
|
|
386
481
|
return "log";
|
|
387
482
|
}
|
|
388
483
|
}
|
|
389
|
-
/**
|
|
390
|
-
*/
|
|
391
484
|
isIgnoredWarning(text) {
|
|
392
485
|
const ignoredPatterns = [
|
|
393
486
|
"findDOMNode is deprecated",
|
|
@@ -404,368 +497,32 @@ var ConsoleTracker = class {
|
|
|
404
497
|
}
|
|
405
498
|
};
|
|
406
499
|
|
|
407
|
-
// src/shared/drivers/
|
|
408
|
-
var import_child_process = require("child_process");
|
|
500
|
+
// src/shared/drivers/previewDriver.ts
|
|
409
501
|
var import_http = __toESM(require("http"));
|
|
410
|
-
var
|
|
411
|
-
var
|
|
502
|
+
var import_fs4 = __toESM(require("fs"));
|
|
503
|
+
var import_path4 = __toESM(require("path"));
|
|
412
504
|
|
|
413
505
|
// src/shared/lib/playwrightLoader.ts
|
|
414
506
|
var import_playwright = require("playwright");
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
executablePath;
|
|
420
|
-
autoLaunch;
|
|
421
|
-
args;
|
|
422
|
-
env;
|
|
423
|
-
cwd;
|
|
424
|
-
childProcess = null;
|
|
425
|
-
browser = null;
|
|
426
|
-
context = null;
|
|
427
|
-
page = null;
|
|
428
|
-
processStderr = "";
|
|
429
|
-
processExited = false;
|
|
430
|
-
exitCode = null;
|
|
431
|
-
constructor(options2) {
|
|
432
|
-
this.port = options2?.port || 9222;
|
|
433
|
-
this.autoLaunch = options2?.autoLaunch ?? true;
|
|
434
|
-
this.executablePath = options2?.executablePath;
|
|
435
|
-
this.args = options2?.args || [];
|
|
436
|
-
this.env = options2?.env || {};
|
|
437
|
-
this.cwd = options2?.cwd;
|
|
438
|
-
}
|
|
439
|
-
async isPortAvailable() {
|
|
440
|
-
return new Promise((resolve) => {
|
|
441
|
-
const req = import_http.default.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
|
|
442
|
-
resolve(res.statusCode === 200);
|
|
443
|
-
});
|
|
444
|
-
req.on("error", () => resolve(false));
|
|
445
|
-
req.setTimeout(800, () => {
|
|
446
|
-
req.destroy();
|
|
447
|
-
resolve(false);
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
async waitForPort(timeoutMs = 25e3) {
|
|
452
|
-
const startTime = Date.now();
|
|
453
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
454
|
-
if (this.processExited) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
`[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
|
|
457
|
-
Stderr: ${this.processStderr.trim() || "(none)"}`
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
if (await this.isPortAvailable()) {
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
464
|
-
}
|
|
465
|
-
throw new Error(
|
|
466
|
-
`Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
|
|
467
|
-
${this.processStderr.trim() || "(no stderr output)"}`
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
471
|
-
const alreadyRunning = await this.isPortAvailable();
|
|
472
|
-
if (!alreadyRunning) {
|
|
473
|
-
if (!this.autoLaunch) {
|
|
474
|
-
throw new Error(
|
|
475
|
-
`App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
if (!this.executablePath) {
|
|
479
|
-
throw new Error(
|
|
480
|
-
`[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
const resolvedExe = import_path3.default.resolve(process.cwd(), this.executablePath);
|
|
484
|
-
if (!import_fs3.default.existsSync(resolvedExe)) {
|
|
485
|
-
throw new Error(
|
|
486
|
-
`[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
|
|
490
|
-
const mergedEnv = {
|
|
491
|
-
...process.env,
|
|
492
|
-
...this.env,
|
|
493
|
-
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
|
|
494
|
-
};
|
|
495
|
-
this.processStderr = "";
|
|
496
|
-
this.processExited = false;
|
|
497
|
-
this.exitCode = null;
|
|
498
|
-
this.childProcess = (0, import_child_process.spawn)(resolvedExe, this.args, {
|
|
499
|
-
env: mergedEnv,
|
|
500
|
-
cwd: this.cwd || import_path3.default.dirname(resolvedExe),
|
|
501
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
502
|
-
detached: false
|
|
503
|
-
});
|
|
504
|
-
this.childProcess.stderr?.on("data", (chunk) => {
|
|
505
|
-
this.processStderr += chunk.toString();
|
|
506
|
-
});
|
|
507
|
-
this.childProcess.on("exit", (code) => {
|
|
508
|
-
this.processExited = true;
|
|
509
|
-
this.exitCode = code;
|
|
510
|
-
});
|
|
511
|
-
this.childProcess.on("error", (err) => {
|
|
512
|
-
console.error("[DesktopDriver] Failed to spawn process:", err);
|
|
513
|
-
});
|
|
514
|
-
console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
|
|
515
|
-
await this.waitForPort();
|
|
516
|
-
} else {
|
|
517
|
-
console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
|
|
518
|
-
}
|
|
519
|
-
console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
|
|
520
|
-
this.browser = await import_playwright.chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
|
|
521
|
-
const contexts = this.browser.contexts();
|
|
522
|
-
this.context = contexts[0] || await this.browser.newContext();
|
|
523
|
-
const pages = this.context.pages();
|
|
524
|
-
if (pages.length > 0) {
|
|
525
|
-
this.page = pages[0];
|
|
526
|
-
} else {
|
|
527
|
-
this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
|
|
528
|
-
}
|
|
529
|
-
if (initialViewport) {
|
|
530
|
-
await this.page.setViewportSize(initialViewport).catch(() => {
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
return { page: this.page, context: this.context, browser: this.browser };
|
|
534
|
-
}
|
|
535
|
-
async stop() {
|
|
536
|
-
if (this.browser) {
|
|
537
|
-
await this.browser.close().catch(() => {
|
|
538
|
-
});
|
|
539
|
-
this.browser = null;
|
|
540
|
-
}
|
|
541
|
-
if (this.childProcess && !this.childProcess.killed) {
|
|
542
|
-
console.log("[DesktopDriver] Terminating spawned desktop process...");
|
|
543
|
-
try {
|
|
544
|
-
if (process.platform === "win32" && this.childProcess.pid) {
|
|
545
|
-
(0, import_child_process.spawn)("taskkill", ["/pid", String(this.childProcess.pid), "/T", "/F"], { stdio: "ignore" });
|
|
546
|
-
} else {
|
|
547
|
-
this.childProcess.kill("SIGTERM");
|
|
548
|
-
}
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
this.childProcess = null;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// src/shared/drivers/previewDriver.ts
|
|
557
|
-
var import_http2 = __toESM(require("http"));
|
|
558
|
-
var import_fs5 = __toESM(require("fs"));
|
|
559
|
-
var import_path5 = __toESM(require("path"));
|
|
560
|
-
|
|
561
|
-
// src/features/mock-ipc/mockIpc.ts
|
|
562
|
-
var MockIpcRegistry = class {
|
|
563
|
-
mocks = /* @__PURE__ */ new Map();
|
|
564
|
-
set(action, data, options2) {
|
|
565
|
-
this.mocks.set(action, {
|
|
566
|
-
action,
|
|
567
|
-
data,
|
|
568
|
-
type: options2?.type ?? "SUCCESS",
|
|
569
|
-
delayMs: options2?.delayMs ?? 20
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
setBatch(entries) {
|
|
573
|
-
for (const entry of entries) {
|
|
574
|
-
this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
remove(action) {
|
|
578
|
-
this.mocks.delete(action);
|
|
579
|
-
}
|
|
580
|
-
clear() {
|
|
581
|
-
this.mocks.clear();
|
|
582
|
-
}
|
|
583
|
-
get(action) {
|
|
584
|
-
return this.mocks.get(action) ?? null;
|
|
585
|
-
}
|
|
586
|
-
toSerializable() {
|
|
587
|
-
const result = {};
|
|
588
|
-
for (const [action, entry] of this.mocks.entries()) {
|
|
589
|
-
result[action] = {
|
|
590
|
-
data: entry.data,
|
|
591
|
-
type: entry.type ?? "SUCCESS",
|
|
592
|
-
delayMs: entry.delayMs ?? 20
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
return result;
|
|
596
|
-
}
|
|
597
|
-
get size() {
|
|
598
|
-
return this.mocks.size;
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
function generateMockIpcScript(registry) {
|
|
602
|
-
const mocksJson = JSON.stringify(registry.toSerializable());
|
|
603
|
-
return `
|
|
604
|
-
(() => {
|
|
605
|
-
const __mockTable = ${mocksJson};
|
|
606
|
-
|
|
607
|
-
window.__visualRunnerMocks = __mockTable;
|
|
608
|
-
|
|
609
|
-
// Generic IPC mock bridge for modern web applications
|
|
610
|
-
window.__mockIpc = {
|
|
611
|
-
invoke: (action, payload) => {
|
|
612
|
-
return new Promise((resolve, reject) => {
|
|
613
|
-
const mock = window.__visualRunnerMocks[action];
|
|
614
|
-
|
|
615
|
-
if (mock) {
|
|
616
|
-
setTimeout(() => {
|
|
617
|
-
if (mock.type === 'ERROR') {
|
|
618
|
-
reject(new Error(mock.data));
|
|
619
|
-
} else {
|
|
620
|
-
resolve(mock.data);
|
|
621
|
-
}
|
|
622
|
-
}, mock.delayMs || 20);
|
|
623
|
-
} else {
|
|
624
|
-
console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
|
|
625
|
-
setTimeout(() => resolve(null), 20);
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
// Legacy fallback for generic window.external
|
|
632
|
-
if (!window.external) {
|
|
633
|
-
window.external = {};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
window.external.sendMessage = (msg) => {
|
|
507
|
+
var chromium = new Proxy(import_playwright.chromium, {
|
|
508
|
+
get(target, prop, receiver) {
|
|
509
|
+
if (prop === "launch") {
|
|
510
|
+
return async (...args) => {
|
|
637
511
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (mock) {
|
|
645
|
-
const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
|
|
646
|
-
setTimeout(() => {
|
|
647
|
-
const cb = window.__mockCallback;
|
|
648
|
-
if (cb) cb(JSON.stringify(response));
|
|
649
|
-
}, mock.delayMs || 20);
|
|
650
|
-
} else {
|
|
651
|
-
setTimeout(() => {
|
|
652
|
-
const cb = window.__mockCallback;
|
|
653
|
-
if (cb) cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
|
|
654
|
-
}, 20);
|
|
512
|
+
return await target.launch(...args);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
|
|
515
|
+
console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
|
|
516
|
+
console.error("\u{1F449} Please install it by running: npx playwright install chromium\n");
|
|
655
517
|
}
|
|
656
|
-
|
|
657
|
-
console.error('[Mock IPC] Failed to process legacy message:', e);
|
|
518
|
+
throw err;
|
|
658
519
|
}
|
|
659
520
|
};
|
|
660
|
-
|
|
661
|
-
window.external.receiveMessage = (callback) => {
|
|
662
|
-
window.__mockCallback = callback;
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys(window.__visualRunnerMocks).length, 'mocked actions');
|
|
666
|
-
})();
|
|
667
|
-
`;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/shared/lib/config.ts
|
|
671
|
-
var import_fs4 = __toESM(require("fs"));
|
|
672
|
-
var import_path4 = __toESM(require("path"));
|
|
673
|
-
function loadConfig(cwd = process.cwd()) {
|
|
674
|
-
const configPath = import_path4.default.join(cwd, "agent-lens.json");
|
|
675
|
-
if (import_fs4.default.existsSync(configPath)) {
|
|
676
|
-
try {
|
|
677
|
-
const raw = import_fs4.default.readFileSync(configPath, "utf8");
|
|
678
|
-
return JSON.parse(raw);
|
|
679
|
-
} catch (e) {
|
|
680
|
-
console.warn(`\u26A0\uFE0F Warning: Failed to parse agent-lens.json:`, e);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
const pkgPath = import_path4.default.join(cwd, "package.json");
|
|
684
|
-
if (import_fs4.default.existsSync(pkgPath)) {
|
|
685
|
-
try {
|
|
686
|
-
const raw = import_fs4.default.readFileSync(pkgPath, "utf8");
|
|
687
|
-
const pkg = JSON.parse(raw);
|
|
688
|
-
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
689
|
-
return pkg.agentLens;
|
|
690
|
-
}
|
|
691
|
-
} catch {
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return {};
|
|
695
|
-
}
|
|
696
|
-
function resolveWwwrootDir(customPath, cwd = process.cwd()) {
|
|
697
|
-
if (customPath) {
|
|
698
|
-
return import_path4.default.resolve(cwd, customPath);
|
|
699
|
-
}
|
|
700
|
-
const standardDirs = [
|
|
701
|
-
"dist",
|
|
702
|
-
"build",
|
|
703
|
-
"wwwroot",
|
|
704
|
-
"Frontend/dist",
|
|
705
|
-
"frontend/dist",
|
|
706
|
-
"client/dist",
|
|
707
|
-
"web/dist",
|
|
708
|
-
"ui/dist"
|
|
709
|
-
];
|
|
710
|
-
for (const rel of standardDirs) {
|
|
711
|
-
const candidate = import_path4.default.resolve(cwd, rel);
|
|
712
|
-
if (import_fs4.default.existsSync(candidate) && import_fs4.default.existsSync(import_path4.default.join(candidate, "index.html"))) {
|
|
713
|
-
return candidate;
|
|
714
521
|
}
|
|
522
|
+
const val = Reflect.get(target, prop, receiver);
|
|
523
|
+
return typeof val === "function" ? val.bind(target) : val;
|
|
715
524
|
}
|
|
716
|
-
|
|
717
|
-
if (foundDeepWwwroot) {
|
|
718
|
-
return foundDeepWwwroot;
|
|
719
|
-
}
|
|
720
|
-
for (const rel of standardDirs) {
|
|
721
|
-
const candidate = import_path4.default.resolve(cwd, rel);
|
|
722
|
-
if (import_fs4.default.existsSync(candidate)) {
|
|
723
|
-
return candidate;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return import_path4.default.resolve(cwd, "dist");
|
|
727
|
-
}
|
|
728
|
-
function detectStartCwd(customCwd, rootCwd = process.cwd()) {
|
|
729
|
-
if (customCwd) {
|
|
730
|
-
return import_path4.default.resolve(rootCwd, customCwd);
|
|
731
|
-
}
|
|
732
|
-
const subdirectories = ["Frontend", "frontend", "client", "web", "ui", "app"];
|
|
733
|
-
for (const sub of subdirectories) {
|
|
734
|
-
const subPkg = import_path4.default.join(rootCwd, sub, "package.json");
|
|
735
|
-
if (import_fs4.default.existsSync(subPkg)) {
|
|
736
|
-
try {
|
|
737
|
-
const json = JSON.parse(import_fs4.default.readFileSync(subPkg, "utf8"));
|
|
738
|
-
if (json.scripts && (json.scripts.dev || json.scripts.start || json.scripts.build)) {
|
|
739
|
-
return import_path4.default.join(rootCwd, sub);
|
|
740
|
-
}
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return rootCwd;
|
|
746
|
-
}
|
|
747
|
-
function findDeepIndexHtmlDir(dir, maxDepth, currentDepth = 0) {
|
|
748
|
-
if (currentDepth > maxDepth || !import_fs4.default.existsSync(dir)) return null;
|
|
749
|
-
try {
|
|
750
|
-
const entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
751
|
-
for (const entry of entries) {
|
|
752
|
-
if (entry.isDirectory()) {
|
|
753
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "artifacts") {
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
const subDir = import_path4.default.join(dir, entry.name);
|
|
757
|
-
if (entry.name === "wwwroot" && import_fs4.default.existsSync(import_path4.default.join(subDir, "index.html"))) {
|
|
758
|
-
return subDir;
|
|
759
|
-
}
|
|
760
|
-
const found = findDeepIndexHtmlDir(subDir, maxDepth, currentDepth + 1);
|
|
761
|
-
if (found) return found;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
} catch {
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
return null;
|
|
768
|
-
}
|
|
525
|
+
});
|
|
769
526
|
|
|
770
527
|
// src/shared/drivers/previewDriver.ts
|
|
771
528
|
var MIME_TYPES = {
|
|
@@ -791,13 +548,12 @@ var PreviewDriver = class {
|
|
|
791
548
|
serverPort = 0;
|
|
792
549
|
options;
|
|
793
550
|
_baseUrl = "";
|
|
794
|
-
|
|
795
|
-
constructor(
|
|
796
|
-
this.options =
|
|
551
|
+
initialRouteMocks = [];
|
|
552
|
+
constructor(options) {
|
|
553
|
+
this.options = options || {};
|
|
797
554
|
if (!this.options.url) {
|
|
798
555
|
this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
|
|
799
556
|
}
|
|
800
|
-
this.mockRegistry = new MockIpcRegistry();
|
|
801
557
|
}
|
|
802
558
|
get baseUrl() {
|
|
803
559
|
return this._baseUrl;
|
|
@@ -805,25 +561,25 @@ var PreviewDriver = class {
|
|
|
805
561
|
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
806
562
|
let targetUrl = this.options.url;
|
|
807
563
|
if (!targetUrl) {
|
|
808
|
-
if (!
|
|
564
|
+
if (!import_fs4.default.existsSync(this.options.wwwrootDir)) {
|
|
809
565
|
throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
|
|
810
566
|
}
|
|
811
567
|
await new Promise((resolve, reject) => {
|
|
812
|
-
this.server =
|
|
568
|
+
this.server = import_http.default.createServer((req, res) => {
|
|
813
569
|
let reqUrl = req.url?.split("?")[0] || "/";
|
|
814
570
|
if (reqUrl === "/") reqUrl = "/index.html";
|
|
815
|
-
let safePath =
|
|
571
|
+
let safePath = import_path4.default.normalize(import_path4.default.join(this.options.wwwrootDir, reqUrl));
|
|
816
572
|
if (!safePath.startsWith(this.options.wwwrootDir)) {
|
|
817
573
|
res.writeHead(403);
|
|
818
574
|
return res.end("Forbidden");
|
|
819
575
|
}
|
|
820
|
-
if (!
|
|
821
|
-
safePath =
|
|
576
|
+
if (!import_fs4.default.existsSync(safePath) || import_fs4.default.statSync(safePath).isDirectory()) {
|
|
577
|
+
safePath = import_path4.default.join(this.options.wwwrootDir, "index.html");
|
|
822
578
|
}
|
|
823
|
-
const ext =
|
|
579
|
+
const ext = import_path4.default.extname(safePath).toLowerCase();
|
|
824
580
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
825
581
|
try {
|
|
826
|
-
const content =
|
|
582
|
+
const content = import_fs4.default.readFileSync(safePath);
|
|
827
583
|
res.writeHead(200, { "Content-Type": contentType });
|
|
828
584
|
res.end(content);
|
|
829
585
|
} catch (e) {
|
|
@@ -847,7 +603,7 @@ var PreviewDriver = class {
|
|
|
847
603
|
this._baseUrl = targetUrl;
|
|
848
604
|
console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
|
|
849
605
|
}
|
|
850
|
-
this.browser = await
|
|
606
|
+
this.browser = await chromium.launch({
|
|
851
607
|
headless: !this.options.headed,
|
|
852
608
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
853
609
|
});
|
|
@@ -855,35 +611,41 @@ var PreviewDriver = class {
|
|
|
855
611
|
viewport: initialViewport,
|
|
856
612
|
deviceScaleFactor: 1
|
|
857
613
|
});
|
|
858
|
-
if (this.mockRegistry.size > 0) {
|
|
859
|
-
const mockScript = generateMockIpcScript(this.mockRegistry);
|
|
860
|
-
await this.context.addInitScript(mockScript);
|
|
861
|
-
}
|
|
862
614
|
this.page = await this.context.newPage();
|
|
615
|
+
if (this.initialRouteMocks.length > 0) {
|
|
616
|
+
for (const entry of this.initialRouteMocks) {
|
|
617
|
+
await this.addRouteMock(entry);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
863
620
|
await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
|
|
864
621
|
return { page: this.page, context: this.context, browser: this.browser };
|
|
865
622
|
}
|
|
866
|
-
async
|
|
623
|
+
async addRouteMock(entry) {
|
|
867
624
|
if (!this.page) {
|
|
868
|
-
|
|
625
|
+
this.initialRouteMocks.push(entry);
|
|
626
|
+
return;
|
|
869
627
|
}
|
|
870
|
-
this.
|
|
871
|
-
|
|
872
|
-
(
|
|
873
|
-
|
|
874
|
-
window.__visualRunnerMocks = {};
|
|
875
|
-
}
|
|
876
|
-
window.__visualRunnerMocks[action2] = mock;
|
|
877
|
-
},
|
|
878
|
-
{
|
|
879
|
-
action,
|
|
880
|
-
mock: {
|
|
881
|
-
data,
|
|
882
|
-
type: options2?.type ?? "SUCCESS",
|
|
883
|
-
delayMs: options2?.delayMs ?? 20
|
|
884
|
-
}
|
|
628
|
+
await this.page.route(entry.url, async (route) => {
|
|
629
|
+
const req = route.request();
|
|
630
|
+
if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
|
|
631
|
+
return route.continue();
|
|
885
632
|
}
|
|
886
|
-
|
|
633
|
+
if (entry.delayMs) {
|
|
634
|
+
await new Promise((r) => setTimeout(r, entry.delayMs));
|
|
635
|
+
}
|
|
636
|
+
const isJson = typeof entry.body === "object" && entry.body !== null;
|
|
637
|
+
await route.fulfill({
|
|
638
|
+
status: entry.status ?? 200,
|
|
639
|
+
contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
|
|
640
|
+
body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
|
|
641
|
+
headers: entry.headers
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
async setupRouteMocks(routes) {
|
|
646
|
+
for (const r of routes) {
|
|
647
|
+
await this.addRouteMock(r);
|
|
648
|
+
}
|
|
887
649
|
}
|
|
888
650
|
async stop() {
|
|
889
651
|
if (this.context) {
|
|
@@ -906,133 +668,511 @@ var PreviewDriver = class {
|
|
|
906
668
|
};
|
|
907
669
|
|
|
908
670
|
// src/shared/lib/processManager.ts
|
|
909
|
-
var
|
|
671
|
+
var import_child_process = require("child_process");
|
|
910
672
|
var import_tree_kill = __toESM(require("tree-kill"));
|
|
911
673
|
var ProcessManager = class {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
exitCode = null;
|
|
917
|
-
/**
|
|
918
|
-
* Spawns a background process (e.g. dev server, backend, or app)
|
|
919
|
-
*/
|
|
920
|
-
async start(command, options2) {
|
|
921
|
-
this.stderrOutput = "";
|
|
922
|
-
this.stdoutOutput = "";
|
|
923
|
-
this.hasExited = false;
|
|
924
|
-
this.exitCode = null;
|
|
925
|
-
const cwd = options2?.cwd || process.cwd();
|
|
926
|
-
const env = { ...process.env, ...options2?.env };
|
|
674
|
+
childProcess = null;
|
|
675
|
+
isStopped = false;
|
|
676
|
+
async start(command, options) {
|
|
677
|
+
const cwd = options?.cwd || process.cwd();
|
|
927
678
|
console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
|
|
928
|
-
this.
|
|
679
|
+
this.childProcess = (0, import_child_process.spawn)(command, {
|
|
929
680
|
cwd,
|
|
930
|
-
env,
|
|
931
|
-
shell:
|
|
681
|
+
env: { ...process.env, ...options?.env },
|
|
682
|
+
shell: true,
|
|
932
683
|
stdio: ["ignore", "pipe", "pipe"]
|
|
933
684
|
});
|
|
934
|
-
this.
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.child.stderr?.on("data", (chunk) => {
|
|
939
|
-
const str = chunk.toString();
|
|
940
|
-
this.stderrOutput += str;
|
|
685
|
+
this.childProcess.stdout?.on("data", (chunk) => {
|
|
686
|
+
const line = chunk.toString().trim();
|
|
687
|
+
if (line) {
|
|
688
|
+
}
|
|
941
689
|
});
|
|
942
|
-
this.
|
|
943
|
-
|
|
944
|
-
|
|
690
|
+
this.childProcess.stderr?.on("data", (chunk) => {
|
|
691
|
+
const line = chunk.toString().trim();
|
|
692
|
+
if (line && !line.includes("ExperimentalWarning")) {
|
|
693
|
+
}
|
|
945
694
|
});
|
|
946
|
-
this.
|
|
947
|
-
|
|
695
|
+
this.childProcess.on("exit", (code, signal) => {
|
|
696
|
+
if (!this.isStopped && code !== 0 && code !== null) {
|
|
697
|
+
console.warn(`\u26A0\uFE0F [ProcessManager] Subprocess exited prematurely with code ${code}, signal ${signal}`);
|
|
698
|
+
}
|
|
948
699
|
});
|
|
949
700
|
}
|
|
950
|
-
/**
|
|
951
|
-
* Polls a URL until it starts responding or until timeout is reached.
|
|
952
|
-
*/
|
|
953
701
|
async waitForUrl(url, timeoutMs = 3e4) {
|
|
702
|
+
console.log(`\u23F3 [ProcessManager] Waiting for ${url} to respond...`);
|
|
954
703
|
const startTime = Date.now();
|
|
955
|
-
console.log(`\u23F3 [ProcessManager] Waiting for URL to become available: ${url} (timeout: ${timeoutMs / 1e3}s)...`);
|
|
956
704
|
while (Date.now() - startTime < timeoutMs) {
|
|
957
|
-
if (this.
|
|
705
|
+
if (this.childProcess && this.childProcess.exitCode !== null) {
|
|
958
706
|
throw new Error(
|
|
959
|
-
`[ProcessManager]
|
|
960
|
-
Stderr:
|
|
961
|
-
${this.stderrOutput.trim() || "(no stderr)"}
|
|
962
|
-
Stdout:
|
|
963
|
-
${this.stdoutOutput.slice(-500).trim()}`
|
|
707
|
+
`[ProcessManager] Server process exited with code ${this.childProcess.exitCode} while waiting for ${url}`
|
|
964
708
|
);
|
|
965
709
|
}
|
|
966
710
|
try {
|
|
967
|
-
const response = await fetch(url, {
|
|
711
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
968
712
|
if (response.status) {
|
|
969
|
-
console.log(`\u2705 [ProcessManager]
|
|
970
|
-
return;
|
|
713
|
+
console.log(`\u2705 [ProcessManager] Target ${url} is ready (status: ${response.status})!`);
|
|
714
|
+
return true;
|
|
971
715
|
}
|
|
972
716
|
} catch {
|
|
973
717
|
}
|
|
974
|
-
await new Promise((
|
|
718
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
975
719
|
}
|
|
976
|
-
throw new Error(
|
|
977
|
-
`[ProcessManager] Timeout (${timeoutMs / 1e3}s) waiting for server at ${url}.
|
|
978
|
-
Last stdout:
|
|
979
|
-
${this.stdoutOutput.slice(-500).trim()}
|
|
980
|
-
Last stderr:
|
|
981
|
-
${this.stderrOutput.trim()}`
|
|
982
|
-
);
|
|
720
|
+
throw new Error(`[ProcessManager] Timeout after ${timeoutMs}ms waiting for ${url} to respond.`);
|
|
983
721
|
}
|
|
984
|
-
/**
|
|
985
|
-
* Gracefully and forcefully kills the process and all its children.
|
|
986
|
-
*/
|
|
987
722
|
async stop() {
|
|
988
|
-
if (
|
|
989
|
-
this.child = null;
|
|
723
|
+
if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
|
|
990
724
|
return;
|
|
991
725
|
}
|
|
992
|
-
|
|
993
|
-
|
|
726
|
+
this.isStopped = true;
|
|
727
|
+
const pid = this.childProcess.pid;
|
|
728
|
+
console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
|
|
994
729
|
await new Promise((resolve) => {
|
|
995
|
-
(0, import_tree_kill.default)(pid, "
|
|
730
|
+
(0, import_tree_kill.default)(pid, "SIGTERM", (err) => {
|
|
996
731
|
if (err) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
732
|
+
try {
|
|
733
|
+
(0, import_tree_kill.default)(pid, "SIGKILL");
|
|
734
|
+
} catch {
|
|
1002
735
|
}
|
|
1003
736
|
}
|
|
1004
737
|
resolve();
|
|
1005
738
|
});
|
|
1006
739
|
});
|
|
1007
|
-
this.
|
|
740
|
+
this.childProcess = null;
|
|
1008
741
|
}
|
|
1009
742
|
};
|
|
1010
743
|
|
|
1011
|
-
// src/
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
744
|
+
// src/shared/lib/pluginLoader.ts
|
|
745
|
+
var import_path5 = __toESM(require("path"));
|
|
746
|
+
var import_fs5 = __toESM(require("fs"));
|
|
747
|
+
var import_jiti = require("jiti");
|
|
748
|
+
function findPackageRoot() {
|
|
749
|
+
let cur = __dirname;
|
|
750
|
+
while (cur !== import_path5.default.dirname(cur)) {
|
|
751
|
+
const pkgPath = import_path5.default.join(cur, "package.json");
|
|
752
|
+
if (import_fs5.default.existsSync(pkgPath)) {
|
|
753
|
+
try {
|
|
754
|
+
const pkg = JSON.parse(import_fs5.default.readFileSync(pkgPath, "utf-8"));
|
|
755
|
+
if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
|
|
756
|
+
return cur;
|
|
757
|
+
}
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
1023
760
|
}
|
|
761
|
+
cur = import_path5.default.dirname(cur);
|
|
1024
762
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
763
|
+
return import_path5.default.resolve(__dirname, "..");
|
|
764
|
+
}
|
|
765
|
+
var PluginManager = class {
|
|
766
|
+
plugins = [];
|
|
767
|
+
jiti = (0, import_jiti.createJiti)(process.cwd());
|
|
768
|
+
register(plugin) {
|
|
769
|
+
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.plugins.push(plugin);
|
|
773
|
+
console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
|
|
774
|
+
}
|
|
775
|
+
async load(pluginSpec) {
|
|
776
|
+
if (typeof pluginSpec === "object" && pluginSpec !== null) {
|
|
777
|
+
this.register(pluginSpec);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const pluginNameOrPath = pluginSpec.trim();
|
|
781
|
+
if (!pluginNameOrPath) return;
|
|
782
|
+
if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
|
|
783
|
+
const resolvedPath = import_path5.default.resolve(process.cwd(), pluginNameOrPath);
|
|
784
|
+
await this.loadFromFile(resolvedPath);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const localCandidates = [
|
|
788
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
|
|
789
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
|
|
790
|
+
import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
|
|
791
|
+
import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
|
|
792
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
|
|
793
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
|
|
794
|
+
import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
|
|
795
|
+
import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
|
|
796
|
+
];
|
|
797
|
+
for (const candidate of localCandidates) {
|
|
798
|
+
if (import_fs5.default.existsSync(candidate)) {
|
|
799
|
+
await this.loadFromFile(candidate);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const pkgRoot = findPackageRoot();
|
|
804
|
+
const builtInCandidates = [
|
|
805
|
+
import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
|
|
806
|
+
import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
|
|
807
|
+
import_path5.default.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
|
|
808
|
+
import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
|
|
809
|
+
import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
|
|
810
|
+
import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
|
|
811
|
+
import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
|
|
812
|
+
import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
|
|
813
|
+
import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
|
|
814
|
+
];
|
|
815
|
+
for (const builtIn of builtInCandidates) {
|
|
816
|
+
if (import_fs5.default.existsSync(builtIn)) {
|
|
817
|
+
await this.loadFromFile(builtIn);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const mod = await this.jiti.import(pluginNameOrPath);
|
|
823
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
824
|
+
if (plugin && plugin.name) {
|
|
825
|
+
this.register(plugin);
|
|
826
|
+
} else {
|
|
827
|
+
console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async loadFromFile(filePath) {
|
|
834
|
+
if (!import_fs5.default.existsSync(filePath)) {
|
|
835
|
+
console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const mod = await this.jiti.import(filePath);
|
|
840
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
841
|
+
if (plugin && plugin.name) {
|
|
842
|
+
this.register(plugin);
|
|
843
|
+
} else {
|
|
844
|
+
console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async loadAll(specs) {
|
|
851
|
+
for (const spec of specs) {
|
|
852
|
+
await this.load(spec);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async runSetup(hookContext) {
|
|
856
|
+
for (const p of this.plugins) {
|
|
857
|
+
if (p.setup) await p.setup(hookContext);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async launchSession(options, hookContext) {
|
|
861
|
+
for (const p of this.plugins) {
|
|
862
|
+
if (p.launchSession) {
|
|
863
|
+
const session = await p.launchSession(options, hookContext);
|
|
864
|
+
if (session) {
|
|
865
|
+
return session;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
async runOnContextCreated(context, hookContext) {
|
|
872
|
+
for (const p of this.plugins) {
|
|
873
|
+
if (p.onContextCreated) await p.onContextCreated(context, hookContext);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async runOnPageCreated(page, context, hookContext) {
|
|
877
|
+
for (const p of this.plugins) {
|
|
878
|
+
if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async extendContext(ctx, page, hookContext) {
|
|
882
|
+
for (const p of this.plugins) {
|
|
883
|
+
if (p.extendContext) {
|
|
884
|
+
const extensions = await p.extendContext(ctx, page, hookContext);
|
|
885
|
+
if (extensions && typeof extensions === "object") {
|
|
886
|
+
Object.assign(ctx, extensions);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async runOnAfterRun(reportData, hookContext) {
|
|
892
|
+
for (const p of this.plugins) {
|
|
893
|
+
if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async runTeardown(hookContext) {
|
|
897
|
+
for (const p of this.plugins) {
|
|
898
|
+
if (p.teardown) {
|
|
899
|
+
try {
|
|
900
|
+
await p.teardown(hookContext);
|
|
901
|
+
} catch (e) {
|
|
902
|
+
console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
get loadedPlugins() {
|
|
908
|
+
return [...this.plugins];
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/features/runner/lib/navigation.ts
|
|
913
|
+
function createNavigator(page, baseUrl, targetUrl) {
|
|
914
|
+
return async (route) => {
|
|
915
|
+
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
916
|
+
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
917
|
+
await page.waitForTimeout(300);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (route.startsWith("#")) {
|
|
921
|
+
await page.evaluate((r) => {
|
|
922
|
+
window.location.hash = r;
|
|
923
|
+
}, route);
|
|
924
|
+
await page.waitForTimeout(300);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (baseUrl && targetUrl) {
|
|
928
|
+
try {
|
|
929
|
+
const fullUrl = new URL(route, baseUrl).toString();
|
|
930
|
+
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
931
|
+
await page.waitForTimeout(300);
|
|
932
|
+
return;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
937
|
+
await page.evaluate((r) => {
|
|
938
|
+
if (window.location.hash !== void 0) {
|
|
939
|
+
window.location.hash = r;
|
|
940
|
+
}
|
|
941
|
+
}, routePath);
|
|
942
|
+
await page.waitForTimeout(300);
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/features/runner/lib/artifactsSync.ts
|
|
947
|
+
var import_fs6 = __toESM(require("fs"));
|
|
948
|
+
var import_path6 = __toESM(require("path"));
|
|
949
|
+
function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
|
|
950
|
+
try {
|
|
951
|
+
const latestDir = import_path6.default.join(artifactsRoot, "latest");
|
|
952
|
+
if (import_fs6.default.existsSync(latestDir)) {
|
|
953
|
+
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
954
|
+
}
|
|
955
|
+
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
956
|
+
if (import_fs6.default.existsSync(reportPath)) {
|
|
957
|
+
import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
|
|
958
|
+
}
|
|
959
|
+
const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
|
|
960
|
+
if (import_fs6.default.existsSync(manifestSrc)) {
|
|
961
|
+
import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
|
|
962
|
+
}
|
|
963
|
+
for (const snap of snapshots) {
|
|
964
|
+
if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
|
|
965
|
+
import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return import_path6.default.join(latestDir, "report.md");
|
|
969
|
+
} catch {
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/features/runner/lib/contextBuilder.ts
|
|
975
|
+
function buildTestContext(options) {
|
|
976
|
+
const {
|
|
977
|
+
page,
|
|
978
|
+
context,
|
|
979
|
+
targetMode,
|
|
980
|
+
initialViewport,
|
|
981
|
+
captureEngine,
|
|
982
|
+
consoleTracker,
|
|
983
|
+
previewDriver,
|
|
984
|
+
doNavigate
|
|
985
|
+
} = options;
|
|
986
|
+
let currentViewport = { ...initialViewport };
|
|
987
|
+
const ctx = {
|
|
988
|
+
page,
|
|
989
|
+
context,
|
|
990
|
+
targetMode,
|
|
991
|
+
currentViewport,
|
|
992
|
+
// --- Snapshots ---
|
|
993
|
+
capture: async (name, opts) => {
|
|
994
|
+
console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
|
|
995
|
+
return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
|
|
996
|
+
},
|
|
997
|
+
captureBurst: async (name, opts) => {
|
|
998
|
+
console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
|
|
999
|
+
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1000
|
+
},
|
|
1001
|
+
// --- Navigation ---
|
|
1002
|
+
navigate: async (route) => {
|
|
1003
|
+
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1004
|
+
await doNavigate(route);
|
|
1005
|
+
},
|
|
1006
|
+
// --- Viewport Management ---
|
|
1007
|
+
resize: async (width, height) => {
|
|
1008
|
+
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1009
|
+
currentViewport = { width, height };
|
|
1010
|
+
ctx.currentViewport = currentViewport;
|
|
1011
|
+
await page.setViewportSize(currentViewport);
|
|
1012
|
+
await page.waitForTimeout(200);
|
|
1013
|
+
},
|
|
1014
|
+
setPreset: async (preset) => {
|
|
1015
|
+
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1016
|
+
await ctx.resize(preset.width, preset.height);
|
|
1017
|
+
},
|
|
1018
|
+
resizeToFit: async (selector, padding = 0) => {
|
|
1019
|
+
let boundingBox;
|
|
1020
|
+
if (selector) {
|
|
1021
|
+
const el = await page.$(selector);
|
|
1022
|
+
if (el) {
|
|
1023
|
+
boundingBox = await el.boundingBox();
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
boundingBox = await page.evaluate(() => ({
|
|
1027
|
+
width: document.documentElement.scrollWidth,
|
|
1028
|
+
height: document.documentElement.scrollHeight
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1031
|
+
if (boundingBox) {
|
|
1032
|
+
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1033
|
+
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1034
|
+
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1035
|
+
await ctx.resize(newWidth, newHeight);
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
// --- Waiting & DOM Observation ---
|
|
1041
|
+
wait: async (ms) => {
|
|
1042
|
+
await page.waitForTimeout(ms);
|
|
1043
|
+
},
|
|
1044
|
+
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1045
|
+
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1046
|
+
},
|
|
1047
|
+
// --- Interaction ---
|
|
1048
|
+
click: async (selector) => {
|
|
1049
|
+
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1050
|
+
await page.click(selector);
|
|
1051
|
+
},
|
|
1052
|
+
rightClick: async (selector) => {
|
|
1053
|
+
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1054
|
+
await page.click(selector, { button: "right" });
|
|
1055
|
+
},
|
|
1056
|
+
type: async (selector, text) => {
|
|
1057
|
+
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1058
|
+
await page.fill(selector, text);
|
|
1059
|
+
},
|
|
1060
|
+
selectOption: async (selector, value) => {
|
|
1061
|
+
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1062
|
+
await page.selectOption(selector, value);
|
|
1063
|
+
},
|
|
1064
|
+
hover: async (selector) => {
|
|
1065
|
+
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1066
|
+
await page.hover(selector);
|
|
1067
|
+
},
|
|
1068
|
+
scroll: async (selector, deltaY) => {
|
|
1069
|
+
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1070
|
+
await page.evaluate(
|
|
1071
|
+
({ sel, dY }) => {
|
|
1072
|
+
const el = document.querySelector(sel);
|
|
1073
|
+
if (el) {
|
|
1074
|
+
el.scrollTop += dY;
|
|
1075
|
+
} else {
|
|
1076
|
+
window.scrollBy(0, dY);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
{ sel: selector, dY: deltaY }
|
|
1080
|
+
);
|
|
1081
|
+
await page.waitForTimeout(100);
|
|
1082
|
+
},
|
|
1083
|
+
// --- Logging ---
|
|
1084
|
+
log: (msg) => {
|
|
1085
|
+
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1086
|
+
},
|
|
1087
|
+
// --- Mock IPC (delegated or stubbed if plugin not loaded) ---
|
|
1088
|
+
setMockIpc: async (action, data, mockOptions) => {
|
|
1089
|
+
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
|
|
1090
|
+
},
|
|
1091
|
+
// --- HTTP Route Mocking ---
|
|
1092
|
+
setMockRoute: async (url, body, routeOptions) => {
|
|
1093
|
+
if (!previewDriver) {
|
|
1094
|
+
console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
|
|
1098
|
+
await previewDriver.addRouteMock({
|
|
1099
|
+
url,
|
|
1100
|
+
body,
|
|
1101
|
+
method: routeOptions?.method,
|
|
1102
|
+
status: routeOptions?.status,
|
|
1103
|
+
delayMs: routeOptions?.delayMs,
|
|
1104
|
+
headers: routeOptions?.headers
|
|
1105
|
+
});
|
|
1106
|
+
},
|
|
1107
|
+
// --- Console Errors ---
|
|
1108
|
+
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1109
|
+
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1110
|
+
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1111
|
+
// --- DOM Assertions ---
|
|
1112
|
+
readText: async (selector) => {
|
|
1113
|
+
const text = await page.textContent(selector);
|
|
1114
|
+
return text ? text.trim() : null;
|
|
1115
|
+
},
|
|
1116
|
+
getPageText: async () => {
|
|
1117
|
+
return await page.evaluate(() => document.body.innerText || "");
|
|
1118
|
+
},
|
|
1119
|
+
isVisible: async (selector) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const element = await page.$(selector);
|
|
1122
|
+
if (!element) return false;
|
|
1123
|
+
return await element.isVisible();
|
|
1124
|
+
} catch {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
getElementCount: async (selector) => {
|
|
1129
|
+
const elements = await page.$$(selector);
|
|
1130
|
+
return elements.length;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
return {
|
|
1134
|
+
ctx,
|
|
1135
|
+
getCurrentViewport: () => currentViewport
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/features/runner/runner.ts
|
|
1140
|
+
async function runVisualScenario(options) {
|
|
1141
|
+
const { scenario } = options;
|
|
1142
|
+
const targetMode = options.targetMode || "preview";
|
|
1143
|
+
const startTime = Date.now();
|
|
1144
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1145
|
+
const artifactsRoot = options.artifactsRoot || import_path7.default.resolve(process.cwd(), "artifacts");
|
|
1146
|
+
if (options.cleanArtifacts && import_fs7.default.existsSync(artifactsRoot)) {
|
|
1147
|
+
console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
|
|
1148
|
+
const entries = import_fs7.default.readdirSync(artifactsRoot, { withFileTypes: true });
|
|
1149
|
+
for (const entry of entries) {
|
|
1150
|
+
import_fs7.default.rmSync(import_path7.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const scenarioArtifactsDir = import_path7.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
|
|
1154
|
+
if (!import_fs7.default.existsSync(scenarioArtifactsDir)) {
|
|
1155
|
+
import_fs7.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
|
|
1028
1156
|
}
|
|
1029
1157
|
const captureEngine = new CaptureEngine(scenarioArtifactsDir);
|
|
1030
1158
|
const consoleTracker = new ConsoleTracker();
|
|
1031
1159
|
const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
|
|
1032
|
-
|
|
1033
|
-
let desktopDriver = null;
|
|
1160
|
+
const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
|
|
1034
1161
|
let previewDriver = null;
|
|
1035
1162
|
let processManager = null;
|
|
1163
|
+
let customDriverStop = null;
|
|
1164
|
+
const pluginManager = new PluginManager();
|
|
1165
|
+
const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
|
|
1166
|
+
if (pluginSpecs.length > 0) {
|
|
1167
|
+
await pluginManager.loadAll(pluginSpecs);
|
|
1168
|
+
}
|
|
1169
|
+
const hookContext = {
|
|
1170
|
+
scenario,
|
|
1171
|
+
targetMode,
|
|
1172
|
+
artifactsDir: scenarioArtifactsDir,
|
|
1173
|
+
cliOptions: options,
|
|
1174
|
+
state: /* @__PURE__ */ new Map()
|
|
1175
|
+
};
|
|
1036
1176
|
try {
|
|
1037
1177
|
let page;
|
|
1038
1178
|
let context;
|
|
@@ -1043,211 +1183,69 @@ async function runVisualScenario(options2) {
|
|
|
1043
1183
|
console.log(`\u{1F4C1} Artifacts: ${scenarioArtifactsDir}`);
|
|
1044
1184
|
console.log(`========================================
|
|
1045
1185
|
`);
|
|
1046
|
-
if (
|
|
1186
|
+
if (options.startCommand) {
|
|
1047
1187
|
processManager = new ProcessManager();
|
|
1048
|
-
await processManager.start(
|
|
1049
|
-
const waitTarget =
|
|
1188
|
+
await processManager.start(options.startCommand, { cwd: options.startCwd });
|
|
1189
|
+
const waitTarget = options.url || "http://localhost:5173";
|
|
1050
1190
|
await processManager.waitForUrl(waitTarget);
|
|
1051
1191
|
}
|
|
1192
|
+
await pluginManager.runSetup(hookContext);
|
|
1052
1193
|
if (typeof scenario.setup === "function") {
|
|
1053
1194
|
console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
|
|
1054
1195
|
await scenario.setup();
|
|
1055
1196
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1197
|
+
const customSession = await pluginManager.launchSession(
|
|
1198
|
+
{ currentViewport, headed: options.headed },
|
|
1199
|
+
hookContext
|
|
1200
|
+
);
|
|
1201
|
+
if (customSession) {
|
|
1202
|
+
page = customSession.page;
|
|
1203
|
+
context = customSession.context;
|
|
1204
|
+
if (customSession.stop) {
|
|
1205
|
+
customDriverStop = customSession.stop;
|
|
1206
|
+
}
|
|
1207
|
+
} else if (targetMode === "desktop") {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
|
|
1210
|
+
);
|
|
1067
1211
|
} else {
|
|
1068
1212
|
previewDriver = new PreviewDriver({
|
|
1069
|
-
wwwrootDir:
|
|
1070
|
-
url:
|
|
1071
|
-
headed:
|
|
1213
|
+
wwwrootDir: options.wwwrootDir,
|
|
1214
|
+
url: options.url,
|
|
1215
|
+
headed: options.headed
|
|
1072
1216
|
});
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
previewDriver.mockRegistry.setBatch(mergedMocks);
|
|
1217
|
+
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1218
|
+
console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
|
|
1219
|
+
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1077
1220
|
}
|
|
1078
1221
|
const res = await previewDriver.start(currentViewport);
|
|
1079
1222
|
page = res.page;
|
|
1080
1223
|
context = res.context;
|
|
1224
|
+
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1225
|
+
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1226
|
+
}
|
|
1081
1227
|
}
|
|
1228
|
+
await pluginManager.runOnContextCreated(context, hookContext);
|
|
1229
|
+
await pluginManager.runOnPageCreated(page, context, hookContext);
|
|
1082
1230
|
consoleTracker.attach(page);
|
|
1083
1231
|
console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
|
|
1084
1232
|
`);
|
|
1085
|
-
const doNavigate =
|
|
1086
|
-
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
1087
|
-
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
1088
|
-
await page.waitForTimeout(300);
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
if (route.startsWith("#")) {
|
|
1092
|
-
await page.evaluate((r) => {
|
|
1093
|
-
window.location.hash = r;
|
|
1094
|
-
}, route);
|
|
1095
|
-
await page.waitForTimeout(300);
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
if (previewDriver?.baseUrl && options2.url) {
|
|
1099
|
-
try {
|
|
1100
|
-
const fullUrl = new URL(route, previewDriver.baseUrl).toString();
|
|
1101
|
-
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
1102
|
-
await page.waitForTimeout(300);
|
|
1103
|
-
return;
|
|
1104
|
-
} catch {
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
1108
|
-
await page.evaluate((r) => {
|
|
1109
|
-
if (window.location.hash !== void 0) {
|
|
1110
|
-
window.location.hash = r;
|
|
1111
|
-
}
|
|
1112
|
-
}, routePath);
|
|
1113
|
-
await page.waitForTimeout(300);
|
|
1114
|
-
};
|
|
1233
|
+
const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
|
|
1115
1234
|
if (scenario.route) {
|
|
1116
|
-
console.log(
|
|
1235
|
+
console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
|
|
1117
1236
|
await doNavigate(scenario.route);
|
|
1118
1237
|
}
|
|
1119
|
-
const ctx = {
|
|
1238
|
+
const { ctx } = buildTestContext({
|
|
1120
1239
|
page,
|
|
1121
1240
|
context,
|
|
1122
1241
|
targetMode,
|
|
1123
|
-
currentViewport,
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1131
|
-
},
|
|
1132
|
-
navigate: async (route) => {
|
|
1133
|
-
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1134
|
-
await doNavigate(route);
|
|
1135
|
-
},
|
|
1136
|
-
// ─── Viewport ───
|
|
1137
|
-
resize: async (width, height) => {
|
|
1138
|
-
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1139
|
-
currentViewport = { width, height };
|
|
1140
|
-
ctx.currentViewport = currentViewport;
|
|
1141
|
-
await page.setViewportSize(currentViewport);
|
|
1142
|
-
await page.waitForTimeout(200);
|
|
1143
|
-
},
|
|
1144
|
-
setPreset: async (preset) => {
|
|
1145
|
-
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1146
|
-
await ctx.resize(preset.width, preset.height);
|
|
1147
|
-
},
|
|
1148
|
-
resizeToFit: async (selector, padding = 0) => {
|
|
1149
|
-
let boundingBox;
|
|
1150
|
-
if (selector) {
|
|
1151
|
-
const el = await page.$(selector);
|
|
1152
|
-
if (el) {
|
|
1153
|
-
boundingBox = await el.boundingBox();
|
|
1154
|
-
}
|
|
1155
|
-
} else {
|
|
1156
|
-
boundingBox = await page.evaluate(() => {
|
|
1157
|
-
return {
|
|
1158
|
-
width: document.documentElement.scrollWidth,
|
|
1159
|
-
height: document.documentElement.scrollHeight
|
|
1160
|
-
};
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
if (boundingBox) {
|
|
1164
|
-
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1165
|
-
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1166
|
-
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1167
|
-
await ctx.resize(newWidth, newHeight);
|
|
1168
|
-
} else {
|
|
1169
|
-
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1170
|
-
}
|
|
1171
|
-
},
|
|
1172
|
-
wait: async (ms) => {
|
|
1173
|
-
await page.waitForTimeout(ms);
|
|
1174
|
-
},
|
|
1175
|
-
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1176
|
-
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1177
|
-
},
|
|
1178
|
-
click: async (selector) => {
|
|
1179
|
-
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1180
|
-
await page.click(selector);
|
|
1181
|
-
},
|
|
1182
|
-
rightClick: async (selector) => {
|
|
1183
|
-
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1184
|
-
await page.click(selector, { button: "right" });
|
|
1185
|
-
},
|
|
1186
|
-
type: async (selector, text) => {
|
|
1187
|
-
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1188
|
-
await page.fill(selector, text);
|
|
1189
|
-
},
|
|
1190
|
-
selectOption: async (selector, value) => {
|
|
1191
|
-
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1192
|
-
await page.selectOption(selector, value);
|
|
1193
|
-
},
|
|
1194
|
-
hover: async (selector) => {
|
|
1195
|
-
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1196
|
-
await page.hover(selector);
|
|
1197
|
-
},
|
|
1198
|
-
scroll: async (selector, deltaY) => {
|
|
1199
|
-
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1200
|
-
await page.evaluate(({ sel, dY }) => {
|
|
1201
|
-
const el = document.querySelector(sel);
|
|
1202
|
-
if (el) {
|
|
1203
|
-
el.scrollTop += dY;
|
|
1204
|
-
} else {
|
|
1205
|
-
window.scrollBy(0, dY);
|
|
1206
|
-
}
|
|
1207
|
-
}, { sel: selector, dY: deltaY });
|
|
1208
|
-
await page.waitForTimeout(100);
|
|
1209
|
-
},
|
|
1210
|
-
log: (msg) => {
|
|
1211
|
-
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1212
|
-
},
|
|
1213
|
-
// ─── Mock IPC ───
|
|
1214
|
-
setMockIpc: async (action, data, mockOptions) => {
|
|
1215
|
-
if (targetMode === "desktop") {
|
|
1216
|
-
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
if (!previewDriver) {
|
|
1220
|
-
console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
|
|
1224
|
-
await previewDriver.updateMockIpc(action, data, mockOptions);
|
|
1225
|
-
},
|
|
1226
|
-
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1227
|
-
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1228
|
-
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1229
|
-
// ─── DOM Assertions ───
|
|
1230
|
-
readText: async (selector) => {
|
|
1231
|
-
const text = await page.textContent(selector);
|
|
1232
|
-
return text ? text.trim() : null;
|
|
1233
|
-
},
|
|
1234
|
-
getPageText: async () => {
|
|
1235
|
-
return await page.evaluate(() => document.body.innerText || "");
|
|
1236
|
-
},
|
|
1237
|
-
isVisible: async (selector) => {
|
|
1238
|
-
try {
|
|
1239
|
-
const element = await page.$(selector);
|
|
1240
|
-
if (!element) return false;
|
|
1241
|
-
return await element.isVisible();
|
|
1242
|
-
} catch {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
},
|
|
1246
|
-
getElementCount: async (selector) => {
|
|
1247
|
-
const elements = await page.$$(selector);
|
|
1248
|
-
return elements.length;
|
|
1249
|
-
}
|
|
1250
|
-
};
|
|
1242
|
+
initialViewport: currentViewport,
|
|
1243
|
+
captureEngine,
|
|
1244
|
+
consoleTracker,
|
|
1245
|
+
previewDriver,
|
|
1246
|
+
doNavigate
|
|
1247
|
+
});
|
|
1248
|
+
await pluginManager.extendContext(ctx, page, hookContext);
|
|
1251
1249
|
await scenario.run(ctx);
|
|
1252
1250
|
const durationMs = Date.now() - startTime;
|
|
1253
1251
|
const snapshots = captureEngine.getSnapshots();
|
|
@@ -1262,38 +1260,19 @@ async function runVisualScenario(options2) {
|
|
|
1262
1260
|
console.log(`
|
|
1263
1261
|
\u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
|
|
1264
1262
|
}
|
|
1265
|
-
const
|
|
1263
|
+
const reportData = {
|
|
1266
1264
|
scenario,
|
|
1267
1265
|
snapshots,
|
|
1268
1266
|
consoleErrors,
|
|
1269
1267
|
consoleWarnings,
|
|
1270
1268
|
outputDir: scenarioArtifactsDir,
|
|
1271
1269
|
targetMode,
|
|
1272
|
-
durationMs
|
|
1273
|
-
|
|
1274
|
-
const syncLatest = (repPath, snaps) => {
|
|
1275
|
-
try {
|
|
1276
|
-
const latestDir = import_path6.default.join(artifactsRoot, "latest");
|
|
1277
|
-
if (import_fs6.default.existsSync(latestDir)) {
|
|
1278
|
-
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
1279
|
-
}
|
|
1280
|
-
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
1281
|
-
import_fs6.default.copyFileSync(repPath, import_path6.default.join(latestDir, "report.md"));
|
|
1282
|
-
const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
|
|
1283
|
-
if (import_fs6.default.existsSync(manifestSrc)) {
|
|
1284
|
-
import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
|
|
1285
|
-
}
|
|
1286
|
-
for (const snap of snaps) {
|
|
1287
|
-
if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
|
|
1288
|
-
import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return import_path6.default.join(latestDir, "report.md");
|
|
1292
|
-
} catch {
|
|
1293
|
-
return void 0;
|
|
1294
|
-
}
|
|
1270
|
+
durationMs,
|
|
1271
|
+
customSections: []
|
|
1295
1272
|
};
|
|
1296
|
-
|
|
1273
|
+
await pluginManager.runOnAfterRun(reportData, hookContext);
|
|
1274
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1275
|
+
const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1297
1276
|
console.log(`
|
|
1298
1277
|
\u2705 Visual Test Completed Successfully!`);
|
|
1299
1278
|
console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
|
|
@@ -1320,7 +1299,7 @@ async function runVisualScenario(options2) {
|
|
|
1320
1299
|
const snapshots = captureEngine.getSnapshots();
|
|
1321
1300
|
const consoleErrors = consoleTracker.getErrors();
|
|
1322
1301
|
const consoleWarnings = consoleTracker.getWarnings();
|
|
1323
|
-
const
|
|
1302
|
+
const reportData = {
|
|
1324
1303
|
scenario,
|
|
1325
1304
|
snapshots,
|
|
1326
1305
|
consoleErrors,
|
|
@@ -1328,16 +1307,9 @@ async function runVisualScenario(options2) {
|
|
|
1328
1307
|
outputDir: scenarioArtifactsDir,
|
|
1329
1308
|
targetMode,
|
|
1330
1309
|
durationMs
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
if (import_fs6.default.existsSync(latestDir)) {
|
|
1335
|
-
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
1336
|
-
}
|
|
1337
|
-
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
1338
|
-
import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1310
|
+
};
|
|
1311
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1312
|
+
syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1341
1313
|
return {
|
|
1342
1314
|
scenarioId: scenario.id,
|
|
1343
1315
|
targetMode,
|
|
@@ -1350,6 +1322,7 @@ async function runVisualScenario(options2) {
|
|
|
1350
1322
|
error: err.message
|
|
1351
1323
|
};
|
|
1352
1324
|
} finally {
|
|
1325
|
+
await pluginManager.runTeardown(hookContext);
|
|
1353
1326
|
if (typeof scenario.teardown === "function") {
|
|
1354
1327
|
try {
|
|
1355
1328
|
console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
|
|
@@ -1358,21 +1331,21 @@ async function runVisualScenario(options2) {
|
|
|
1358
1331
|
console.error(`\u26A0\uFE0F [Scenario Teardown] Error during teardown:`, teardownErr.message);
|
|
1359
1332
|
}
|
|
1360
1333
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
for (const cleanTarget of
|
|
1334
|
+
if (options.cleanPaths && options.cleanPaths.length > 0) {
|
|
1335
|
+
for (const cleanTarget of options.cleanPaths) {
|
|
1363
1336
|
try {
|
|
1364
|
-
const resolvedCleanPath =
|
|
1365
|
-
if (
|
|
1337
|
+
const resolvedCleanPath = import_path7.default.resolve(process.cwd(), cleanTarget);
|
|
1338
|
+
if (import_fs7.default.existsSync(resolvedCleanPath)) {
|
|
1366
1339
|
console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
|
|
1367
|
-
|
|
1340
|
+
import_fs7.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
|
|
1368
1341
|
}
|
|
1369
1342
|
} catch (cleanErr) {
|
|
1370
1343
|
console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
|
|
1371
1344
|
}
|
|
1372
1345
|
}
|
|
1373
1346
|
}
|
|
1374
|
-
if (!
|
|
1375
|
-
if (
|
|
1347
|
+
if (!options.detach) {
|
|
1348
|
+
if (customDriverStop) await customDriverStop();
|
|
1376
1349
|
if (previewDriver) await previewDriver.stop();
|
|
1377
1350
|
if (processManager) await processManager.stop();
|
|
1378
1351
|
} else {
|
|
@@ -1381,12 +1354,9 @@ async function runVisualScenario(options2) {
|
|
|
1381
1354
|
}
|
|
1382
1355
|
}
|
|
1383
1356
|
|
|
1384
|
-
// src/app/cli.ts
|
|
1385
|
-
var import_jiti = require("jiti");
|
|
1386
|
-
|
|
1387
1357
|
// src/features/snap/snap.ts
|
|
1388
|
-
var
|
|
1389
|
-
var
|
|
1358
|
+
var import_path8 = __toESM(require("path"));
|
|
1359
|
+
var import_fs8 = __toESM(require("fs"));
|
|
1390
1360
|
var PRESET_MAP = {
|
|
1391
1361
|
default: VIEWPORT_PRESETS.DEFAULT,
|
|
1392
1362
|
desktop: VIEWPORT_PRESETS.DEFAULT,
|
|
@@ -1415,6 +1385,7 @@ function parseViewportPresets(raw) {
|
|
|
1415
1385
|
}
|
|
1416
1386
|
return presets.length > 0 ? presets : [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
|
|
1417
1387
|
}
|
|
1388
|
+
var CANDIDATE_PORTS = [5173, 3e3, 4321, 4200, 8080, 8e3, 3001];
|
|
1418
1389
|
async function isPortResponding(url) {
|
|
1419
1390
|
try {
|
|
1420
1391
|
const res = await fetch(url, { signal: AbortSignal.timeout(800) });
|
|
@@ -1423,26 +1394,33 @@ async function isPortResponding(url) {
|
|
|
1423
1394
|
return false;
|
|
1424
1395
|
}
|
|
1425
1396
|
}
|
|
1426
|
-
async function
|
|
1427
|
-
|
|
1397
|
+
async function detectActiveDevServer() {
|
|
1398
|
+
for (const port of CANDIDATE_PORTS) {
|
|
1399
|
+
const url = `http://localhost:${port}`;
|
|
1400
|
+
if (await isPortResponding(url)) {
|
|
1401
|
+
return url;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
async function runQuickSnap(options) {
|
|
1407
|
+
let targetUrl = options.url;
|
|
1428
1408
|
let useStaticPreview = false;
|
|
1429
1409
|
let wwwrootDir;
|
|
1430
|
-
if (!targetUrl && !
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
targetUrl = "http://localhost:3000";
|
|
1436
|
-
console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:3000`);
|
|
1410
|
+
if (!targetUrl && !options.start) {
|
|
1411
|
+
const activeUrl = await detectActiveDevServer();
|
|
1412
|
+
if (activeUrl) {
|
|
1413
|
+
targetUrl = activeUrl;
|
|
1414
|
+
console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
|
|
1437
1415
|
} else {
|
|
1438
1416
|
const candidateDir = resolveWwwrootDir();
|
|
1439
|
-
if (
|
|
1417
|
+
if (import_fs8.default.existsSync(candidateDir) && import_fs8.default.existsSync(import_path8.default.join(candidateDir, "index.html"))) {
|
|
1440
1418
|
useStaticPreview = true;
|
|
1441
1419
|
wwwrootDir = candidateDir;
|
|
1442
1420
|
console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
|
|
1443
1421
|
} else {
|
|
1444
1422
|
console.error(`
|
|
1445
|
-
\u274C [Quick Snap] No active server found on
|
|
1423
|
+
\u274C [Quick Snap] No active server found on common ports (${CANDIDATE_PORTS.join(", ")}), and no static build found.`);
|
|
1446
1424
|
console.log(`\u{1F4A1} Suggested actions:`);
|
|
1447
1425
|
console.log(` 1. Pass a start command: npx agent-lens snap --start="npm run dev"`);
|
|
1448
1426
|
console.log(` 2. Specify your URL: npx agent-lens snap --url=http://localhost:8080`);
|
|
@@ -1451,17 +1429,17 @@ async function runQuickSnap(options2) {
|
|
|
1451
1429
|
return false;
|
|
1452
1430
|
}
|
|
1453
1431
|
}
|
|
1454
|
-
} else if (!targetUrl &&
|
|
1432
|
+
} else if (!targetUrl && options.start) {
|
|
1455
1433
|
targetUrl = "http://localhost:5173";
|
|
1456
1434
|
}
|
|
1457
|
-
const waitMs =
|
|
1458
|
-
const snapshotPrefix =
|
|
1459
|
-
const targetViewports = parseViewportPresets(
|
|
1435
|
+
const waitMs = options.waitMs ?? 1e3;
|
|
1436
|
+
const snapshotPrefix = options.name || "quick_snap";
|
|
1437
|
+
const targetViewports = parseViewportPresets(options.viewports);
|
|
1460
1438
|
console.log(`
|
|
1461
1439
|
\u{1F4F8} [Quick Snap] Preparing instant verification for: ${targetUrl || wwwrootDir}`);
|
|
1462
1440
|
console.log(`\u{1F4D0} [Quick Snap] Testing ${targetViewports.length} viewports: ${targetViewports.map((v) => `${v.name} (${v.width}x${v.height})`).join(", ")}`);
|
|
1463
|
-
if (
|
|
1464
|
-
console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${
|
|
1441
|
+
if (options.selector) {
|
|
1442
|
+
console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${options.selector}"`);
|
|
1465
1443
|
}
|
|
1466
1444
|
const snapScenario = defineVisualTest({
|
|
1467
1445
|
id: "quick-snap",
|
|
@@ -1478,16 +1456,16 @@ async function runQuickSnap(options2) {
|
|
|
1478
1456
|
ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
|
|
1479
1457
|
await ctx.setPreset(vp);
|
|
1480
1458
|
await ctx.wait(200);
|
|
1481
|
-
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}
|
|
1459
|
+
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
|
|
1482
1460
|
}
|
|
1483
|
-
if (
|
|
1484
|
-
ctx.log(`Focusing on selector: "${
|
|
1461
|
+
if (options.selector) {
|
|
1462
|
+
ctx.log(`Focusing on selector: "${options.selector}"`);
|
|
1485
1463
|
try {
|
|
1486
1464
|
await ctx.setPreset(VIEWPORT_PRESETS.DEFAULT);
|
|
1487
|
-
await ctx.resizeToFit(
|
|
1488
|
-
await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector:
|
|
1465
|
+
await ctx.resizeToFit(options.selector, 15);
|
|
1466
|
+
await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector: options.selector });
|
|
1489
1467
|
} catch (err) {
|
|
1490
|
-
ctx.log(`\u26A0\uFE0F Could not isolate selector "${
|
|
1468
|
+
ctx.log(`\u26A0\uFE0F Could not isolate selector "${options.selector}": ${err.message}`);
|
|
1491
1469
|
}
|
|
1492
1470
|
}
|
|
1493
1471
|
const errors = ctx.getConsoleErrors();
|
|
@@ -1500,18 +1478,19 @@ async function runQuickSnap(options2) {
|
|
|
1500
1478
|
});
|
|
1501
1479
|
const result = await runVisualScenario({
|
|
1502
1480
|
scenario: snapScenario,
|
|
1503
|
-
targetMode:
|
|
1481
|
+
targetMode: options.mode || "preview",
|
|
1504
1482
|
url: useStaticPreview ? void 0 : targetUrl,
|
|
1505
1483
|
wwwrootDir: useStaticPreview ? wwwrootDir : void 0,
|
|
1506
|
-
startCommand:
|
|
1507
|
-
startCwd:
|
|
1508
|
-
executablePath:
|
|
1509
|
-
cleanPaths:
|
|
1510
|
-
cleanArtifacts:
|
|
1511
|
-
port:
|
|
1512
|
-
headed:
|
|
1513
|
-
detach:
|
|
1514
|
-
artifactsRoot:
|
|
1484
|
+
startCommand: options.start,
|
|
1485
|
+
startCwd: options.startCwd,
|
|
1486
|
+
executablePath: options.exe,
|
|
1487
|
+
cleanPaths: options.clean,
|
|
1488
|
+
cleanArtifacts: options.cleanArtifacts,
|
|
1489
|
+
port: options.port,
|
|
1490
|
+
headed: options.headed,
|
|
1491
|
+
detach: options.detach,
|
|
1492
|
+
artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
|
|
1493
|
+
plugins: options.plugins
|
|
1515
1494
|
});
|
|
1516
1495
|
console.log(`
|
|
1517
1496
|
========================================`);
|
|
@@ -1525,96 +1504,112 @@ async function runQuickSnap(options2) {
|
|
|
1525
1504
|
return result.success && result.consoleErrors === 0;
|
|
1526
1505
|
}
|
|
1527
1506
|
|
|
1528
|
-
// src/app/
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1507
|
+
// src/app/lib/argsParser.ts
|
|
1508
|
+
function parseCliArgs(argv, fileConfig) {
|
|
1509
|
+
const isInitCommand = argv[0] === "init";
|
|
1510
|
+
const isSnapCommand = argv[0] === "snap";
|
|
1511
|
+
const effectiveArgs = isSnapCommand ? argv.slice(1) : argv;
|
|
1512
|
+
const options = {
|
|
1513
|
+
mode: fileConfig.mode || "preview",
|
|
1514
|
+
port: fileConfig.port || 9222,
|
|
1515
|
+
headed: fileConfig.headed ?? false,
|
|
1516
|
+
detach: fileConfig.detach ?? false,
|
|
1517
|
+
build: fileConfig.buildCommand || false,
|
|
1518
|
+
start: fileConfig.startCommand,
|
|
1519
|
+
startCwd: fileConfig.startCwd,
|
|
1520
|
+
cleanArtifacts: fileConfig.cleanArtifacts ?? false,
|
|
1521
|
+
url: fileConfig.url,
|
|
1522
|
+
exe: fileConfig.executablePath,
|
|
1523
|
+
clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
|
|
1524
|
+
dir: fileConfig.scenarios,
|
|
1525
|
+
wwwroot: fileConfig.wwwroot,
|
|
1526
|
+
outDir: fileConfig.outDir,
|
|
1527
|
+
plugins: fileConfig.plugins ? [...fileConfig.plugins] : []
|
|
1528
|
+
};
|
|
1529
|
+
for (const arg of effectiveArgs) {
|
|
1530
|
+
if (arg === "--help" || arg === "-h") {
|
|
1531
|
+
options.help = true;
|
|
1532
|
+
} else if (arg.startsWith("--scenario=")) {
|
|
1533
|
+
options.scenario = arg.split("=")[1];
|
|
1534
|
+
} else if (arg === "--all") {
|
|
1535
|
+
options.all = true;
|
|
1536
|
+
} else if (arg.startsWith("--mode=")) {
|
|
1537
|
+
options.mode = arg.split("=")[1].toLowerCase();
|
|
1538
|
+
} else if (arg.startsWith("--port=")) {
|
|
1539
|
+
options.port = parseInt(arg.split("=")[1], 10) || 9222;
|
|
1540
|
+
} else if (arg === "--headed") {
|
|
1541
|
+
options.headed = true;
|
|
1542
|
+
} else if (arg === "--detach") {
|
|
1543
|
+
options.detach = true;
|
|
1544
|
+
} else if (arg === "--build") {
|
|
1545
|
+
options.build = true;
|
|
1546
|
+
} else if (arg.startsWith("--build=")) {
|
|
1547
|
+
options.build = arg.slice("--build=".length);
|
|
1548
|
+
} else if (arg.startsWith("--start=")) {
|
|
1549
|
+
options.start = arg.slice("--start=".length);
|
|
1550
|
+
} else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
|
|
1551
|
+
options.startCwd = arg.split("=")[1];
|
|
1552
|
+
} else if (arg === "--clean-artifacts") {
|
|
1553
|
+
options.cleanArtifacts = true;
|
|
1554
|
+
} else if (arg.startsWith("--url=")) {
|
|
1555
|
+
options.url = arg.split("=")[1];
|
|
1556
|
+
} else if (arg.startsWith("--selector=")) {
|
|
1557
|
+
options.selector = arg.split("=")[1];
|
|
1558
|
+
} else if (arg.startsWith("--viewports=")) {
|
|
1559
|
+
const raw = arg.slice("--viewports=".length);
|
|
1560
|
+
options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1561
|
+
} else if (arg.startsWith("--wait=")) {
|
|
1562
|
+
options.waitMs = parseInt(arg.slice("--wait=".length), 10);
|
|
1563
|
+
} else if (arg.startsWith("--name=")) {
|
|
1564
|
+
options.name = arg.slice("--name=".length);
|
|
1565
|
+
} else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
|
|
1566
|
+
const prefix = arg.startsWith("--exe=") ? "--exe=" : "--executable=";
|
|
1567
|
+
options.exe = arg.slice(prefix.length);
|
|
1568
|
+
} else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
|
|
1569
|
+
const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
|
|
1570
|
+
const rawPaths = arg.slice(prefix.length);
|
|
1571
|
+
options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1572
|
+
} else if (arg.startsWith("--dir=")) {
|
|
1573
|
+
options.dir = arg.split("=")[1];
|
|
1574
|
+
} else if (arg.startsWith("--wwwroot=")) {
|
|
1575
|
+
options.wwwroot = arg.split("=")[1];
|
|
1576
|
+
} else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
|
|
1577
|
+
options.outDir = arg.split("=")[1];
|
|
1578
|
+
} else if (arg.startsWith("--plugin=") || arg.startsWith("--plugins=")) {
|
|
1579
|
+
const raw = arg.split("=")[1] || "";
|
|
1580
|
+
const items = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1581
|
+
options.plugins = [...options.plugins || [], ...items];
|
|
1582
|
+
} else if (arg === "--full" || arg === "--full-page") {
|
|
1583
|
+
options.fullPage = true;
|
|
1584
|
+
}
|
|
1602
1585
|
}
|
|
1586
|
+
if (options.start && !options.startCwd) {
|
|
1587
|
+
options.startCwd = detectStartCwd(options.startCwd);
|
|
1588
|
+
}
|
|
1589
|
+
if ((options.mode === "desktop" || options.exe) && !options.plugins?.includes("desktop-webview2")) {
|
|
1590
|
+
options.plugins = ["desktop-webview2", ...options.plugins || []];
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
isSnapCommand,
|
|
1594
|
+
isInitCommand,
|
|
1595
|
+
options
|
|
1596
|
+
};
|
|
1603
1597
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
}
|
|
1598
|
+
|
|
1599
|
+
// src/app/lib/help.ts
|
|
1607
1600
|
function printHelp() {
|
|
1608
1601
|
console.log(`
|
|
1609
1602
|
\u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
|
|
1610
1603
|
|
|
1611
1604
|
Usage:
|
|
1612
1605
|
npx agent-lens snap [options] Instant one-shot visual & console check (no test files needed)
|
|
1606
|
+
npx agent-lens live [action] Interactive live session (start, click, type, snap, stop)
|
|
1613
1607
|
npx agent-lens [options] Run scripted scenario tests from scenarios/
|
|
1614
1608
|
npx agent-lens init Generate starter scenario template & mocks
|
|
1615
1609
|
|
|
1616
1610
|
Commands:
|
|
1617
1611
|
snap Take immediate multi-viewport screenshots of a URL & check console errors
|
|
1612
|
+
live Interactive control: click coordinates/selectors, type text, snap --full
|
|
1618
1613
|
init Generate starter template in scenarios/template.scenario.ts and mocks.ts
|
|
1619
1614
|
|
|
1620
1615
|
Options:
|
|
@@ -1622,6 +1617,7 @@ Options:
|
|
|
1622
1617
|
--start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
|
|
1623
1618
|
--start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
|
|
1624
1619
|
--clean-artifacts Purge previous test artifacts to prevent folder bloat
|
|
1620
|
+
--full Capture full scrollable page instead of only the viewport
|
|
1625
1621
|
--selector=<css> Target a specific element to focus on / resize-to-fit
|
|
1626
1622
|
--viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
|
|
1627
1623
|
--wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
|
|
@@ -1633,6 +1629,7 @@ Options:
|
|
|
1633
1629
|
--port=<port> CDP remote debugging port [default: 9222]
|
|
1634
1630
|
--build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
|
|
1635
1631
|
--clean=<paths> Comma-separated paths to safely delete upon test exit (e.g. --clean="./temp,./cache")
|
|
1632
|
+
--plugin=<names> Comma-separated plugins (e.g. --plugin=desktop-webview2,mock-ipc)
|
|
1636
1633
|
--headed Show Chromium browser window
|
|
1637
1634
|
--detach Keep browser/app open after finishing
|
|
1638
1635
|
--dir=<path> Custom scenarios directory [default: scenarios]
|
|
@@ -1641,13 +1638,17 @@ Options:
|
|
|
1641
1638
|
--help, -h Show this help message
|
|
1642
1639
|
`);
|
|
1643
1640
|
}
|
|
1641
|
+
|
|
1642
|
+
// src/app/lib/templateInit.ts
|
|
1643
|
+
var import_path9 = __toESM(require("path"));
|
|
1644
|
+
var import_fs9 = __toESM(require("fs"));
|
|
1644
1645
|
function initScenarioTemplate() {
|
|
1645
|
-
const targetDir =
|
|
1646
|
-
if (!
|
|
1647
|
-
|
|
1646
|
+
const targetDir = import_path9.default.resolve(process.cwd(), "scenarios");
|
|
1647
|
+
if (!import_fs9.default.existsSync(targetDir)) {
|
|
1648
|
+
import_fs9.default.mkdirSync(targetDir, { recursive: true });
|
|
1648
1649
|
}
|
|
1649
|
-
const templatePath =
|
|
1650
|
-
if (!
|
|
1650
|
+
const templatePath = import_path9.default.join(targetDir, "template.scenario.ts");
|
|
1651
|
+
if (!import_fs9.default.existsSync(templatePath)) {
|
|
1651
1652
|
const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
|
|
1652
1653
|
|
|
1653
1654
|
export default defineVisualTest({
|
|
@@ -1678,13 +1679,13 @@ export default defineVisualTest({
|
|
|
1678
1679
|
}
|
|
1679
1680
|
});
|
|
1680
1681
|
`;
|
|
1681
|
-
|
|
1682
|
+
import_fs9.default.writeFileSync(templatePath, templateContent, "utf8");
|
|
1682
1683
|
console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
|
|
1683
1684
|
} else {
|
|
1684
1685
|
console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
|
|
1685
1686
|
}
|
|
1686
|
-
const mocksPath =
|
|
1687
|
-
if (!
|
|
1687
|
+
const mocksPath = import_path9.default.join(targetDir, "mocks.ts");
|
|
1688
|
+
if (!import_fs9.default.existsSync(mocksPath)) {
|
|
1688
1689
|
const mocksContent = `/**
|
|
1689
1690
|
* Global IPC & API Mocks
|
|
1690
1691
|
*
|
|
@@ -1695,21 +1696,21 @@ export default [
|
|
|
1695
1696
|
{ action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
|
|
1696
1697
|
];
|
|
1697
1698
|
`;
|
|
1698
|
-
|
|
1699
|
+
import_fs9.default.writeFileSync(mocksPath, mocksContent, "utf8");
|
|
1699
1700
|
console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
|
|
1700
1701
|
}
|
|
1701
1702
|
console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
|
|
1702
1703
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1704
|
+
|
|
1705
|
+
// src/app/lib/scenarioFinder.ts
|
|
1706
|
+
var import_path10 = __toESM(require("path"));
|
|
1707
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1707
1708
|
async function findScenarios(dir, specificName) {
|
|
1708
1709
|
const results = [];
|
|
1709
|
-
if (!
|
|
1710
|
-
const items =
|
|
1710
|
+
if (!import_fs10.default.existsSync(dir)) return results;
|
|
1711
|
+
const items = import_fs10.default.readdirSync(dir, { withFileTypes: true });
|
|
1711
1712
|
for (const item of items) {
|
|
1712
|
-
const fullPath =
|
|
1713
|
+
const fullPath = import_path10.default.join(dir, item.name);
|
|
1713
1714
|
if (item.isDirectory()) {
|
|
1714
1715
|
results.push(...await findScenarios(fullPath, specificName));
|
|
1715
1716
|
} else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
|
|
@@ -1722,7 +1723,7 @@ async function findScenarios(dir, specificName) {
|
|
|
1722
1723
|
}
|
|
1723
1724
|
function resolveScenariosDirectory(customDir) {
|
|
1724
1725
|
if (customDir) {
|
|
1725
|
-
return
|
|
1726
|
+
return import_path10.default.resolve(process.cwd(), customDir);
|
|
1726
1727
|
}
|
|
1727
1728
|
const candidates = [
|
|
1728
1729
|
"scenarios",
|
|
@@ -1732,122 +1733,333 @@ function resolveScenariosDirectory(customDir) {
|
|
|
1732
1733
|
"src/scenarios"
|
|
1733
1734
|
];
|
|
1734
1735
|
for (const c of candidates) {
|
|
1735
|
-
const p =
|
|
1736
|
-
if (
|
|
1736
|
+
const p = import_path10.default.resolve(process.cwd(), c);
|
|
1737
|
+
if (import_fs10.default.existsSync(p)) {
|
|
1737
1738
|
return p;
|
|
1738
1739
|
}
|
|
1739
1740
|
}
|
|
1740
|
-
return
|
|
1741
|
+
return import_path10.default.resolve(process.cwd(), "scenarios");
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/plugins/live-controller/actions.ts
|
|
1745
|
+
var import_path12 = __toESM(require("path"));
|
|
1746
|
+
|
|
1747
|
+
// src/plugins/live-controller/session.ts
|
|
1748
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1749
|
+
var import_path11 = __toESM(require("path"));
|
|
1750
|
+
var import_tree_kill2 = __toESM(require("tree-kill"));
|
|
1751
|
+
var SESSION_DIR = import_path11.default.resolve(process.cwd(), ".agent-lens");
|
|
1752
|
+
var SESSION_FILE = import_path11.default.join(SESSION_DIR, "live-session.json");
|
|
1753
|
+
var LIVE_ARTIFACTS_DIR = import_path11.default.resolve(process.cwd(), "artifacts", "live");
|
|
1754
|
+
function getLiveArtifactsDir() {
|
|
1755
|
+
if (!import_fs11.default.existsSync(LIVE_ARTIFACTS_DIR)) {
|
|
1756
|
+
import_fs11.default.mkdirSync(LIVE_ARTIFACTS_DIR, { recursive: true });
|
|
1757
|
+
}
|
|
1758
|
+
return LIVE_ARTIFACTS_DIR;
|
|
1759
|
+
}
|
|
1760
|
+
function readLiveSession() {
|
|
1761
|
+
if (!import_fs11.default.existsSync(SESSION_FILE)) return null;
|
|
1762
|
+
try {
|
|
1763
|
+
return JSON.parse(import_fs11.default.readFileSync(SESSION_FILE, "utf-8"));
|
|
1764
|
+
} catch {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function saveLiveSession(info) {
|
|
1769
|
+
if (!import_fs11.default.existsSync(SESSION_DIR)) {
|
|
1770
|
+
import_fs11.default.mkdirSync(SESSION_DIR, { recursive: true });
|
|
1771
|
+
}
|
|
1772
|
+
import_fs11.default.writeFileSync(SESSION_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
1773
|
+
}
|
|
1774
|
+
function clearLiveSession() {
|
|
1775
|
+
if (import_fs11.default.existsSync(SESSION_FILE)) {
|
|
1776
|
+
try {
|
|
1777
|
+
import_fs11.default.unlinkSync(SESSION_FILE);
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function connectToLiveSession() {
|
|
1783
|
+
const session = readLiveSession();
|
|
1784
|
+
if (!session) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
`No active live session found. Start one with: npx agent-lens live start --url=http://localhost:5173`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${session.port}`);
|
|
1790
|
+
const contexts = browser.contexts();
|
|
1791
|
+
const context = contexts[0] || await browser.newContext();
|
|
1792
|
+
const pages = context.pages();
|
|
1793
|
+
const page = pages[0] || await context.waitForEvent("page", { timeout: 5e3 });
|
|
1794
|
+
return { browser, context, page };
|
|
1741
1795
|
}
|
|
1742
|
-
async function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
name: options.name,
|
|
1752
|
-
clean: options.clean,
|
|
1753
|
-
cleanArtifacts: options.cleanArtifacts,
|
|
1754
|
-
headed: options.headed,
|
|
1755
|
-
detach: options.detach,
|
|
1756
|
-
outDir: options.outDir,
|
|
1757
|
-
mode: options.mode,
|
|
1758
|
-
exe: options.exe,
|
|
1759
|
-
port: options.port
|
|
1796
|
+
async function stopLiveSession() {
|
|
1797
|
+
const session = readLiveSession();
|
|
1798
|
+
if (!session) {
|
|
1799
|
+
console.log(`\u2139\uFE0F [Live] No active session to stop.`);
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const { browser } = await connectToLiveSession();
|
|
1804
|
+
await browser.close().catch(() => {
|
|
1760
1805
|
});
|
|
1761
|
-
|
|
1806
|
+
} catch {
|
|
1762
1807
|
}
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1808
|
+
if (session.pid) {
|
|
1809
|
+
await new Promise((resolve) => {
|
|
1810
|
+
(0, import_tree_kill2.default)(session.pid, "SIGTERM", () => resolve());
|
|
1811
|
+
});
|
|
1767
1812
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1813
|
+
clearLiveSession();
|
|
1814
|
+
console.log(`\u{1F6D1} [Live] Session stopped and cleaned up.`);
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/plugins/live-controller/actions.ts
|
|
1819
|
+
async function clickCoords(page, x, y, options) {
|
|
1820
|
+
console.log(`\u{1F5B1}\uFE0F [LiveController] Clicking coordinates: (${x}, ${y})`);
|
|
1821
|
+
await page.mouse.click(x, y, options);
|
|
1822
|
+
await page.waitForTimeout(200);
|
|
1823
|
+
}
|
|
1824
|
+
async function snapLive(page, options) {
|
|
1825
|
+
const artifactsDir = getLiveArtifactsDir();
|
|
1826
|
+
const fileName = options?.name ? `${options.name}.png` : "current.png";
|
|
1827
|
+
const filePath = import_path12.default.join(artifactsDir, fileName);
|
|
1828
|
+
if (options?.selector) {
|
|
1829
|
+
const el = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
1830
|
+
await el.screenshot({ path: filePath });
|
|
1831
|
+
} else {
|
|
1832
|
+
await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
|
|
1774
1833
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1834
|
+
console.log(`\u{1F4F8} [LiveController] Snapshot saved: ${filePath}${options?.fullPage ? " (Full Page)" : ""}`);
|
|
1835
|
+
return filePath;
|
|
1836
|
+
}
|
|
1837
|
+
async function handleLiveCli(argv) {
|
|
1838
|
+
const action = argv[0]?.toLowerCase();
|
|
1839
|
+
switch (action) {
|
|
1840
|
+
case "start": {
|
|
1841
|
+
const urlArg = argv.find((a) => a.startsWith("--url="))?.split("=")[1] || "http://localhost:5173";
|
|
1842
|
+
const portArg = parseInt(argv.find((a) => a.startsWith("--port="))?.split("=")[1] || "9223", 10);
|
|
1843
|
+
const isHeaded = argv.includes("--headed");
|
|
1844
|
+
console.log(`\u{1F680} [Live] Starting background browser for ${urlArg} on CDP port ${portArg}...`);
|
|
1845
|
+
const browser = await chromium.launch({
|
|
1846
|
+
headless: !isHeaded,
|
|
1847
|
+
args: [`--remote-debugging-port=${portArg}`, "--no-sandbox"]
|
|
1848
|
+
});
|
|
1849
|
+
const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
|
|
1850
|
+
const page = await context.newPage();
|
|
1851
|
+
await page.goto(urlArg, { waitUntil: "domcontentloaded" });
|
|
1852
|
+
await page.waitForTimeout(500);
|
|
1853
|
+
saveLiveSession({
|
|
1854
|
+
port: portArg,
|
|
1855
|
+
url: urlArg,
|
|
1856
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1857
|
+
});
|
|
1858
|
+
await snapLive(page, { name: "current" });
|
|
1859
|
+
console.log(`\u2705 [Live] Session active! You can now send live commands:
|
|
1860
|
+
`);
|
|
1861
|
+
console.log(` npx agent-lens live click 450 120`);
|
|
1862
|
+
console.log(` npx agent-lens live type "input" "hello"`);
|
|
1863
|
+
console.log(` npx agent-lens live snap --full`);
|
|
1864
|
+
console.log(` npx agent-lens live stop
|
|
1865
|
+
`);
|
|
1866
|
+
return true;
|
|
1786
1867
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
const m = await jiti.import(mocksPath);
|
|
1803
|
-
globalMocks = m.default || m.mocks || [];
|
|
1804
|
-
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path8.default.basename(mocksPath)}`);
|
|
1805
|
-
break;
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
1868
|
+
case "click": {
|
|
1869
|
+
const { page, browser } = await connectToLiveSession();
|
|
1870
|
+
const firstArg = argv[1];
|
|
1871
|
+
const secondArg = argv[2];
|
|
1872
|
+
const x = parseInt(firstArg, 10);
|
|
1873
|
+
const y = parseInt(secondArg, 10);
|
|
1874
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
1875
|
+
await clickCoords(page, x, y);
|
|
1876
|
+
} else if (firstArg) {
|
|
1877
|
+
console.log(`\u{1F5B1}\uFE0F [Live] Clicking selector: "${firstArg}"`);
|
|
1878
|
+
await page.click(firstArg);
|
|
1879
|
+
} else {
|
|
1880
|
+
console.error(`\u274C Usage: npx agent-lens live click <x> <y> OR npx agent-lens live click <selector>`);
|
|
1881
|
+
await browser.close();
|
|
1882
|
+
return false;
|
|
1808
1883
|
}
|
|
1884
|
+
await snapLive(page, { name: "current" });
|
|
1885
|
+
return true;
|
|
1809
1886
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
continue;
|
|
1887
|
+
case "type": {
|
|
1888
|
+
const { page, browser } = await connectToLiveSession();
|
|
1889
|
+
const selector = argv[1];
|
|
1890
|
+
const text = argv[2];
|
|
1891
|
+
if (!selector || text === void 0) {
|
|
1892
|
+
console.error(`\u274C Usage: npx agent-lens live type <selector> <text>`);
|
|
1893
|
+
await browser.close();
|
|
1894
|
+
return false;
|
|
1819
1895
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1896
|
+
console.log(`\u2328\uFE0F [Live] Typing into "${selector}": "${text}"`);
|
|
1897
|
+
await page.fill(selector, text);
|
|
1898
|
+
await snapLive(page, { name: "current" });
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
case "snap": {
|
|
1902
|
+
const { page } = await connectToLiveSession();
|
|
1903
|
+
const isFull = argv.includes("--full");
|
|
1904
|
+
const nameArg = argv.find((a) => !a.startsWith("--") && a !== "snap");
|
|
1905
|
+
await snapLive(page, { name: nameArg || "current", fullPage: isFull });
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
case "stop": {
|
|
1909
|
+
return await stopLiveSession();
|
|
1910
|
+
}
|
|
1911
|
+
default: {
|
|
1912
|
+
console.log(`
|
|
1913
|
+
\u2139\uFE0F AgentLens Live Controller:
|
|
1914
|
+
npx agent-lens live start --url=<url> Start background live session
|
|
1915
|
+
npx agent-lens live click <x> <y> Click at pixel coordinates
|
|
1916
|
+
npx agent-lens live click <selector> Click CSS selector
|
|
1917
|
+
npx agent-lens live type <sel> <text> Fill input field
|
|
1918
|
+
npx agent-lens live snap [name] [--full] Capture current or full-page screenshot
|
|
1919
|
+
npx agent-lens live stop Close live browser and finish
|
|
1920
|
+
`);
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/app/cli.ts
|
|
1927
|
+
var rawArgs = process.argv.slice(2);
|
|
1928
|
+
if (rawArgs[0] === "live") {
|
|
1929
|
+
handleLiveCli(rawArgs.slice(1)).then((success) => process.exit(success ? 0 : 1)).catch((err) => {
|
|
1930
|
+
console.error("\u274C Live command failed:", err instanceof Error ? err.message : err);
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
});
|
|
1933
|
+
} else {
|
|
1934
|
+
const fileConfig = loadConfig();
|
|
1935
|
+
const { isSnapCommand, isInitCommand, options } = parseCliArgs(rawArgs, fileConfig);
|
|
1936
|
+
if (isInitCommand) {
|
|
1937
|
+
initScenarioTemplate();
|
|
1938
|
+
process.exit(0);
|
|
1939
|
+
}
|
|
1940
|
+
if (options.help) {
|
|
1941
|
+
printHelp();
|
|
1942
|
+
process.exit(0);
|
|
1943
|
+
}
|
|
1944
|
+
async function main() {
|
|
1945
|
+
if (isSnapCommand || options.url && !options.scenario && !options.all) {
|
|
1946
|
+
const snapSuccess = await runQuickSnap({
|
|
1823
1947
|
url: options.url,
|
|
1824
|
-
|
|
1948
|
+
start: options.start,
|
|
1825
1949
|
startCwd: options.startCwd,
|
|
1950
|
+
selector: options.selector,
|
|
1951
|
+
viewports: options.viewports,
|
|
1952
|
+
waitMs: options.waitMs,
|
|
1953
|
+
name: options.name,
|
|
1954
|
+
clean: options.clean,
|
|
1826
1955
|
cleanArtifacts: options.cleanArtifacts,
|
|
1827
|
-
port: options.port,
|
|
1828
1956
|
headed: options.headed,
|
|
1829
1957
|
detach: options.detach,
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1958
|
+
outDir: options.outDir,
|
|
1959
|
+
mode: options.mode,
|
|
1960
|
+
exe: options.exe,
|
|
1961
|
+
port: options.port,
|
|
1962
|
+
plugins: options.plugins,
|
|
1963
|
+
fullPage: options.fullPage
|
|
1836
1964
|
});
|
|
1837
|
-
|
|
1965
|
+
process.exit(snapSuccess ? 0 : 1);
|
|
1966
|
+
}
|
|
1967
|
+
if (!options.scenario && !options.all) {
|
|
1968
|
+
console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
|
|
1969
|
+
printHelp();
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
const scenariosDir = resolveScenariosDirectory(options.dir);
|
|
1973
|
+
const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
|
|
1974
|
+
if (scenarioPaths.length === 0) {
|
|
1975
|
+
console.error(`\u274C No scenarios found in ${scenariosDir}`);
|
|
1976
|
+
console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
const shouldBuild = options.build || fileConfig.autoBuild;
|
|
1980
|
+
if (shouldBuild) {
|
|
1981
|
+
const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
|
|
1982
|
+
console.log(`
|
|
1983
|
+
\u{1F528} [Build] Running build process: "${buildCmd}"...`);
|
|
1984
|
+
try {
|
|
1985
|
+
(0, import_child_process2.execSync)(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
console.error(`\u274C Build failed: ${buildCmd}`);
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
} else if (options.mode === "preview" && !options.url && !options.start) {
|
|
1991
|
+
console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
|
|
1992
|
+
}
|
|
1993
|
+
console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
|
|
1994
|
+
const jiti = (0, import_jiti2.createJiti)(process.cwd());
|
|
1995
|
+
let globalMocks = [];
|
|
1996
|
+
const mockCandidates = [
|
|
1997
|
+
import_path13.default.join(scenariosDir, "mocks.ts"),
|
|
1998
|
+
import_path13.default.join(scenariosDir, "mocks.js"),
|
|
1999
|
+
import_path13.default.join(process.cwd(), "mocks.ts"),
|
|
2000
|
+
import_path13.default.join(process.cwd(), "mocks.js")
|
|
2001
|
+
];
|
|
2002
|
+
for (const mocksPath of mockCandidates) {
|
|
2003
|
+
if (import_fs12.default.existsSync(mocksPath)) {
|
|
2004
|
+
try {
|
|
2005
|
+
const m = await jiti.import(mocksPath);
|
|
2006
|
+
globalMocks = m.default || m.mocks || [];
|
|
2007
|
+
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path13.default.basename(mocksPath)}`);
|
|
2008
|
+
break;
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (globalMocks.length > 0 && !options.plugins?.includes("mock-ipc")) {
|
|
2015
|
+
options.plugins = ["mock-ipc", ...options.plugins || []];
|
|
2016
|
+
}
|
|
2017
|
+
let allSuccess = true;
|
|
2018
|
+
for (const scenarioPath of scenarioPaths) {
|
|
2019
|
+
try {
|
|
2020
|
+
const scenarioModule = await jiti.import(scenarioPath);
|
|
2021
|
+
const scenario = scenarioModule.default || scenarioModule.scenario;
|
|
2022
|
+
if (!scenario || typeof scenario.run !== "function") {
|
|
2023
|
+
console.error(`\u26A0\uFE0F Skipped: file ${import_path13.default.basename(scenarioPath)} does not export a VisualScenario object by default.`);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
const scenarioPlugins = [...options.plugins || []];
|
|
2027
|
+
if (scenario.mockIpc && scenario.mockIpc.length > 0 && !scenarioPlugins.includes("mock-ipc")) {
|
|
2028
|
+
scenarioPlugins.push("mock-ipc");
|
|
2029
|
+
}
|
|
2030
|
+
const result = await runVisualScenario({
|
|
2031
|
+
scenario,
|
|
2032
|
+
targetMode: options.mode,
|
|
2033
|
+
url: options.url,
|
|
2034
|
+
startCommand: options.start,
|
|
2035
|
+
startCwd: options.startCwd,
|
|
2036
|
+
cleanArtifacts: options.cleanArtifacts,
|
|
2037
|
+
port: options.port,
|
|
2038
|
+
headed: options.headed,
|
|
2039
|
+
detach: options.detach,
|
|
2040
|
+
executablePath: options.exe,
|
|
2041
|
+
cleanPaths: options.clean,
|
|
2042
|
+
desktopEnv: fileConfig.env,
|
|
2043
|
+
wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
|
|
2044
|
+
artifactsRoot: options.outDir ? import_path13.default.resolve(process.cwd(), options.outDir) : void 0,
|
|
2045
|
+
globalMocks,
|
|
2046
|
+
plugins: scenarioPlugins
|
|
2047
|
+
});
|
|
2048
|
+
if (!result.success) {
|
|
2049
|
+
allSuccess = false;
|
|
2050
|
+
}
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
console.error(`\u274C Failed to load scenario ${import_path13.default.basename(scenarioPath)}:`, err);
|
|
1838
2053
|
allSuccess = false;
|
|
1839
2054
|
}
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
|
|
2055
|
+
}
|
|
2056
|
+
if (!allSuccess) {
|
|
2057
|
+
process.exit(1);
|
|
1843
2058
|
}
|
|
1844
2059
|
}
|
|
1845
|
-
|
|
2060
|
+
main().catch((err) => {
|
|
2061
|
+
console.error("Fatal error:", err);
|
|
1846
2062
|
process.exit(1);
|
|
1847
|
-
}
|
|
2063
|
+
});
|
|
1848
2064
|
}
|
|
1849
|
-
main().catch((err) => {
|
|
1850
|
-
console.error("Fatal error:", err);
|
|
1851
|
-
process.exit(1);
|
|
1852
|
-
});
|
|
1853
2065
|
//# sourceMappingURL=cli.js.map
|