@_deep4wee/agent-lens 1.1.0 → 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 +331 -227
- package/dist/cli.js +1068 -911
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1277 -1033
- 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 -178
- package/dist/index.d.ts +131 -178
- 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 +130 -43
- 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.mjs
CHANGED
|
@@ -6,6 +6,114 @@ var __commonJS = (cb, mod) => function __require() {
|
|
|
6
6
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// src/shared/lib/config.ts
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
function loadConfig(cwd = process.cwd()) {
|
|
13
|
+
const jsonConfigPath = path.join(cwd, "agent-lens.json");
|
|
14
|
+
if (fs.existsSync(jsonConfigPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(jsonConfigPath, "utf-8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
23
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(packageJsonPath, "utf-8");
|
|
26
|
+
const pkg = JSON.parse(raw);
|
|
27
|
+
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
28
|
+
return pkg.agentLens;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
function detectStartCwd(providedCwd) {
|
|
36
|
+
if (providedCwd) {
|
|
37
|
+
const resolved = path.resolve(process.cwd(), providedCwd);
|
|
38
|
+
if (fs.existsSync(resolved)) {
|
|
39
|
+
return providedCwd;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const rootPkgPath = path.join(process.cwd(), "package.json");
|
|
43
|
+
let rootHasDevScript = false;
|
|
44
|
+
if (fs.existsSync(rootPkgPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
47
|
+
rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (rootHasDevScript) {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
const candidatePkg = path.join(process.cwd(), candidate, "package.json");
|
|
57
|
+
if (fs.existsSync(candidatePkg)) {
|
|
58
|
+
return `./${candidate}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
function resolveWwwrootDir(customDir) {
|
|
64
|
+
if (customDir) {
|
|
65
|
+
return path.resolve(process.cwd(), customDir);
|
|
66
|
+
}
|
|
67
|
+
const candidates = [
|
|
68
|
+
"dist",
|
|
69
|
+
"build",
|
|
70
|
+
"out",
|
|
71
|
+
"wwwroot",
|
|
72
|
+
"Frontend/dist",
|
|
73
|
+
"frontend/dist",
|
|
74
|
+
"client/dist"
|
|
75
|
+
];
|
|
76
|
+
for (const c of candidates) {
|
|
77
|
+
const candidatePath = path.resolve(process.cwd(), c);
|
|
78
|
+
if (fs.existsSync(candidatePath) && fs.existsSync(path.join(candidatePath, "index.html"))) {
|
|
79
|
+
return candidatePath;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const c of candidates) {
|
|
83
|
+
const candidatePath = path.resolve(process.cwd(), c);
|
|
84
|
+
if (fs.existsSync(candidatePath)) {
|
|
85
|
+
return candidatePath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return path.resolve(process.cwd(), "dist");
|
|
89
|
+
}
|
|
90
|
+
var init_config = __esm({
|
|
91
|
+
"src/shared/lib/config.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/shared/api/plugin.ts
|
|
97
|
+
var init_plugin = __esm({
|
|
98
|
+
"src/shared/api/plugin.ts"() {
|
|
99
|
+
"use strict";
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/shared/types/console.ts
|
|
104
|
+
var init_console = __esm({
|
|
105
|
+
"src/shared/types/console.ts"() {
|
|
106
|
+
"use strict";
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// src/shared/types/ipc.ts
|
|
111
|
+
var init_ipc = __esm({
|
|
112
|
+
"src/shared/types/ipc.ts"() {
|
|
113
|
+
"use strict";
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
9
117
|
// src/shared/api/dsl.ts
|
|
10
118
|
function defineVisualTest(scenario) {
|
|
11
119
|
return scenario;
|
|
@@ -14,10 +122,13 @@ var VIEWPORT_PRESETS;
|
|
|
14
122
|
var init_dsl = __esm({
|
|
15
123
|
"src/shared/api/dsl.ts"() {
|
|
16
124
|
"use strict";
|
|
125
|
+
init_plugin();
|
|
126
|
+
init_console();
|
|
127
|
+
init_ipc();
|
|
17
128
|
VIEWPORT_PRESETS = {
|
|
18
|
-
/** Minimum supported window size
|
|
129
|
+
/** Minimum supported window size */
|
|
19
130
|
MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
|
|
20
|
-
/** Standard default window size
|
|
131
|
+
/** Standard default window size */
|
|
21
132
|
DEFAULT: { name: "default", width: 1200, height: 800 },
|
|
22
133
|
/** Wide screen for checking grids and tables */
|
|
23
134
|
WIDE: { name: "wide", width: 1600, height: 900 },
|
|
@@ -28,8 +139,8 @@ var init_dsl = __esm({
|
|
|
28
139
|
});
|
|
29
140
|
|
|
30
141
|
// src/features/capture/capture.ts
|
|
31
|
-
import
|
|
32
|
-
import
|
|
142
|
+
import fs2 from "fs";
|
|
143
|
+
import path2 from "path";
|
|
33
144
|
var CaptureEngine;
|
|
34
145
|
var init_capture = __esm({
|
|
35
146
|
"src/features/capture/capture.ts"() {
|
|
@@ -40,8 +151,8 @@ var init_capture = __esm({
|
|
|
40
151
|
recordedSnapshots = [];
|
|
41
152
|
constructor(outputDir) {
|
|
42
153
|
this.outputDir = outputDir;
|
|
43
|
-
if (!
|
|
44
|
-
|
|
154
|
+
if (!fs2.existsSync(this.outputDir)) {
|
|
155
|
+
fs2.mkdirSync(this.outputDir, { recursive: true });
|
|
45
156
|
}
|
|
46
157
|
}
|
|
47
158
|
getSnapshots() {
|
|
@@ -57,7 +168,7 @@ var init_capture = __esm({
|
|
|
57
168
|
const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
|
|
58
169
|
const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
59
170
|
const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
|
|
60
|
-
const filePath =
|
|
171
|
+
const filePath = path2.join(this.outputDir, fileName);
|
|
61
172
|
if (options?.selector) {
|
|
62
173
|
const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
63
174
|
await element.screenshot({ path: filePath });
|
|
@@ -93,7 +204,7 @@ var init_capture = __esm({
|
|
|
93
204
|
const elapsedMs = frame * interval;
|
|
94
205
|
const paddedFrame = String(frame + 1).padStart(2, "0");
|
|
95
206
|
const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
|
|
96
|
-
const filePath =
|
|
207
|
+
const filePath = path2.join(this.outputDir, fileName);
|
|
97
208
|
if (options.selector) {
|
|
98
209
|
const element = await page.$(options.selector);
|
|
99
210
|
if (element) {
|
|
@@ -130,8 +241,8 @@ var init_capture = __esm({
|
|
|
130
241
|
});
|
|
131
242
|
|
|
132
243
|
// src/features/reporter/reporter.ts
|
|
133
|
-
import
|
|
134
|
-
import
|
|
244
|
+
import fs3 from "fs";
|
|
245
|
+
import path3 from "path";
|
|
135
246
|
var VisualReporter;
|
|
136
247
|
var init_reporter = __esm({
|
|
137
248
|
"src/features/reporter/reporter.ts"() {
|
|
@@ -147,8 +258,8 @@ var init_reporter = __esm({
|
|
|
147
258
|
targetMode,
|
|
148
259
|
durationMs
|
|
149
260
|
} = data;
|
|
150
|
-
const manifestPath =
|
|
151
|
-
const reportPath =
|
|
261
|
+
const manifestPath = path3.join(outputDir, "manifest.json");
|
|
262
|
+
const reportPath = path3.join(outputDir, "report.md");
|
|
152
263
|
const manifest = {
|
|
153
264
|
scenarioId: scenario.id,
|
|
154
265
|
title: scenario.title,
|
|
@@ -163,7 +274,7 @@ var init_reporter = __esm({
|
|
|
163
274
|
errors: consoleErrors,
|
|
164
275
|
warnings: consoleWarnings
|
|
165
276
|
};
|
|
166
|
-
|
|
277
|
+
fs3.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
167
278
|
const rows = [];
|
|
168
279
|
const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
|
|
169
280
|
const burstGroups = /* @__PURE__ */ new Map();
|
|
@@ -256,6 +367,17 @@ ${err.stack}
|
|
|
256
367
|
if (consoleWarnings.length > 20) {
|
|
257
368
|
consoleSections += `
|
|
258
369
|
*...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
let pluginSections = "";
|
|
374
|
+
if (data.customSections && data.customSections.length > 0) {
|
|
375
|
+
for (const sec of data.customSections) {
|
|
376
|
+
pluginSections += `
|
|
377
|
+
## ${sec.title}
|
|
378
|
+
|
|
379
|
+
${sec.content}
|
|
380
|
+
|
|
259
381
|
`;
|
|
260
382
|
}
|
|
261
383
|
}
|
|
@@ -278,6 +400,7 @@ ${rows.join("\n")}
|
|
|
278
400
|
|
|
279
401
|
${burstSections}
|
|
280
402
|
${consoleSections}
|
|
403
|
+
${pluginSections}
|
|
281
404
|
---
|
|
282
405
|
|
|
283
406
|
## \u{1F4CB} AI Agent Verification Checklist:
|
|
@@ -291,7 +414,7 @@ ${consoleSections}
|
|
|
291
414
|
- [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
|
|
292
415
|
|
|
293
416
|
`;
|
|
294
|
-
|
|
417
|
+
fs3.writeFileSync(reportPath, reportContent, "utf-8");
|
|
295
418
|
return reportPath;
|
|
296
419
|
}
|
|
297
420
|
static getHealthStatus(errors, warnings) {
|
|
@@ -429,551 +552,154 @@ var init_playwrightLoader = __esm({
|
|
|
429
552
|
}
|
|
430
553
|
});
|
|
431
554
|
|
|
432
|
-
// src/shared/drivers/
|
|
433
|
-
import { spawn } from "child_process";
|
|
555
|
+
// src/shared/drivers/previewDriver.ts
|
|
434
556
|
import http from "http";
|
|
435
|
-
import
|
|
436
|
-
import
|
|
437
|
-
|
|
438
|
-
var
|
|
439
|
-
|
|
440
|
-
"src/shared/drivers/desktopDriver.ts"() {
|
|
557
|
+
import fs4 from "fs";
|
|
558
|
+
import path4 from "path";
|
|
559
|
+
var MIME_TYPES, PreviewDriver;
|
|
560
|
+
var init_previewDriver = __esm({
|
|
561
|
+
"src/shared/drivers/previewDriver.ts"() {
|
|
441
562
|
"use strict";
|
|
442
563
|
init_playwrightLoader();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
564
|
+
init_config();
|
|
565
|
+
MIME_TYPES = {
|
|
566
|
+
".html": "text/html; charset=utf-8",
|
|
567
|
+
".js": "application/javascript; charset=utf-8",
|
|
568
|
+
".css": "text/css; charset=utf-8",
|
|
569
|
+
".json": "application/json; charset=utf-8",
|
|
570
|
+
".png": "image/png",
|
|
571
|
+
".jpg": "image/jpeg",
|
|
572
|
+
".jpeg": "image/jpeg",
|
|
573
|
+
".gif": "image/gif",
|
|
574
|
+
".svg": "image/svg+xml",
|
|
575
|
+
".ico": "image/x-icon",
|
|
576
|
+
".woff": "font/woff",
|
|
577
|
+
".woff2": "font/woff2",
|
|
578
|
+
".ttf": "font/ttf"
|
|
579
|
+
};
|
|
580
|
+
PreviewDriver = class {
|
|
581
|
+
server = null;
|
|
451
582
|
browser = null;
|
|
452
583
|
context = null;
|
|
453
584
|
page = null;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
585
|
+
serverPort = 0;
|
|
586
|
+
options;
|
|
587
|
+
_baseUrl = "";
|
|
588
|
+
initialRouteMocks = [];
|
|
457
589
|
constructor(options) {
|
|
458
|
-
this.
|
|
459
|
-
this.
|
|
460
|
-
|
|
461
|
-
this.args = options?.args || [];
|
|
462
|
-
this.env = options?.env || {};
|
|
463
|
-
this.cwd = options?.cwd;
|
|
464
|
-
}
|
|
465
|
-
async isPortAvailable() {
|
|
466
|
-
return new Promise((resolve) => {
|
|
467
|
-
const req = http.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
|
|
468
|
-
resolve(res.statusCode === 200);
|
|
469
|
-
});
|
|
470
|
-
req.on("error", () => resolve(false));
|
|
471
|
-
req.setTimeout(800, () => {
|
|
472
|
-
req.destroy();
|
|
473
|
-
resolve(false);
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
async waitForPort(timeoutMs = 25e3) {
|
|
478
|
-
const startTime = Date.now();
|
|
479
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
480
|
-
if (this.processExited) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
`[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
|
|
483
|
-
Stderr: ${this.processStderr.trim() || "(none)"}`
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
if (await this.isPortAvailable()) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
590
|
+
this.options = options || {};
|
|
591
|
+
if (!this.options.url) {
|
|
592
|
+
this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
|
|
490
593
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
594
|
+
}
|
|
595
|
+
get baseUrl() {
|
|
596
|
+
return this._baseUrl;
|
|
495
597
|
}
|
|
496
598
|
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
497
|
-
|
|
498
|
-
if (!
|
|
499
|
-
if (!this.
|
|
500
|
-
throw new Error(
|
|
501
|
-
`App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
if (!this.executablePath) {
|
|
505
|
-
throw new Error(
|
|
506
|
-
`[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
const resolvedExe = path3.resolve(process.cwd(), this.executablePath);
|
|
510
|
-
if (!fs3.existsSync(resolvedExe)) {
|
|
511
|
-
throw new Error(
|
|
512
|
-
`[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
|
|
516
|
-
const finalArgs = [...this.args];
|
|
517
|
-
const hasDebugPort = finalArgs.some((a) => a.startsWith("--remote-debugging-port="));
|
|
518
|
-
if (!hasDebugPort) {
|
|
519
|
-
finalArgs.push(`--remote-debugging-port=${this.port}`);
|
|
599
|
+
let targetUrl = this.options.url;
|
|
600
|
+
if (!targetUrl) {
|
|
601
|
+
if (!fs4.existsSync(this.options.wwwrootDir)) {
|
|
602
|
+
throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
|
|
520
603
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
604
|
+
await new Promise((resolve, reject) => {
|
|
605
|
+
this.server = http.createServer((req, res) => {
|
|
606
|
+
let reqUrl = req.url?.split("?")[0] || "/";
|
|
607
|
+
if (reqUrl === "/") reqUrl = "/index.html";
|
|
608
|
+
let safePath = path4.normalize(path4.join(this.options.wwwrootDir, reqUrl));
|
|
609
|
+
if (!safePath.startsWith(this.options.wwwrootDir)) {
|
|
610
|
+
res.writeHead(403);
|
|
611
|
+
return res.end("Forbidden");
|
|
612
|
+
}
|
|
613
|
+
if (!fs4.existsSync(safePath) || fs4.statSync(safePath).isDirectory()) {
|
|
614
|
+
safePath = path4.join(this.options.wwwrootDir, "index.html");
|
|
615
|
+
}
|
|
616
|
+
const ext = path4.extname(safePath).toLowerCase();
|
|
617
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
618
|
+
try {
|
|
619
|
+
const content = fs4.readFileSync(safePath);
|
|
620
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
621
|
+
res.end(content);
|
|
622
|
+
} catch (e) {
|
|
623
|
+
res.writeHead(500);
|
|
624
|
+
res.end(`Server error: ${e.message}`);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
this.server.listen(0, "127.0.0.1", () => {
|
|
628
|
+
const addr = this.server?.address();
|
|
629
|
+
if (typeof addr === "object" && addr?.port) {
|
|
630
|
+
this.serverPort = addr.port;
|
|
631
|
+
this._baseUrl = `http://127.0.0.1:${this.serverPort}`;
|
|
632
|
+
resolve();
|
|
633
|
+
} else {
|
|
634
|
+
reject(new Error("Failed to acquire port for static preview server"));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
554
637
|
});
|
|
555
|
-
|
|
556
|
-
await this.waitForPort();
|
|
638
|
+
targetUrl = `${this._baseUrl}/index.html`;
|
|
557
639
|
} else {
|
|
558
|
-
|
|
640
|
+
this._baseUrl = targetUrl;
|
|
641
|
+
console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
|
|
559
642
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
643
|
+
this.browser = await chromium.launch({
|
|
644
|
+
headless: !this.options.headed,
|
|
645
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
646
|
+
});
|
|
647
|
+
this.context = await this.browser.newContext({
|
|
648
|
+
viewport: initialViewport,
|
|
649
|
+
deviceScaleFactor: 1
|
|
650
|
+
});
|
|
651
|
+
this.page = await this.context.newPage();
|
|
652
|
+
if (this.initialRouteMocks.length > 0) {
|
|
653
|
+
for (const entry of this.initialRouteMocks) {
|
|
654
|
+
await this.addRouteMock(entry);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
|
|
658
|
+
return { page: this.page, context: this.context, browser: this.browser };
|
|
659
|
+
}
|
|
660
|
+
async addRouteMock(entry) {
|
|
661
|
+
if (!this.page) {
|
|
662
|
+
this.initialRouteMocks.push(entry);
|
|
663
|
+
return;
|
|
569
664
|
}
|
|
570
|
-
|
|
571
|
-
|
|
665
|
+
await this.page.route(entry.url, async (route) => {
|
|
666
|
+
const req = route.request();
|
|
667
|
+
if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
|
|
668
|
+
return route.continue();
|
|
669
|
+
}
|
|
670
|
+
if (entry.delayMs) {
|
|
671
|
+
await new Promise((r) => setTimeout(r, entry.delayMs));
|
|
672
|
+
}
|
|
673
|
+
const isJson = typeof entry.body === "object" && entry.body !== null;
|
|
674
|
+
await route.fulfill({
|
|
675
|
+
status: entry.status ?? 200,
|
|
676
|
+
contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
|
|
677
|
+
body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
|
|
678
|
+
headers: entry.headers
|
|
572
679
|
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
async setupRouteMocks(routes) {
|
|
683
|
+
for (const r of routes) {
|
|
684
|
+
await this.addRouteMock(r);
|
|
573
685
|
}
|
|
574
|
-
return { page: this.page, context: this.context, browser: this.browser };
|
|
575
686
|
}
|
|
576
687
|
async stop() {
|
|
688
|
+
if (this.context) {
|
|
689
|
+
await this.context.close().catch(() => {
|
|
690
|
+
});
|
|
691
|
+
this.context = null;
|
|
692
|
+
}
|
|
577
693
|
if (this.browser) {
|
|
578
694
|
await this.browser.close().catch(() => {
|
|
579
695
|
});
|
|
580
696
|
this.browser = null;
|
|
581
697
|
}
|
|
582
|
-
if (this.
|
|
583
|
-
console.log("[DesktopDriver] Terminating spawned desktop process tree...");
|
|
584
|
-
const pid = this.childProcess.pid;
|
|
698
|
+
if (this.server) {
|
|
585
699
|
await new Promise((resolve) => {
|
|
586
|
-
|
|
700
|
+
this.server?.close(() => resolve());
|
|
587
701
|
});
|
|
588
|
-
this.
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// src/features/mock-ipc/mockIpc.ts
|
|
596
|
-
function generateMockIpcScript(registry) {
|
|
597
|
-
const mocksJson = JSON.stringify(registry.toSerializable());
|
|
598
|
-
return `
|
|
599
|
-
(() => {
|
|
600
|
-
const __mockTable = ${mocksJson};
|
|
601
|
-
|
|
602
|
-
window.__visualRunnerMocks = __mockTable;
|
|
603
|
-
|
|
604
|
-
// Generic IPC mock bridge for modern web applications
|
|
605
|
-
window.__mockIpc = {
|
|
606
|
-
invoke: (action, payload) => {
|
|
607
|
-
return new Promise((resolve, reject) => {
|
|
608
|
-
const mock = window.__visualRunnerMocks[action];
|
|
609
|
-
|
|
610
|
-
if (mock) {
|
|
611
|
-
setTimeout(() => {
|
|
612
|
-
if (mock.type === 'ERROR') {
|
|
613
|
-
reject(new Error(mock.data));
|
|
614
|
-
} else {
|
|
615
|
-
resolve(mock.data);
|
|
616
|
-
}
|
|
617
|
-
}, mock.delayMs || 20);
|
|
618
|
-
} else {
|
|
619
|
-
console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
|
|
620
|
-
setTimeout(() => resolve(null), 20);
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
// Safe fallback bridge for hybrid webviews (Photino / CEF / WebView2)
|
|
627
|
-
try {
|
|
628
|
-
if (!window.external) {
|
|
629
|
-
(window as any).external = {};
|
|
630
|
-
}
|
|
631
|
-
(window.external as any).sendMessage = (msg) => {
|
|
632
|
-
try {
|
|
633
|
-
const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
634
|
-
const action = parsed.Action || parsed.action;
|
|
635
|
-
const id = parsed.Id || parsed.id;
|
|
636
|
-
const mock = (window as any).__visualRunnerMocks[action];
|
|
637
|
-
|
|
638
|
-
if (mock) {
|
|
639
|
-
const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
|
|
640
|
-
setTimeout(() => {
|
|
641
|
-
const cb = (window as any).__mockCallback;
|
|
642
|
-
if (typeof cb === 'function') cb(JSON.stringify(response));
|
|
643
|
-
}, mock.delayMs || 20);
|
|
644
|
-
} else {
|
|
645
|
-
setTimeout(() => {
|
|
646
|
-
const cb = (window as any).__mockCallback;
|
|
647
|
-
if (typeof cb === 'function') cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
|
|
648
|
-
}, 20);
|
|
649
|
-
}
|
|
650
|
-
} catch (e) {
|
|
651
|
-
console.error('[Mock IPC] Failed to process message:', e);
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
(window.external as any).receiveMessage = (callback) => {
|
|
656
|
-
(window as any).__mockCallback = callback;
|
|
657
|
-
};
|
|
658
|
-
} catch {
|
|
659
|
-
// Ignored if window.external is read-only in strict Chromium sandboxes
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys((window as any).__visualRunnerMocks).length, 'mocked actions');
|
|
663
|
-
})();
|
|
664
|
-
|
|
665
|
-
`;
|
|
666
|
-
}
|
|
667
|
-
var MockIpcRegistry;
|
|
668
|
-
var init_mockIpc = __esm({
|
|
669
|
-
"src/features/mock-ipc/mockIpc.ts"() {
|
|
670
|
-
"use strict";
|
|
671
|
-
MockIpcRegistry = class {
|
|
672
|
-
mocks = /* @__PURE__ */ new Map();
|
|
673
|
-
set(action, data, options) {
|
|
674
|
-
this.mocks.set(action, {
|
|
675
|
-
action,
|
|
676
|
-
data,
|
|
677
|
-
type: options?.type ?? "SUCCESS",
|
|
678
|
-
delayMs: options?.delayMs ?? 20
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
setBatch(entries) {
|
|
682
|
-
for (const entry of entries) {
|
|
683
|
-
this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
remove(action) {
|
|
687
|
-
this.mocks.delete(action);
|
|
688
|
-
}
|
|
689
|
-
clear() {
|
|
690
|
-
this.mocks.clear();
|
|
691
|
-
}
|
|
692
|
-
get(action) {
|
|
693
|
-
return this.mocks.get(action) ?? null;
|
|
694
|
-
}
|
|
695
|
-
toSerializable() {
|
|
696
|
-
const result = {};
|
|
697
|
-
for (const [action, entry] of this.mocks.entries()) {
|
|
698
|
-
result[action] = {
|
|
699
|
-
data: entry.data,
|
|
700
|
-
type: entry.type ?? "SUCCESS",
|
|
701
|
-
delayMs: entry.delayMs ?? 20
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
return result;
|
|
705
|
-
}
|
|
706
|
-
get size() {
|
|
707
|
-
return this.mocks.size;
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// src/shared/lib/config.ts
|
|
714
|
-
import path4 from "path";
|
|
715
|
-
import fs4 from "fs";
|
|
716
|
-
function loadConfig(cwd = process.cwd()) {
|
|
717
|
-
const jsonConfigPath = path4.join(cwd, "agent-lens.json");
|
|
718
|
-
if (fs4.existsSync(jsonConfigPath)) {
|
|
719
|
-
try {
|
|
720
|
-
const raw = fs4.readFileSync(jsonConfigPath, "utf-8");
|
|
721
|
-
return JSON.parse(raw);
|
|
722
|
-
} catch (e) {
|
|
723
|
-
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const packageJsonPath = path4.join(cwd, "package.json");
|
|
727
|
-
if (fs4.existsSync(packageJsonPath)) {
|
|
728
|
-
try {
|
|
729
|
-
const raw = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
730
|
-
const pkg = JSON.parse(raw);
|
|
731
|
-
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
732
|
-
return pkg.agentLens;
|
|
733
|
-
}
|
|
734
|
-
} catch {
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return {};
|
|
738
|
-
}
|
|
739
|
-
function detectStartCwd(providedCwd) {
|
|
740
|
-
if (providedCwd) {
|
|
741
|
-
const resolved = path4.resolve(process.cwd(), providedCwd);
|
|
742
|
-
if (fs4.existsSync(resolved)) {
|
|
743
|
-
return providedCwd;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
const rootPkgPath = path4.join(process.cwd(), "package.json");
|
|
747
|
-
let rootHasDevScript = false;
|
|
748
|
-
if (fs4.existsSync(rootPkgPath)) {
|
|
749
|
-
try {
|
|
750
|
-
const rootPkg = JSON.parse(fs4.readFileSync(rootPkgPath, "utf-8"));
|
|
751
|
-
rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
|
|
752
|
-
} catch {
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (rootHasDevScript) {
|
|
756
|
-
return void 0;
|
|
757
|
-
}
|
|
758
|
-
const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
|
|
759
|
-
for (const candidate of candidates) {
|
|
760
|
-
const candidatePkg = path4.join(process.cwd(), candidate, "package.json");
|
|
761
|
-
if (fs4.existsSync(candidatePkg)) {
|
|
762
|
-
return `./${candidate}`;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return void 0;
|
|
766
|
-
}
|
|
767
|
-
function resolveWwwrootDir(customDir) {
|
|
768
|
-
if (customDir) {
|
|
769
|
-
return path4.resolve(process.cwd(), customDir);
|
|
770
|
-
}
|
|
771
|
-
const candidates = [
|
|
772
|
-
"dist",
|
|
773
|
-
"build",
|
|
774
|
-
"out",
|
|
775
|
-
"wwwroot",
|
|
776
|
-
"Frontend/dist",
|
|
777
|
-
"frontend/dist",
|
|
778
|
-
"client/dist"
|
|
779
|
-
];
|
|
780
|
-
for (const c of candidates) {
|
|
781
|
-
const candidatePath = path4.resolve(process.cwd(), c);
|
|
782
|
-
if (fs4.existsSync(candidatePath) && fs4.existsSync(path4.join(candidatePath, "index.html"))) {
|
|
783
|
-
return candidatePath;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
for (const c of candidates) {
|
|
787
|
-
const candidatePath = path4.resolve(process.cwd(), c);
|
|
788
|
-
if (fs4.existsSync(candidatePath)) {
|
|
789
|
-
return candidatePath;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
return path4.resolve(process.cwd(), "dist");
|
|
793
|
-
}
|
|
794
|
-
var init_config = __esm({
|
|
795
|
-
"src/shared/lib/config.ts"() {
|
|
796
|
-
"use strict";
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
// src/shared/drivers/previewDriver.ts
|
|
801
|
-
import http2 from "http";
|
|
802
|
-
import fs5 from "fs";
|
|
803
|
-
import path5 from "path";
|
|
804
|
-
var MIME_TYPES, PreviewDriver;
|
|
805
|
-
var init_previewDriver = __esm({
|
|
806
|
-
"src/shared/drivers/previewDriver.ts"() {
|
|
807
|
-
"use strict";
|
|
808
|
-
init_playwrightLoader();
|
|
809
|
-
init_mockIpc();
|
|
810
|
-
init_config();
|
|
811
|
-
MIME_TYPES = {
|
|
812
|
-
".html": "text/html; charset=utf-8",
|
|
813
|
-
".js": "application/javascript; charset=utf-8",
|
|
814
|
-
".css": "text/css; charset=utf-8",
|
|
815
|
-
".json": "application/json; charset=utf-8",
|
|
816
|
-
".png": "image/png",
|
|
817
|
-
".jpg": "image/jpeg",
|
|
818
|
-
".jpeg": "image/jpeg",
|
|
819
|
-
".gif": "image/gif",
|
|
820
|
-
".svg": "image/svg+xml",
|
|
821
|
-
".ico": "image/x-icon",
|
|
822
|
-
".woff": "font/woff",
|
|
823
|
-
".woff2": "font/woff2",
|
|
824
|
-
".ttf": "font/ttf"
|
|
825
|
-
};
|
|
826
|
-
PreviewDriver = class {
|
|
827
|
-
server = null;
|
|
828
|
-
browser = null;
|
|
829
|
-
context = null;
|
|
830
|
-
page = null;
|
|
831
|
-
serverPort = 0;
|
|
832
|
-
options;
|
|
833
|
-
_baseUrl = "";
|
|
834
|
-
initialRouteMocks = [];
|
|
835
|
-
mockRegistry;
|
|
836
|
-
constructor(options) {
|
|
837
|
-
this.options = options || {};
|
|
838
|
-
if (!this.options.url) {
|
|
839
|
-
this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
|
|
840
|
-
}
|
|
841
|
-
this.mockRegistry = new MockIpcRegistry();
|
|
842
|
-
}
|
|
843
|
-
get baseUrl() {
|
|
844
|
-
return this._baseUrl;
|
|
845
|
-
}
|
|
846
|
-
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
847
|
-
let targetUrl = this.options.url;
|
|
848
|
-
if (!targetUrl) {
|
|
849
|
-
if (!fs5.existsSync(this.options.wwwrootDir)) {
|
|
850
|
-
throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
|
|
851
|
-
}
|
|
852
|
-
await new Promise((resolve, reject) => {
|
|
853
|
-
this.server = http2.createServer((req, res) => {
|
|
854
|
-
let reqUrl = req.url?.split("?")[0] || "/";
|
|
855
|
-
if (reqUrl === "/") reqUrl = "/index.html";
|
|
856
|
-
let safePath = path5.normalize(path5.join(this.options.wwwrootDir, reqUrl));
|
|
857
|
-
if (!safePath.startsWith(this.options.wwwrootDir)) {
|
|
858
|
-
res.writeHead(403);
|
|
859
|
-
return res.end("Forbidden");
|
|
860
|
-
}
|
|
861
|
-
if (!fs5.existsSync(safePath) || fs5.statSync(safePath).isDirectory()) {
|
|
862
|
-
safePath = path5.join(this.options.wwwrootDir, "index.html");
|
|
863
|
-
}
|
|
864
|
-
const ext = path5.extname(safePath).toLowerCase();
|
|
865
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
866
|
-
try {
|
|
867
|
-
const content = fs5.readFileSync(safePath);
|
|
868
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
869
|
-
res.end(content);
|
|
870
|
-
} catch (e) {
|
|
871
|
-
res.writeHead(500);
|
|
872
|
-
res.end(`Server error: ${e.message}`);
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
this.server.listen(0, "127.0.0.1", () => {
|
|
876
|
-
const addr = this.server?.address();
|
|
877
|
-
if (typeof addr === "object" && addr?.port) {
|
|
878
|
-
this.serverPort = addr.port;
|
|
879
|
-
this._baseUrl = `http://127.0.0.1:${this.serverPort}`;
|
|
880
|
-
resolve();
|
|
881
|
-
} else {
|
|
882
|
-
reject(new Error("Failed to acquire port for static preview server"));
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
});
|
|
886
|
-
targetUrl = `${this._baseUrl}/index.html`;
|
|
887
|
-
} else {
|
|
888
|
-
this._baseUrl = targetUrl;
|
|
889
|
-
console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
|
|
890
|
-
}
|
|
891
|
-
this.browser = await chromium.launch({
|
|
892
|
-
headless: !this.options.headed,
|
|
893
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
894
|
-
});
|
|
895
|
-
this.context = await this.browser.newContext({
|
|
896
|
-
viewport: initialViewport,
|
|
897
|
-
deviceScaleFactor: 1
|
|
898
|
-
});
|
|
899
|
-
if (this.mockRegistry.size > 0) {
|
|
900
|
-
const mockScript = generateMockIpcScript(this.mockRegistry);
|
|
901
|
-
await this.context.addInitScript(mockScript);
|
|
902
|
-
}
|
|
903
|
-
this.page = await this.context.newPage();
|
|
904
|
-
if (this.initialRouteMocks.length > 0) {
|
|
905
|
-
for (const entry of this.initialRouteMocks) {
|
|
906
|
-
await this.addRouteMock(entry);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
|
|
910
|
-
return { page: this.page, context: this.context, browser: this.browser };
|
|
911
|
-
}
|
|
912
|
-
async addRouteMock(entry) {
|
|
913
|
-
if (!this.page) {
|
|
914
|
-
this.initialRouteMocks.push(entry);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
await this.page.route(entry.url, async (route) => {
|
|
918
|
-
const req = route.request();
|
|
919
|
-
if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
|
|
920
|
-
return route.continue();
|
|
921
|
-
}
|
|
922
|
-
if (entry.delayMs) {
|
|
923
|
-
await new Promise((r) => setTimeout(r, entry.delayMs));
|
|
924
|
-
}
|
|
925
|
-
const isJson = typeof entry.body === "object" && entry.body !== null;
|
|
926
|
-
await route.fulfill({
|
|
927
|
-
status: entry.status ?? 200,
|
|
928
|
-
contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
|
|
929
|
-
body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
|
|
930
|
-
headers: entry.headers
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
async setupRouteMocks(routes) {
|
|
935
|
-
for (const r of routes) {
|
|
936
|
-
await this.addRouteMock(r);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
async updateMockIpc(action, data, options) {
|
|
940
|
-
if (!this.page) {
|
|
941
|
-
throw new Error("PreviewDriver not started. Call start() first.");
|
|
942
|
-
}
|
|
943
|
-
this.mockRegistry.set(action, data, options);
|
|
944
|
-
await this.page.evaluate(
|
|
945
|
-
({ action: action2, mock }) => {
|
|
946
|
-
if (!window.__visualRunnerMocks) {
|
|
947
|
-
window.__visualRunnerMocks = {};
|
|
948
|
-
}
|
|
949
|
-
window.__visualRunnerMocks[action2] = mock;
|
|
950
|
-
},
|
|
951
|
-
{
|
|
952
|
-
action,
|
|
953
|
-
mock: {
|
|
954
|
-
data,
|
|
955
|
-
type: options?.type ?? "SUCCESS",
|
|
956
|
-
delayMs: options?.delayMs ?? 20
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
async stop() {
|
|
962
|
-
if (this.context) {
|
|
963
|
-
await this.context.close().catch(() => {
|
|
964
|
-
});
|
|
965
|
-
this.context = null;
|
|
966
|
-
}
|
|
967
|
-
if (this.browser) {
|
|
968
|
-
await this.browser.close().catch(() => {
|
|
969
|
-
});
|
|
970
|
-
this.browser = null;
|
|
971
|
-
}
|
|
972
|
-
if (this.server) {
|
|
973
|
-
await new Promise((resolve) => {
|
|
974
|
-
this.server?.close(() => resolve());
|
|
975
|
-
});
|
|
976
|
-
this.server = null;
|
|
702
|
+
this.server = null;
|
|
977
703
|
}
|
|
978
704
|
}
|
|
979
705
|
};
|
|
@@ -981,8 +707,8 @@ var init_previewDriver = __esm({
|
|
|
981
707
|
});
|
|
982
708
|
|
|
983
709
|
// src/shared/lib/processManager.ts
|
|
984
|
-
import { spawn
|
|
985
|
-
import
|
|
710
|
+
import { spawn } from "child_process";
|
|
711
|
+
import treeKill from "tree-kill";
|
|
986
712
|
var ProcessManager;
|
|
987
713
|
var init_processManager = __esm({
|
|
988
714
|
"src/shared/lib/processManager.ts"() {
|
|
@@ -993,7 +719,7 @@ var init_processManager = __esm({
|
|
|
993
719
|
async start(command, options) {
|
|
994
720
|
const cwd = options?.cwd || process.cwd();
|
|
995
721
|
console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
|
|
996
|
-
this.childProcess =
|
|
722
|
+
this.childProcess = spawn(command, {
|
|
997
723
|
cwd,
|
|
998
724
|
env: { ...process.env, ...options?.env },
|
|
999
725
|
shell: true,
|
|
@@ -1044,10 +770,10 @@ var init_processManager = __esm({
|
|
|
1044
770
|
const pid = this.childProcess.pid;
|
|
1045
771
|
console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
|
|
1046
772
|
await new Promise((resolve) => {
|
|
1047
|
-
|
|
773
|
+
treeKill(pid, "SIGTERM", (err) => {
|
|
1048
774
|
if (err) {
|
|
1049
775
|
try {
|
|
1050
|
-
|
|
776
|
+
treeKill(pid, "SIGKILL");
|
|
1051
777
|
} catch {
|
|
1052
778
|
}
|
|
1053
779
|
}
|
|
@@ -1056,37 +782,465 @@ var init_processManager = __esm({
|
|
|
1056
782
|
});
|
|
1057
783
|
this.childProcess = null;
|
|
1058
784
|
}
|
|
1059
|
-
};
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// src/shared/lib/pluginLoader.ts
|
|
790
|
+
import path5 from "path";
|
|
791
|
+
import fs5 from "fs";
|
|
792
|
+
import { createJiti } from "jiti";
|
|
793
|
+
function findPackageRoot() {
|
|
794
|
+
let cur = __dirname;
|
|
795
|
+
while (cur !== path5.dirname(cur)) {
|
|
796
|
+
const pkgPath = path5.join(cur, "package.json");
|
|
797
|
+
if (fs5.existsSync(pkgPath)) {
|
|
798
|
+
try {
|
|
799
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
800
|
+
if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
|
|
801
|
+
return cur;
|
|
802
|
+
}
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
cur = path5.dirname(cur);
|
|
807
|
+
}
|
|
808
|
+
return path5.resolve(__dirname, "..");
|
|
809
|
+
}
|
|
810
|
+
var PluginManager;
|
|
811
|
+
var init_pluginLoader = __esm({
|
|
812
|
+
"src/shared/lib/pluginLoader.ts"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
PluginManager = class {
|
|
815
|
+
plugins = [];
|
|
816
|
+
jiti = createJiti(process.cwd());
|
|
817
|
+
register(plugin) {
|
|
818
|
+
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this.plugins.push(plugin);
|
|
822
|
+
console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
|
|
823
|
+
}
|
|
824
|
+
async load(pluginSpec) {
|
|
825
|
+
if (typeof pluginSpec === "object" && pluginSpec !== null) {
|
|
826
|
+
this.register(pluginSpec);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const pluginNameOrPath = pluginSpec.trim();
|
|
830
|
+
if (!pluginNameOrPath) return;
|
|
831
|
+
if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
|
|
832
|
+
const resolvedPath = path5.resolve(process.cwd(), pluginNameOrPath);
|
|
833
|
+
await this.loadFromFile(resolvedPath);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const localCandidates = [
|
|
837
|
+
path5.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
|
|
838
|
+
path5.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
|
|
839
|
+
path5.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
|
|
840
|
+
path5.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
|
|
841
|
+
path5.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
|
|
842
|
+
path5.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
|
|
843
|
+
path5.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
|
|
844
|
+
path5.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
|
|
845
|
+
];
|
|
846
|
+
for (const candidate of localCandidates) {
|
|
847
|
+
if (fs5.existsSync(candidate)) {
|
|
848
|
+
await this.loadFromFile(candidate);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const pkgRoot = findPackageRoot();
|
|
853
|
+
const builtInCandidates = [
|
|
854
|
+
path5.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
|
|
855
|
+
path5.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
|
|
856
|
+
path5.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
|
|
857
|
+
path5.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
|
|
858
|
+
path5.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
|
|
859
|
+
path5.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
|
|
860
|
+
path5.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
|
|
861
|
+
path5.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
|
|
862
|
+
path5.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
|
|
863
|
+
];
|
|
864
|
+
for (const builtIn of builtInCandidates) {
|
|
865
|
+
if (fs5.existsSync(builtIn)) {
|
|
866
|
+
await this.loadFromFile(builtIn);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const mod = await this.jiti.import(pluginNameOrPath);
|
|
872
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
873
|
+
if (plugin && plugin.name) {
|
|
874
|
+
this.register(plugin);
|
|
875
|
+
} else {
|
|
876
|
+
console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
|
|
877
|
+
}
|
|
878
|
+
} catch (err) {
|
|
879
|
+
console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async loadFromFile(filePath) {
|
|
883
|
+
if (!fs5.existsSync(filePath)) {
|
|
884
|
+
console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const mod = await this.jiti.import(filePath);
|
|
889
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
890
|
+
if (plugin && plugin.name) {
|
|
891
|
+
this.register(plugin);
|
|
892
|
+
} else {
|
|
893
|
+
console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
|
|
894
|
+
}
|
|
895
|
+
} catch (err) {
|
|
896
|
+
console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async loadAll(specs) {
|
|
900
|
+
for (const spec of specs) {
|
|
901
|
+
await this.load(spec);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async runSetup(hookContext) {
|
|
905
|
+
for (const p of this.plugins) {
|
|
906
|
+
if (p.setup) await p.setup(hookContext);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async launchSession(options, hookContext) {
|
|
910
|
+
for (const p of this.plugins) {
|
|
911
|
+
if (p.launchSession) {
|
|
912
|
+
const session = await p.launchSession(options, hookContext);
|
|
913
|
+
if (session) {
|
|
914
|
+
return session;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
async runOnContextCreated(context, hookContext) {
|
|
921
|
+
for (const p of this.plugins) {
|
|
922
|
+
if (p.onContextCreated) await p.onContextCreated(context, hookContext);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async runOnPageCreated(page, context, hookContext) {
|
|
926
|
+
for (const p of this.plugins) {
|
|
927
|
+
if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async extendContext(ctx, page, hookContext) {
|
|
931
|
+
for (const p of this.plugins) {
|
|
932
|
+
if (p.extendContext) {
|
|
933
|
+
const extensions = await p.extendContext(ctx, page, hookContext);
|
|
934
|
+
if (extensions && typeof extensions === "object") {
|
|
935
|
+
Object.assign(ctx, extensions);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async runOnAfterRun(reportData, hookContext) {
|
|
941
|
+
for (const p of this.plugins) {
|
|
942
|
+
if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async runTeardown(hookContext) {
|
|
946
|
+
for (const p of this.plugins) {
|
|
947
|
+
if (p.teardown) {
|
|
948
|
+
try {
|
|
949
|
+
await p.teardown(hookContext);
|
|
950
|
+
} catch (e) {
|
|
951
|
+
console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
get loadedPlugins() {
|
|
957
|
+
return [...this.plugins];
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// src/features/runner/lib/navigation.ts
|
|
964
|
+
function createNavigator(page, baseUrl, targetUrl) {
|
|
965
|
+
return async (route) => {
|
|
966
|
+
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
967
|
+
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
968
|
+
await page.waitForTimeout(300);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (route.startsWith("#")) {
|
|
972
|
+
await page.evaluate((r) => {
|
|
973
|
+
window.location.hash = r;
|
|
974
|
+
}, route);
|
|
975
|
+
await page.waitForTimeout(300);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (baseUrl && targetUrl) {
|
|
979
|
+
try {
|
|
980
|
+
const fullUrl = new URL(route, baseUrl).toString();
|
|
981
|
+
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
982
|
+
await page.waitForTimeout(300);
|
|
983
|
+
return;
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
988
|
+
await page.evaluate((r) => {
|
|
989
|
+
if (window.location.hash !== void 0) {
|
|
990
|
+
window.location.hash = r;
|
|
991
|
+
}
|
|
992
|
+
}, routePath);
|
|
993
|
+
await page.waitForTimeout(300);
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
var init_navigation = __esm({
|
|
997
|
+
"src/features/runner/lib/navigation.ts"() {
|
|
998
|
+
"use strict";
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// src/features/runner/lib/artifactsSync.ts
|
|
1003
|
+
import fs6 from "fs";
|
|
1004
|
+
import path6 from "path";
|
|
1005
|
+
function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
|
|
1006
|
+
try {
|
|
1007
|
+
const latestDir = path6.join(artifactsRoot, "latest");
|
|
1008
|
+
if (fs6.existsSync(latestDir)) {
|
|
1009
|
+
fs6.rmSync(latestDir, { recursive: true, force: true });
|
|
1010
|
+
}
|
|
1011
|
+
fs6.mkdirSync(latestDir, { recursive: true });
|
|
1012
|
+
if (fs6.existsSync(reportPath)) {
|
|
1013
|
+
fs6.copyFileSync(reportPath, path6.join(latestDir, "report.md"));
|
|
1014
|
+
}
|
|
1015
|
+
const manifestSrc = path6.join(scenarioArtifactsDir, "manifest.json");
|
|
1016
|
+
if (fs6.existsSync(manifestSrc)) {
|
|
1017
|
+
fs6.copyFileSync(manifestSrc, path6.join(latestDir, "manifest.json"));
|
|
1018
|
+
}
|
|
1019
|
+
for (const snap of snapshots) {
|
|
1020
|
+
if (snap.filePath && fs6.existsSync(snap.filePath)) {
|
|
1021
|
+
fs6.copyFileSync(snap.filePath, path6.join(latestDir, snap.fileName));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return path6.join(latestDir, "report.md");
|
|
1025
|
+
} catch {
|
|
1026
|
+
return void 0;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
var init_artifactsSync = __esm({
|
|
1030
|
+
"src/features/runner/lib/artifactsSync.ts"() {
|
|
1031
|
+
"use strict";
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// src/features/runner/lib/contextBuilder.ts
|
|
1036
|
+
function buildTestContext(options) {
|
|
1037
|
+
const {
|
|
1038
|
+
page,
|
|
1039
|
+
context,
|
|
1040
|
+
targetMode,
|
|
1041
|
+
initialViewport,
|
|
1042
|
+
captureEngine,
|
|
1043
|
+
consoleTracker,
|
|
1044
|
+
previewDriver,
|
|
1045
|
+
doNavigate
|
|
1046
|
+
} = options;
|
|
1047
|
+
let currentViewport = { ...initialViewport };
|
|
1048
|
+
const ctx = {
|
|
1049
|
+
page,
|
|
1050
|
+
context,
|
|
1051
|
+
targetMode,
|
|
1052
|
+
currentViewport,
|
|
1053
|
+
// --- Snapshots ---
|
|
1054
|
+
capture: async (name, opts) => {
|
|
1055
|
+
console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
|
|
1056
|
+
return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
|
|
1057
|
+
},
|
|
1058
|
+
captureBurst: async (name, opts) => {
|
|
1059
|
+
console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
|
|
1060
|
+
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1061
|
+
},
|
|
1062
|
+
// --- Navigation ---
|
|
1063
|
+
navigate: async (route) => {
|
|
1064
|
+
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1065
|
+
await doNavigate(route);
|
|
1066
|
+
},
|
|
1067
|
+
// --- Viewport Management ---
|
|
1068
|
+
resize: async (width, height) => {
|
|
1069
|
+
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1070
|
+
currentViewport = { width, height };
|
|
1071
|
+
ctx.currentViewport = currentViewport;
|
|
1072
|
+
await page.setViewportSize(currentViewport);
|
|
1073
|
+
await page.waitForTimeout(200);
|
|
1074
|
+
},
|
|
1075
|
+
setPreset: async (preset) => {
|
|
1076
|
+
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1077
|
+
await ctx.resize(preset.width, preset.height);
|
|
1078
|
+
},
|
|
1079
|
+
resizeToFit: async (selector, padding = 0) => {
|
|
1080
|
+
let boundingBox;
|
|
1081
|
+
if (selector) {
|
|
1082
|
+
const el = await page.$(selector);
|
|
1083
|
+
if (el) {
|
|
1084
|
+
boundingBox = await el.boundingBox();
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
boundingBox = await page.evaluate(() => ({
|
|
1088
|
+
width: document.documentElement.scrollWidth,
|
|
1089
|
+
height: document.documentElement.scrollHeight
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
if (boundingBox) {
|
|
1093
|
+
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1094
|
+
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1095
|
+
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1096
|
+
await ctx.resize(newWidth, newHeight);
|
|
1097
|
+
} else {
|
|
1098
|
+
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
// --- Waiting & DOM Observation ---
|
|
1102
|
+
wait: async (ms) => {
|
|
1103
|
+
await page.waitForTimeout(ms);
|
|
1104
|
+
},
|
|
1105
|
+
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1106
|
+
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1107
|
+
},
|
|
1108
|
+
// --- Interaction ---
|
|
1109
|
+
click: async (selector) => {
|
|
1110
|
+
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1111
|
+
await page.click(selector);
|
|
1112
|
+
},
|
|
1113
|
+
rightClick: async (selector) => {
|
|
1114
|
+
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1115
|
+
await page.click(selector, { button: "right" });
|
|
1116
|
+
},
|
|
1117
|
+
type: async (selector, text) => {
|
|
1118
|
+
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1119
|
+
await page.fill(selector, text);
|
|
1120
|
+
},
|
|
1121
|
+
selectOption: async (selector, value) => {
|
|
1122
|
+
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1123
|
+
await page.selectOption(selector, value);
|
|
1124
|
+
},
|
|
1125
|
+
hover: async (selector) => {
|
|
1126
|
+
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1127
|
+
await page.hover(selector);
|
|
1128
|
+
},
|
|
1129
|
+
scroll: async (selector, deltaY) => {
|
|
1130
|
+
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1131
|
+
await page.evaluate(
|
|
1132
|
+
({ sel, dY }) => {
|
|
1133
|
+
const el = document.querySelector(sel);
|
|
1134
|
+
if (el) {
|
|
1135
|
+
el.scrollTop += dY;
|
|
1136
|
+
} else {
|
|
1137
|
+
window.scrollBy(0, dY);
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
{ sel: selector, dY: deltaY }
|
|
1141
|
+
);
|
|
1142
|
+
await page.waitForTimeout(100);
|
|
1143
|
+
},
|
|
1144
|
+
// --- Logging ---
|
|
1145
|
+
log: (msg) => {
|
|
1146
|
+
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1147
|
+
},
|
|
1148
|
+
// --- Mock IPC (delegated or stubbed if plugin not loaded) ---
|
|
1149
|
+
setMockIpc: async (action, data, mockOptions) => {
|
|
1150
|
+
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
|
|
1151
|
+
},
|
|
1152
|
+
// --- HTTP Route Mocking ---
|
|
1153
|
+
setMockRoute: async (url, body, routeOptions) => {
|
|
1154
|
+
if (!previewDriver) {
|
|
1155
|
+
console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
|
|
1159
|
+
await previewDriver.addRouteMock({
|
|
1160
|
+
url,
|
|
1161
|
+
body,
|
|
1162
|
+
method: routeOptions?.method,
|
|
1163
|
+
status: routeOptions?.status,
|
|
1164
|
+
delayMs: routeOptions?.delayMs,
|
|
1165
|
+
headers: routeOptions?.headers
|
|
1166
|
+
});
|
|
1167
|
+
},
|
|
1168
|
+
// --- Console Errors ---
|
|
1169
|
+
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1170
|
+
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1171
|
+
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1172
|
+
// --- DOM Assertions ---
|
|
1173
|
+
readText: async (selector) => {
|
|
1174
|
+
const text = await page.textContent(selector);
|
|
1175
|
+
return text ? text.trim() : null;
|
|
1176
|
+
},
|
|
1177
|
+
getPageText: async () => {
|
|
1178
|
+
return await page.evaluate(() => document.body.innerText || "");
|
|
1179
|
+
},
|
|
1180
|
+
isVisible: async (selector) => {
|
|
1181
|
+
try {
|
|
1182
|
+
const element = await page.$(selector);
|
|
1183
|
+
if (!element) return false;
|
|
1184
|
+
return await element.isVisible();
|
|
1185
|
+
} catch {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
getElementCount: async (selector) => {
|
|
1190
|
+
const elements = await page.$$(selector);
|
|
1191
|
+
return elements.length;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
return {
|
|
1195
|
+
ctx,
|
|
1196
|
+
getCurrentViewport: () => currentViewport
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
var init_contextBuilder = __esm({
|
|
1200
|
+
"src/features/runner/lib/contextBuilder.ts"() {
|
|
1201
|
+
"use strict";
|
|
1060
1202
|
}
|
|
1061
1203
|
});
|
|
1062
1204
|
|
|
1063
1205
|
// src/features/runner/runner.ts
|
|
1064
|
-
import
|
|
1065
|
-
import
|
|
1206
|
+
import path7 from "path";
|
|
1207
|
+
import fs7 from "fs";
|
|
1066
1208
|
async function runVisualScenario(options) {
|
|
1067
1209
|
const { scenario } = options;
|
|
1068
1210
|
const targetMode = options.targetMode || "preview";
|
|
1069
1211
|
const startTime = Date.now();
|
|
1070
1212
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1071
|
-
const artifactsRoot = options.artifactsRoot ||
|
|
1072
|
-
if (options.cleanArtifacts &&
|
|
1213
|
+
const artifactsRoot = options.artifactsRoot || path7.resolve(process.cwd(), "artifacts");
|
|
1214
|
+
if (options.cleanArtifacts && fs7.existsSync(artifactsRoot)) {
|
|
1073
1215
|
console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
|
|
1074
|
-
const entries =
|
|
1216
|
+
const entries = fs7.readdirSync(artifactsRoot, { withFileTypes: true });
|
|
1075
1217
|
for (const entry of entries) {
|
|
1076
|
-
|
|
1218
|
+
fs7.rmSync(path7.join(artifactsRoot, entry.name), { recursive: true, force: true });
|
|
1077
1219
|
}
|
|
1078
1220
|
}
|
|
1079
|
-
const scenarioArtifactsDir =
|
|
1080
|
-
if (!
|
|
1081
|
-
|
|
1221
|
+
const scenarioArtifactsDir = path7.join(artifactsRoot, `${scenario.id}_${timestamp}`);
|
|
1222
|
+
if (!fs7.existsSync(scenarioArtifactsDir)) {
|
|
1223
|
+
fs7.mkdirSync(scenarioArtifactsDir, { recursive: true });
|
|
1082
1224
|
}
|
|
1083
1225
|
const captureEngine = new CaptureEngine(scenarioArtifactsDir);
|
|
1084
1226
|
const consoleTracker = new ConsoleTracker();
|
|
1085
1227
|
const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
|
|
1086
|
-
|
|
1087
|
-
let desktopDriver = null;
|
|
1228
|
+
const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
|
|
1088
1229
|
let previewDriver = null;
|
|
1089
1230
|
let processManager = null;
|
|
1231
|
+
let customDriverStop = null;
|
|
1232
|
+
const pluginManager = new PluginManager();
|
|
1233
|
+
const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
|
|
1234
|
+
if (pluginSpecs.length > 0) {
|
|
1235
|
+
await pluginManager.loadAll(pluginSpecs);
|
|
1236
|
+
}
|
|
1237
|
+
const hookContext = {
|
|
1238
|
+
scenario,
|
|
1239
|
+
targetMode,
|
|
1240
|
+
artifactsDir: scenarioArtifactsDir,
|
|
1241
|
+
cliOptions: options,
|
|
1242
|
+
state: /* @__PURE__ */ new Map()
|
|
1243
|
+
};
|
|
1090
1244
|
try {
|
|
1091
1245
|
let page;
|
|
1092
1246
|
let context;
|
|
@@ -1103,32 +1257,31 @@ async function runVisualScenario(options) {
|
|
|
1103
1257
|
const waitTarget = options.url || "http://localhost:5173";
|
|
1104
1258
|
await processManager.waitForUrl(waitTarget);
|
|
1105
1259
|
}
|
|
1260
|
+
await pluginManager.runSetup(hookContext);
|
|
1106
1261
|
if (typeof scenario.setup === "function") {
|
|
1107
1262
|
console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
|
|
1108
1263
|
await scenario.setup();
|
|
1109
1264
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1265
|
+
const customSession = await pluginManager.launchSession(
|
|
1266
|
+
{ currentViewport, headed: options.headed },
|
|
1267
|
+
hookContext
|
|
1268
|
+
);
|
|
1269
|
+
if (customSession) {
|
|
1270
|
+
page = customSession.page;
|
|
1271
|
+
context = customSession.context;
|
|
1272
|
+
if (customSession.stop) {
|
|
1273
|
+
customDriverStop = customSession.stop;
|
|
1274
|
+
}
|
|
1275
|
+
} else if (targetMode === "desktop") {
|
|
1276
|
+
throw new Error(
|
|
1277
|
+
`[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
|
|
1278
|
+
);
|
|
1121
1279
|
} else {
|
|
1122
1280
|
previewDriver = new PreviewDriver({
|
|
1123
1281
|
wwwrootDir: options.wwwrootDir,
|
|
1124
1282
|
url: options.url,
|
|
1125
1283
|
headed: options.headed
|
|
1126
1284
|
});
|
|
1127
|
-
const mergedMocks = [...options.globalMocks || [], ...scenario.mockIpc || []];
|
|
1128
|
-
if (mergedMocks.length > 0) {
|
|
1129
|
-
console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
|
|
1130
|
-
previewDriver.mockRegistry.setBatch(mergedMocks);
|
|
1131
|
-
}
|
|
1132
1285
|
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1133
1286
|
console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
|
|
1134
1287
|
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
@@ -1140,194 +1293,27 @@ async function runVisualScenario(options) {
|
|
|
1140
1293
|
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1141
1294
|
}
|
|
1142
1295
|
}
|
|
1296
|
+
await pluginManager.runOnContextCreated(context, hookContext);
|
|
1297
|
+
await pluginManager.runOnPageCreated(page, context, hookContext);
|
|
1143
1298
|
consoleTracker.attach(page);
|
|
1144
1299
|
console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
|
|
1145
1300
|
`);
|
|
1146
|
-
const doNavigate =
|
|
1147
|
-
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
1148
|
-
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
1149
|
-
await page.waitForTimeout(300);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
if (route.startsWith("#")) {
|
|
1153
|
-
await page.evaluate((r) => {
|
|
1154
|
-
window.location.hash = r;
|
|
1155
|
-
}, route);
|
|
1156
|
-
await page.waitForTimeout(300);
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
if (previewDriver?.baseUrl && options.url) {
|
|
1160
|
-
try {
|
|
1161
|
-
const fullUrl = new URL(route, previewDriver.baseUrl).toString();
|
|
1162
|
-
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
1163
|
-
await page.waitForTimeout(300);
|
|
1164
|
-
return;
|
|
1165
|
-
} catch {
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
1169
|
-
await page.evaluate((r) => {
|
|
1170
|
-
if (window.location.hash !== void 0) {
|
|
1171
|
-
window.location.hash = r;
|
|
1172
|
-
}
|
|
1173
|
-
}, routePath);
|
|
1174
|
-
await page.waitForTimeout(300);
|
|
1175
|
-
};
|
|
1301
|
+
const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
|
|
1176
1302
|
if (scenario.route) {
|
|
1177
|
-
console.log(
|
|
1303
|
+
console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
|
|
1178
1304
|
await doNavigate(scenario.route);
|
|
1179
1305
|
}
|
|
1180
|
-
const ctx = {
|
|
1306
|
+
const { ctx } = buildTestContext({
|
|
1181
1307
|
page,
|
|
1182
1308
|
context,
|
|
1183
1309
|
targetMode,
|
|
1184
|
-
currentViewport,
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1192
|
-
},
|
|
1193
|
-
navigate: async (route) => {
|
|
1194
|
-
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1195
|
-
await doNavigate(route);
|
|
1196
|
-
},
|
|
1197
|
-
// ─── Viewport ───
|
|
1198
|
-
resize: async (width, height) => {
|
|
1199
|
-
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1200
|
-
currentViewport = { width, height };
|
|
1201
|
-
ctx.currentViewport = currentViewport;
|
|
1202
|
-
await page.setViewportSize(currentViewport);
|
|
1203
|
-
await page.waitForTimeout(200);
|
|
1204
|
-
},
|
|
1205
|
-
setPreset: async (preset) => {
|
|
1206
|
-
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1207
|
-
await ctx.resize(preset.width, preset.height);
|
|
1208
|
-
},
|
|
1209
|
-
resizeToFit: async (selector, padding = 0) => {
|
|
1210
|
-
let boundingBox;
|
|
1211
|
-
if (selector) {
|
|
1212
|
-
const el = await page.$(selector);
|
|
1213
|
-
if (el) {
|
|
1214
|
-
boundingBox = await el.boundingBox();
|
|
1215
|
-
}
|
|
1216
|
-
} else {
|
|
1217
|
-
boundingBox = await page.evaluate(() => {
|
|
1218
|
-
return {
|
|
1219
|
-
width: document.documentElement.scrollWidth,
|
|
1220
|
-
height: document.documentElement.scrollHeight
|
|
1221
|
-
};
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
if (boundingBox) {
|
|
1225
|
-
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1226
|
-
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1227
|
-
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1228
|
-
await ctx.resize(newWidth, newHeight);
|
|
1229
|
-
} else {
|
|
1230
|
-
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1231
|
-
}
|
|
1232
|
-
},
|
|
1233
|
-
wait: async (ms) => {
|
|
1234
|
-
await page.waitForTimeout(ms);
|
|
1235
|
-
},
|
|
1236
|
-
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1237
|
-
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1238
|
-
},
|
|
1239
|
-
click: async (selector) => {
|
|
1240
|
-
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1241
|
-
await page.click(selector);
|
|
1242
|
-
},
|
|
1243
|
-
rightClick: async (selector) => {
|
|
1244
|
-
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1245
|
-
await page.click(selector, { button: "right" });
|
|
1246
|
-
},
|
|
1247
|
-
type: async (selector, text) => {
|
|
1248
|
-
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1249
|
-
await page.fill(selector, text);
|
|
1250
|
-
},
|
|
1251
|
-
selectOption: async (selector, value) => {
|
|
1252
|
-
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1253
|
-
await page.selectOption(selector, value);
|
|
1254
|
-
},
|
|
1255
|
-
hover: async (selector) => {
|
|
1256
|
-
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1257
|
-
await page.hover(selector);
|
|
1258
|
-
},
|
|
1259
|
-
scroll: async (selector, deltaY) => {
|
|
1260
|
-
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1261
|
-
await page.evaluate(({ sel, dY }) => {
|
|
1262
|
-
const el = document.querySelector(sel);
|
|
1263
|
-
if (el) {
|
|
1264
|
-
el.scrollTop += dY;
|
|
1265
|
-
} else {
|
|
1266
|
-
window.scrollBy(0, dY);
|
|
1267
|
-
}
|
|
1268
|
-
}, { sel: selector, dY: deltaY });
|
|
1269
|
-
await page.waitForTimeout(100);
|
|
1270
|
-
},
|
|
1271
|
-
log: (msg) => {
|
|
1272
|
-
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1273
|
-
},
|
|
1274
|
-
// ─── Mock IPC ───
|
|
1275
|
-
setMockIpc: async (action, data, mockOptions) => {
|
|
1276
|
-
if (targetMode === "desktop") {
|
|
1277
|
-
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
if (!previewDriver) {
|
|
1281
|
-
console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
|
|
1285
|
-
await previewDriver.updateMockIpc(action, data, mockOptions);
|
|
1286
|
-
},
|
|
1287
|
-
setMockRoute: async (url, body, routeOptions) => {
|
|
1288
|
-
if (targetMode === "desktop") {
|
|
1289
|
-
console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored in desktop mode`);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
if (!previewDriver) {
|
|
1293
|
-
console.log(`\u26A0\uFE0F [Mock Route] PreviewDriver not available`);
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
|
|
1297
|
-
await previewDriver.addRouteMock({
|
|
1298
|
-
url,
|
|
1299
|
-
body,
|
|
1300
|
-
method: routeOptions?.method,
|
|
1301
|
-
status: routeOptions?.status,
|
|
1302
|
-
delayMs: routeOptions?.delayMs,
|
|
1303
|
-
headers: routeOptions?.headers
|
|
1304
|
-
});
|
|
1305
|
-
},
|
|
1306
|
-
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1307
|
-
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1308
|
-
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1309
|
-
// ─── DOM Assertions ───
|
|
1310
|
-
readText: async (selector) => {
|
|
1311
|
-
const text = await page.textContent(selector);
|
|
1312
|
-
return text ? text.trim() : null;
|
|
1313
|
-
},
|
|
1314
|
-
getPageText: async () => {
|
|
1315
|
-
return await page.evaluate(() => document.body.innerText || "");
|
|
1316
|
-
},
|
|
1317
|
-
isVisible: async (selector) => {
|
|
1318
|
-
try {
|
|
1319
|
-
const element = await page.$(selector);
|
|
1320
|
-
if (!element) return false;
|
|
1321
|
-
return await element.isVisible();
|
|
1322
|
-
} catch {
|
|
1323
|
-
return false;
|
|
1324
|
-
}
|
|
1325
|
-
},
|
|
1326
|
-
getElementCount: async (selector) => {
|
|
1327
|
-
const elements = await page.$$(selector);
|
|
1328
|
-
return elements.length;
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1310
|
+
initialViewport: currentViewport,
|
|
1311
|
+
captureEngine,
|
|
1312
|
+
consoleTracker,
|
|
1313
|
+
previewDriver,
|
|
1314
|
+
doNavigate
|
|
1315
|
+
});
|
|
1316
|
+
await pluginManager.extendContext(ctx, page, hookContext);
|
|
1331
1317
|
await scenario.run(ctx);
|
|
1332
1318
|
const durationMs = Date.now() - startTime;
|
|
1333
1319
|
const snapshots = captureEngine.getSnapshots();
|
|
@@ -1342,38 +1328,19 @@ async function runVisualScenario(options) {
|
|
|
1342
1328
|
console.log(`
|
|
1343
1329
|
\u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
|
|
1344
1330
|
}
|
|
1345
|
-
const
|
|
1331
|
+
const reportData = {
|
|
1346
1332
|
scenario,
|
|
1347
1333
|
snapshots,
|
|
1348
1334
|
consoleErrors,
|
|
1349
1335
|
consoleWarnings,
|
|
1350
1336
|
outputDir: scenarioArtifactsDir,
|
|
1351
1337
|
targetMode,
|
|
1352
|
-
durationMs
|
|
1353
|
-
|
|
1354
|
-
const syncLatest = (repPath, snaps) => {
|
|
1355
|
-
try {
|
|
1356
|
-
const latestDir = path6.join(artifactsRoot, "latest");
|
|
1357
|
-
if (fs6.existsSync(latestDir)) {
|
|
1358
|
-
fs6.rmSync(latestDir, { recursive: true, force: true });
|
|
1359
|
-
}
|
|
1360
|
-
fs6.mkdirSync(latestDir, { recursive: true });
|
|
1361
|
-
fs6.copyFileSync(repPath, path6.join(latestDir, "report.md"));
|
|
1362
|
-
const manifestSrc = path6.join(scenarioArtifactsDir, "manifest.json");
|
|
1363
|
-
if (fs6.existsSync(manifestSrc)) {
|
|
1364
|
-
fs6.copyFileSync(manifestSrc, path6.join(latestDir, "manifest.json"));
|
|
1365
|
-
}
|
|
1366
|
-
for (const snap of snaps) {
|
|
1367
|
-
if (snap.filePath && fs6.existsSync(snap.filePath)) {
|
|
1368
|
-
fs6.copyFileSync(snap.filePath, path6.join(latestDir, snap.fileName));
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return path6.join(latestDir, "report.md");
|
|
1372
|
-
} catch {
|
|
1373
|
-
return void 0;
|
|
1374
|
-
}
|
|
1338
|
+
durationMs,
|
|
1339
|
+
customSections: []
|
|
1375
1340
|
};
|
|
1376
|
-
|
|
1341
|
+
await pluginManager.runOnAfterRun(reportData, hookContext);
|
|
1342
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1343
|
+
const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1377
1344
|
console.log(`
|
|
1378
1345
|
\u2705 Visual Test Completed Successfully!`);
|
|
1379
1346
|
console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
|
|
@@ -1400,7 +1367,7 @@ async function runVisualScenario(options) {
|
|
|
1400
1367
|
const snapshots = captureEngine.getSnapshots();
|
|
1401
1368
|
const consoleErrors = consoleTracker.getErrors();
|
|
1402
1369
|
const consoleWarnings = consoleTracker.getWarnings();
|
|
1403
|
-
const
|
|
1370
|
+
const reportData = {
|
|
1404
1371
|
scenario,
|
|
1405
1372
|
snapshots,
|
|
1406
1373
|
consoleErrors,
|
|
@@ -1408,16 +1375,9 @@ async function runVisualScenario(options) {
|
|
|
1408
1375
|
outputDir: scenarioArtifactsDir,
|
|
1409
1376
|
targetMode,
|
|
1410
1377
|
durationMs
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
if (fs6.existsSync(latestDir)) {
|
|
1415
|
-
fs6.rmSync(latestDir, { recursive: true, force: true });
|
|
1416
|
-
}
|
|
1417
|
-
fs6.mkdirSync(latestDir, { recursive: true });
|
|
1418
|
-
fs6.copyFileSync(reportPath, path6.join(latestDir, "report.md"));
|
|
1419
|
-
} catch {
|
|
1420
|
-
}
|
|
1378
|
+
};
|
|
1379
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1380
|
+
syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1421
1381
|
return {
|
|
1422
1382
|
scenarioId: scenario.id,
|
|
1423
1383
|
targetMode,
|
|
@@ -1430,6 +1390,7 @@ async function runVisualScenario(options) {
|
|
|
1430
1390
|
error: err.message
|
|
1431
1391
|
};
|
|
1432
1392
|
} finally {
|
|
1393
|
+
await pluginManager.runTeardown(hookContext);
|
|
1433
1394
|
if (typeof scenario.teardown === "function") {
|
|
1434
1395
|
try {
|
|
1435
1396
|
console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
|
|
@@ -1441,10 +1402,10 @@ async function runVisualScenario(options) {
|
|
|
1441
1402
|
if (options.cleanPaths && options.cleanPaths.length > 0) {
|
|
1442
1403
|
for (const cleanTarget of options.cleanPaths) {
|
|
1443
1404
|
try {
|
|
1444
|
-
const resolvedCleanPath =
|
|
1445
|
-
if (
|
|
1405
|
+
const resolvedCleanPath = path7.resolve(process.cwd(), cleanTarget);
|
|
1406
|
+
if (fs7.existsSync(resolvedCleanPath)) {
|
|
1446
1407
|
console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
|
|
1447
|
-
|
|
1408
|
+
fs7.rmSync(resolvedCleanPath, { recursive: true, force: true });
|
|
1448
1409
|
}
|
|
1449
1410
|
} catch (cleanErr) {
|
|
1450
1411
|
console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
|
|
@@ -1452,7 +1413,7 @@ async function runVisualScenario(options) {
|
|
|
1452
1413
|
}
|
|
1453
1414
|
}
|
|
1454
1415
|
if (!options.detach) {
|
|
1455
|
-
if (
|
|
1416
|
+
if (customDriverStop) await customDriverStop();
|
|
1456
1417
|
if (previewDriver) await previewDriver.stop();
|
|
1457
1418
|
if (processManager) await processManager.stop();
|
|
1458
1419
|
} else {
|
|
@@ -1467,15 +1428,29 @@ var init_runner = __esm({
|
|
|
1467
1428
|
init_capture();
|
|
1468
1429
|
init_reporter();
|
|
1469
1430
|
init_consoleTracker();
|
|
1470
|
-
init_desktopDriver();
|
|
1471
1431
|
init_previewDriver();
|
|
1472
1432
|
init_processManager();
|
|
1433
|
+
init_pluginLoader();
|
|
1434
|
+
init_navigation();
|
|
1435
|
+
init_artifactsSync();
|
|
1436
|
+
init_contextBuilder();
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// src/features/runner/index.ts
|
|
1441
|
+
var init_runner2 = __esm({
|
|
1442
|
+
"src/features/runner/index.ts"() {
|
|
1443
|
+
"use strict";
|
|
1444
|
+
init_runner();
|
|
1445
|
+
init_navigation();
|
|
1446
|
+
init_artifactsSync();
|
|
1447
|
+
init_contextBuilder();
|
|
1473
1448
|
}
|
|
1474
1449
|
});
|
|
1475
1450
|
|
|
1476
1451
|
// src/features/snap/snap.ts
|
|
1477
|
-
import
|
|
1478
|
-
import
|
|
1452
|
+
import path8 from "path";
|
|
1453
|
+
import fs8 from "fs";
|
|
1479
1454
|
function parseViewportPresets(raw) {
|
|
1480
1455
|
if (!raw || raw.length === 0) {
|
|
1481
1456
|
return [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
|
|
@@ -1522,7 +1497,7 @@ async function runQuickSnap(options) {
|
|
|
1522
1497
|
console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
|
|
1523
1498
|
} else {
|
|
1524
1499
|
const candidateDir = resolveWwwrootDir();
|
|
1525
|
-
if (
|
|
1500
|
+
if (fs8.existsSync(candidateDir) && fs8.existsSync(path8.join(candidateDir, "index.html"))) {
|
|
1526
1501
|
useStaticPreview = true;
|
|
1527
1502
|
wwwrootDir = candidateDir;
|
|
1528
1503
|
console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
|
|
@@ -1564,7 +1539,7 @@ async function runQuickSnap(options) {
|
|
|
1564
1539
|
ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
|
|
1565
1540
|
await ctx.setPreset(vp);
|
|
1566
1541
|
await ctx.wait(200);
|
|
1567
|
-
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}
|
|
1542
|
+
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
|
|
1568
1543
|
}
|
|
1569
1544
|
if (options.selector) {
|
|
1570
1545
|
ctx.log(`Focusing on selector: "${options.selector}"`);
|
|
@@ -1597,7 +1572,8 @@ async function runQuickSnap(options) {
|
|
|
1597
1572
|
port: options.port,
|
|
1598
1573
|
headed: options.headed,
|
|
1599
1574
|
detach: options.detach,
|
|
1600
|
-
artifactsRoot: options.outDir ?
|
|
1575
|
+
artifactsRoot: options.outDir ? path8.resolve(process.cwd(), options.outDir) : void 0,
|
|
1576
|
+
plugins: options.plugins
|
|
1601
1577
|
});
|
|
1602
1578
|
console.log(`
|
|
1603
1579
|
========================================`);
|
|
@@ -1631,109 +1607,118 @@ var init_snap = __esm({
|
|
|
1631
1607
|
}
|
|
1632
1608
|
});
|
|
1633
1609
|
|
|
1634
|
-
// src/app/
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
} else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
|
|
1712
|
-
const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
|
|
1713
|
-
const rawPaths = arg.slice(prefix.length);
|
|
1714
|
-
options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1715
|
-
} else if (arg.startsWith("--dir=")) {
|
|
1716
|
-
options.dir = arg.split("=")[1];
|
|
1717
|
-
} else if (arg.startsWith("--wwwroot=")) {
|
|
1718
|
-
options.wwwroot = arg.split("=")[1];
|
|
1719
|
-
} else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
|
|
1720
|
-
options.outDir = arg.split("=")[1];
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
if (options.start && !options.startCwd) {
|
|
1724
|
-
options.startCwd = detectStartCwd(options.startCwd);
|
|
1610
|
+
// src/app/lib/argsParser.ts
|
|
1611
|
+
function parseCliArgs(argv, fileConfig) {
|
|
1612
|
+
const isInitCommand = argv[0] === "init";
|
|
1613
|
+
const isSnapCommand = argv[0] === "snap";
|
|
1614
|
+
const effectiveArgs = isSnapCommand ? argv.slice(1) : argv;
|
|
1615
|
+
const options = {
|
|
1616
|
+
mode: fileConfig.mode || "preview",
|
|
1617
|
+
port: fileConfig.port || 9222,
|
|
1618
|
+
headed: fileConfig.headed ?? false,
|
|
1619
|
+
detach: fileConfig.detach ?? false,
|
|
1620
|
+
build: fileConfig.buildCommand || false,
|
|
1621
|
+
start: fileConfig.startCommand,
|
|
1622
|
+
startCwd: fileConfig.startCwd,
|
|
1623
|
+
cleanArtifacts: fileConfig.cleanArtifacts ?? false,
|
|
1624
|
+
url: fileConfig.url,
|
|
1625
|
+
exe: fileConfig.executablePath,
|
|
1626
|
+
clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
|
|
1627
|
+
dir: fileConfig.scenarios,
|
|
1628
|
+
wwwroot: fileConfig.wwwroot,
|
|
1629
|
+
outDir: fileConfig.outDir,
|
|
1630
|
+
plugins: fileConfig.plugins ? [...fileConfig.plugins] : []
|
|
1631
|
+
};
|
|
1632
|
+
for (const arg of effectiveArgs) {
|
|
1633
|
+
if (arg === "--help" || arg === "-h") {
|
|
1634
|
+
options.help = true;
|
|
1635
|
+
} else if (arg.startsWith("--scenario=")) {
|
|
1636
|
+
options.scenario = arg.split("=")[1];
|
|
1637
|
+
} else if (arg === "--all") {
|
|
1638
|
+
options.all = true;
|
|
1639
|
+
} else if (arg.startsWith("--mode=")) {
|
|
1640
|
+
options.mode = arg.split("=")[1].toLowerCase();
|
|
1641
|
+
} else if (arg.startsWith("--port=")) {
|
|
1642
|
+
options.port = parseInt(arg.split("=")[1], 10) || 9222;
|
|
1643
|
+
} else if (arg === "--headed") {
|
|
1644
|
+
options.headed = true;
|
|
1645
|
+
} else if (arg === "--detach") {
|
|
1646
|
+
options.detach = true;
|
|
1647
|
+
} else if (arg === "--build") {
|
|
1648
|
+
options.build = true;
|
|
1649
|
+
} else if (arg.startsWith("--build=")) {
|
|
1650
|
+
options.build = arg.slice("--build=".length);
|
|
1651
|
+
} else if (arg.startsWith("--start=")) {
|
|
1652
|
+
options.start = arg.slice("--start=".length);
|
|
1653
|
+
} else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
|
|
1654
|
+
options.startCwd = arg.split("=")[1];
|
|
1655
|
+
} else if (arg === "--clean-artifacts") {
|
|
1656
|
+
options.cleanArtifacts = true;
|
|
1657
|
+
} else if (arg.startsWith("--url=")) {
|
|
1658
|
+
options.url = arg.split("=")[1];
|
|
1659
|
+
} else if (arg.startsWith("--selector=")) {
|
|
1660
|
+
options.selector = arg.split("=")[1];
|
|
1661
|
+
} else if (arg.startsWith("--viewports=")) {
|
|
1662
|
+
const raw = arg.slice("--viewports=".length);
|
|
1663
|
+
options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1664
|
+
} else if (arg.startsWith("--wait=")) {
|
|
1665
|
+
options.waitMs = parseInt(arg.slice("--wait=".length), 10);
|
|
1666
|
+
} else if (arg.startsWith("--name=")) {
|
|
1667
|
+
options.name = arg.slice("--name=".length);
|
|
1668
|
+
} else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
|
|
1669
|
+
const prefix = arg.startsWith("--exe=") ? "--exe=" : "--executable=";
|
|
1670
|
+
options.exe = arg.slice(prefix.length);
|
|
1671
|
+
} else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
|
|
1672
|
+
const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
|
|
1673
|
+
const rawPaths = arg.slice(prefix.length);
|
|
1674
|
+
options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1675
|
+
} else if (arg.startsWith("--dir=")) {
|
|
1676
|
+
options.dir = arg.split("=")[1];
|
|
1677
|
+
} else if (arg.startsWith("--wwwroot=")) {
|
|
1678
|
+
options.wwwroot = arg.split("=")[1];
|
|
1679
|
+
} else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
|
|
1680
|
+
options.outDir = arg.split("=")[1];
|
|
1681
|
+
} else if (arg.startsWith("--plugin=") || arg.startsWith("--plugins=")) {
|
|
1682
|
+
const raw = arg.split("=")[1] || "";
|
|
1683
|
+
const items = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1684
|
+
options.plugins = [...options.plugins || [], ...items];
|
|
1685
|
+
} else if (arg === "--full" || arg === "--full-page") {
|
|
1686
|
+
options.fullPage = true;
|
|
1725
1687
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1688
|
+
}
|
|
1689
|
+
if (options.start && !options.startCwd) {
|
|
1690
|
+
options.startCwd = detectStartCwd(options.startCwd);
|
|
1691
|
+
}
|
|
1692
|
+
if ((options.mode === "desktop" || options.exe) && !options.plugins?.includes("desktop-webview2")) {
|
|
1693
|
+
options.plugins = ["desktop-webview2", ...options.plugins || []];
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
isSnapCommand,
|
|
1697
|
+
isInitCommand,
|
|
1698
|
+
options
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
var init_argsParser = __esm({
|
|
1702
|
+
"src/app/lib/argsParser.ts"() {
|
|
1703
|
+
"use strict";
|
|
1704
|
+
init_config();
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// src/app/lib/help.ts
|
|
1709
|
+
function printHelp() {
|
|
1710
|
+
console.log(`
|
|
1728
1711
|
\u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
|
|
1729
1712
|
|
|
1730
1713
|
Usage:
|
|
1731
1714
|
npx agent-lens snap [options] Instant one-shot visual & console check (no test files needed)
|
|
1715
|
+
npx agent-lens live [action] Interactive live session (start, click, type, snap, stop)
|
|
1732
1716
|
npx agent-lens [options] Run scripted scenario tests from scenarios/
|
|
1733
1717
|
npx agent-lens init Generate starter scenario template & mocks
|
|
1734
1718
|
|
|
1735
1719
|
Commands:
|
|
1736
1720
|
snap Take immediate multi-viewport screenshots of a URL & check console errors
|
|
1721
|
+
live Interactive control: click coordinates/selectors, type text, snap --full
|
|
1737
1722
|
init Generate starter template in scenarios/template.scenario.ts and mocks.ts
|
|
1738
1723
|
|
|
1739
1724
|
Options:
|
|
@@ -1741,6 +1726,7 @@ Options:
|
|
|
1741
1726
|
--start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
|
|
1742
1727
|
--start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
|
|
1743
1728
|
--clean-artifacts Purge previous test artifacts to prevent folder bloat
|
|
1729
|
+
--full Capture full scrollable page instead of only the viewport
|
|
1744
1730
|
--selector=<css> Target a specific element to focus on / resize-to-fit
|
|
1745
1731
|
--viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
|
|
1746
1732
|
--wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
|
|
@@ -1752,6 +1738,7 @@ Options:
|
|
|
1752
1738
|
--port=<port> CDP remote debugging port [default: 9222]
|
|
1753
1739
|
--build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
|
|
1754
1740
|
--clean=<paths> Comma-separated paths to safely delete upon test exit (e.g. --clean="./temp,./cache")
|
|
1741
|
+
--plugin=<names> Comma-separated plugins (e.g. --plugin=desktop-webview2,mock-ipc)
|
|
1755
1742
|
--headed Show Chromium browser window
|
|
1756
1743
|
--detach Keep browser/app open after finishing
|
|
1757
1744
|
--dir=<path> Custom scenarios directory [default: scenarios]
|
|
@@ -1759,15 +1746,24 @@ Options:
|
|
|
1759
1746
|
--folder=<path> Directory to save visual artifacts/reports (also --outDir)
|
|
1760
1747
|
--help, -h Show this help message
|
|
1761
1748
|
`);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1749
|
+
}
|
|
1750
|
+
var init_help = __esm({
|
|
1751
|
+
"src/app/lib/help.ts"() {
|
|
1752
|
+
"use strict";
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
// src/app/lib/templateInit.ts
|
|
1757
|
+
import path9 from "path";
|
|
1758
|
+
import fs9 from "fs";
|
|
1759
|
+
function initScenarioTemplate() {
|
|
1760
|
+
const targetDir = path9.resolve(process.cwd(), "scenarios");
|
|
1761
|
+
if (!fs9.existsSync(targetDir)) {
|
|
1762
|
+
fs9.mkdirSync(targetDir, { recursive: true });
|
|
1763
|
+
}
|
|
1764
|
+
const templatePath = path9.join(targetDir, "template.scenario.ts");
|
|
1765
|
+
if (!fs9.existsSync(templatePath)) {
|
|
1766
|
+
const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
|
|
1771
1767
|
|
|
1772
1768
|
export default defineVisualTest({
|
|
1773
1769
|
id: 'template-check',
|
|
@@ -1797,14 +1793,14 @@ export default defineVisualTest({
|
|
|
1797
1793
|
}
|
|
1798
1794
|
});
|
|
1799
1795
|
`;
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1796
|
+
fs9.writeFileSync(templatePath, templateContent, "utf8");
|
|
1797
|
+
console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
|
|
1798
|
+
} else {
|
|
1799
|
+
console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
|
|
1800
|
+
}
|
|
1801
|
+
const mocksPath = path9.join(targetDir, "mocks.ts");
|
|
1802
|
+
if (!fs9.existsSync(mocksPath)) {
|
|
1803
|
+
const mocksContent = `/**
|
|
1808
1804
|
* Global IPC & API Mocks
|
|
1809
1805
|
*
|
|
1810
1806
|
* Export an array of base mocks to satisfy root application state on boot.
|
|
@@ -1814,160 +1810,408 @@ export default [
|
|
|
1814
1810
|
{ action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
|
|
1815
1811
|
];
|
|
1816
1812
|
`;
|
|
1817
|
-
|
|
1818
|
-
|
|
1813
|
+
fs9.writeFileSync(mocksPath, mocksContent, "utf8");
|
|
1814
|
+
console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
|
|
1815
|
+
}
|
|
1816
|
+
console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
|
|
1817
|
+
}
|
|
1818
|
+
var init_templateInit = __esm({
|
|
1819
|
+
"src/app/lib/templateInit.ts"() {
|
|
1820
|
+
"use strict";
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
// src/app/lib/scenarioFinder.ts
|
|
1825
|
+
import path10 from "path";
|
|
1826
|
+
import fs10 from "fs";
|
|
1827
|
+
async function findScenarios(dir, specificName) {
|
|
1828
|
+
const results = [];
|
|
1829
|
+
if (!fs10.existsSync(dir)) return results;
|
|
1830
|
+
const items = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1831
|
+
for (const item of items) {
|
|
1832
|
+
const fullPath = path10.join(dir, item.name);
|
|
1833
|
+
if (item.isDirectory()) {
|
|
1834
|
+
results.push(...await findScenarios(fullPath, specificName));
|
|
1835
|
+
} else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
|
|
1836
|
+
if (!specificName || item.name.startsWith(specificName)) {
|
|
1837
|
+
results.push(fullPath);
|
|
1819
1838
|
}
|
|
1820
|
-
console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
|
|
1821
1839
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1840
|
+
}
|
|
1841
|
+
return results;
|
|
1842
|
+
}
|
|
1843
|
+
function resolveScenariosDirectory(customDir) {
|
|
1844
|
+
if (customDir) {
|
|
1845
|
+
return path10.resolve(process.cwd(), customDir);
|
|
1846
|
+
}
|
|
1847
|
+
const candidates = [
|
|
1848
|
+
"scenarios",
|
|
1849
|
+
"tests/visual",
|
|
1850
|
+
"tests/scenarios",
|
|
1851
|
+
"test/scenarios",
|
|
1852
|
+
"src/scenarios"
|
|
1853
|
+
];
|
|
1854
|
+
for (const c of candidates) {
|
|
1855
|
+
const p = path10.resolve(process.cwd(), c);
|
|
1856
|
+
if (fs10.existsSync(p)) {
|
|
1857
|
+
return p;
|
|
1825
1858
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1859
|
+
}
|
|
1860
|
+
return path10.resolve(process.cwd(), "scenarios");
|
|
1861
|
+
}
|
|
1862
|
+
var init_scenarioFinder = __esm({
|
|
1863
|
+
"src/app/lib/scenarioFinder.ts"() {
|
|
1864
|
+
"use strict";
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
// src/plugins/live-controller/session.ts
|
|
1869
|
+
import fs11 from "fs";
|
|
1870
|
+
import path11 from "path";
|
|
1871
|
+
import treeKill2 from "tree-kill";
|
|
1872
|
+
function getLiveArtifactsDir() {
|
|
1873
|
+
if (!fs11.existsSync(LIVE_ARTIFACTS_DIR)) {
|
|
1874
|
+
fs11.mkdirSync(LIVE_ARTIFACTS_DIR, { recursive: true });
|
|
1875
|
+
}
|
|
1876
|
+
return LIVE_ARTIFACTS_DIR;
|
|
1877
|
+
}
|
|
1878
|
+
function readLiveSession() {
|
|
1879
|
+
if (!fs11.existsSync(SESSION_FILE)) return null;
|
|
1880
|
+
try {
|
|
1881
|
+
return JSON.parse(fs11.readFileSync(SESSION_FILE, "utf-8"));
|
|
1882
|
+
} catch {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function saveLiveSession(info) {
|
|
1887
|
+
if (!fs11.existsSync(SESSION_DIR)) {
|
|
1888
|
+
fs11.mkdirSync(SESSION_DIR, { recursive: true });
|
|
1889
|
+
}
|
|
1890
|
+
fs11.writeFileSync(SESSION_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
1891
|
+
}
|
|
1892
|
+
function clearLiveSession() {
|
|
1893
|
+
if (fs11.existsSync(SESSION_FILE)) {
|
|
1894
|
+
try {
|
|
1895
|
+
fs11.unlinkSync(SESSION_FILE);
|
|
1896
|
+
} catch {
|
|
1841
1897
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
async function connectToLiveSession() {
|
|
1901
|
+
const session = readLiveSession();
|
|
1902
|
+
if (!session) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`No active live session found. Start one with: npx agent-lens live start --url=http://localhost:5173`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${session.port}`);
|
|
1908
|
+
const contexts = browser.contexts();
|
|
1909
|
+
const context = contexts[0] || await browser.newContext();
|
|
1910
|
+
const pages = context.pages();
|
|
1911
|
+
const page = pages[0] || await context.waitForEvent("page", { timeout: 5e3 });
|
|
1912
|
+
return { browser, context, page };
|
|
1913
|
+
}
|
|
1914
|
+
async function stopLiveSession() {
|
|
1915
|
+
const session = readLiveSession();
|
|
1916
|
+
if (!session) {
|
|
1917
|
+
console.log(`\u2139\uFE0F [Live] No active session to stop.`);
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
try {
|
|
1921
|
+
const { browser } = await connectToLiveSession();
|
|
1922
|
+
await browser.close().catch(() => {
|
|
1923
|
+
});
|
|
1924
|
+
} catch {
|
|
1925
|
+
}
|
|
1926
|
+
if (session.pid) {
|
|
1927
|
+
await new Promise((resolve) => {
|
|
1928
|
+
treeKill2(session.pid, "SIGTERM", () => resolve());
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
clearLiveSession();
|
|
1932
|
+
console.log(`\u{1F6D1} [Live] Session stopped and cleaned up.`);
|
|
1933
|
+
return true;
|
|
1934
|
+
}
|
|
1935
|
+
var SESSION_DIR, SESSION_FILE, LIVE_ARTIFACTS_DIR;
|
|
1936
|
+
var init_session = __esm({
|
|
1937
|
+
"src/plugins/live-controller/session.ts"() {
|
|
1938
|
+
"use strict";
|
|
1939
|
+
init_playwrightLoader();
|
|
1940
|
+
SESSION_DIR = path11.resolve(process.cwd(), ".agent-lens");
|
|
1941
|
+
SESSION_FILE = path11.join(SESSION_DIR, "live-session.json");
|
|
1942
|
+
LIVE_ARTIFACTS_DIR = path11.resolve(process.cwd(), "artifacts", "live");
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// src/plugins/live-controller/actions.ts
|
|
1947
|
+
import path12 from "path";
|
|
1948
|
+
async function clickCoords(page, x, y, options) {
|
|
1949
|
+
console.log(`\u{1F5B1}\uFE0F [LiveController] Clicking coordinates: (${x}, ${y})`);
|
|
1950
|
+
await page.mouse.click(x, y, options);
|
|
1951
|
+
await page.waitForTimeout(200);
|
|
1952
|
+
}
|
|
1953
|
+
async function snapLive(page, options) {
|
|
1954
|
+
const artifactsDir = getLiveArtifactsDir();
|
|
1955
|
+
const fileName = options?.name ? `${options.name}.png` : "current.png";
|
|
1956
|
+
const filePath = path12.join(artifactsDir, fileName);
|
|
1957
|
+
if (options?.selector) {
|
|
1958
|
+
const el = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
1959
|
+
await el.screenshot({ path: filePath });
|
|
1960
|
+
} else {
|
|
1961
|
+
await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
|
|
1962
|
+
}
|
|
1963
|
+
console.log(`\u{1F4F8} [LiveController] Snapshot saved: ${filePath}${options?.fullPage ? " (Full Page)" : ""}`);
|
|
1964
|
+
return filePath;
|
|
1965
|
+
}
|
|
1966
|
+
async function handleLiveCli(argv) {
|
|
1967
|
+
const action = argv[0]?.toLowerCase();
|
|
1968
|
+
switch (action) {
|
|
1969
|
+
case "start": {
|
|
1970
|
+
const urlArg = argv.find((a) => a.startsWith("--url="))?.split("=")[1] || "http://localhost:5173";
|
|
1971
|
+
const portArg = parseInt(argv.find((a) => a.startsWith("--port="))?.split("=")[1] || "9223", 10);
|
|
1972
|
+
const isHeaded = argv.includes("--headed");
|
|
1973
|
+
console.log(`\u{1F680} [Live] Starting background browser for ${urlArg} on CDP port ${portArg}...`);
|
|
1974
|
+
const browser = await chromium.launch({
|
|
1975
|
+
headless: !isHeaded,
|
|
1976
|
+
args: [`--remote-debugging-port=${portArg}`, "--no-sandbox"]
|
|
1977
|
+
});
|
|
1978
|
+
const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
|
|
1979
|
+
const page = await context.newPage();
|
|
1980
|
+
await page.goto(urlArg, { waitUntil: "domcontentloaded" });
|
|
1981
|
+
await page.waitForTimeout(500);
|
|
1982
|
+
saveLiveSession({
|
|
1983
|
+
port: portArg,
|
|
1984
|
+
url: urlArg,
|
|
1985
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1986
|
+
});
|
|
1987
|
+
await snapLive(page, { name: "current" });
|
|
1988
|
+
console.log(`\u2705 [Live] Session active! You can now send live commands:
|
|
1989
|
+
`);
|
|
1990
|
+
console.log(` npx agent-lens live click 450 120`);
|
|
1991
|
+
console.log(` npx agent-lens live type "input" "hello"`);
|
|
1992
|
+
console.log(` npx agent-lens live snap --full`);
|
|
1993
|
+
console.log(` npx agent-lens live stop
|
|
1994
|
+
`);
|
|
1995
|
+
return true;
|
|
1860
1996
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
mode: options.mode,
|
|
1877
|
-
exe: options.exe,
|
|
1878
|
-
port: options.port
|
|
1879
|
-
});
|
|
1880
|
-
process.exit(snapSuccess ? 0 : 1);
|
|
1997
|
+
case "click": {
|
|
1998
|
+
const { page, browser } = await connectToLiveSession();
|
|
1999
|
+
const firstArg = argv[1];
|
|
2000
|
+
const secondArg = argv[2];
|
|
2001
|
+
const x = parseInt(firstArg, 10);
|
|
2002
|
+
const y = parseInt(secondArg, 10);
|
|
2003
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
2004
|
+
await clickCoords(page, x, y);
|
|
2005
|
+
} else if (firstArg) {
|
|
2006
|
+
console.log(`\u{1F5B1}\uFE0F [Live] Clicking selector: "${firstArg}"`);
|
|
2007
|
+
await page.click(firstArg);
|
|
2008
|
+
} else {
|
|
2009
|
+
console.error(`\u274C Usage: npx agent-lens live click <x> <y> OR npx agent-lens live click <selector>`);
|
|
2010
|
+
await browser.close();
|
|
2011
|
+
return false;
|
|
1881
2012
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2013
|
+
await snapLive(page, { name: "current" });
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
case "type": {
|
|
2017
|
+
const { page, browser } = await connectToLiveSession();
|
|
2018
|
+
const selector = argv[1];
|
|
2019
|
+
const text = argv[2];
|
|
2020
|
+
if (!selector || text === void 0) {
|
|
2021
|
+
console.error(`\u274C Usage: npx agent-lens live type <selector> <text>`);
|
|
2022
|
+
await browser.close();
|
|
2023
|
+
return false;
|
|
1886
2024
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2025
|
+
console.log(`\u2328\uFE0F [Live] Typing into "${selector}": "${text}"`);
|
|
2026
|
+
await page.fill(selector, text);
|
|
2027
|
+
await snapLive(page, { name: "current" });
|
|
2028
|
+
return true;
|
|
2029
|
+
}
|
|
2030
|
+
case "snap": {
|
|
2031
|
+
const { page } = await connectToLiveSession();
|
|
2032
|
+
const isFull = argv.includes("--full");
|
|
2033
|
+
const nameArg = argv.find((a) => !a.startsWith("--") && a !== "snap");
|
|
2034
|
+
await snapLive(page, { name: nameArg || "current", fullPage: isFull });
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
case "stop": {
|
|
2038
|
+
return await stopLiveSession();
|
|
2039
|
+
}
|
|
2040
|
+
default: {
|
|
2041
|
+
console.log(`
|
|
2042
|
+
\u2139\uFE0F AgentLens Live Controller:
|
|
2043
|
+
npx agent-lens live start --url=<url> Start background live session
|
|
2044
|
+
npx agent-lens live click <x> <y> Click at pixel coordinates
|
|
2045
|
+
npx agent-lens live click <selector> Click CSS selector
|
|
2046
|
+
npx agent-lens live type <sel> <text> Fill input field
|
|
2047
|
+
npx agent-lens live snap [name] [--full] Capture current or full-page screenshot
|
|
2048
|
+
npx agent-lens live stop Close live browser and finish
|
|
2049
|
+
`);
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
var init_actions = __esm({
|
|
2055
|
+
"src/plugins/live-controller/actions.ts"() {
|
|
2056
|
+
"use strict";
|
|
2057
|
+
init_session();
|
|
2058
|
+
init_playwrightLoader();
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
// src/app/cli.ts
|
|
2063
|
+
import path13 from "path";
|
|
2064
|
+
import fs12 from "fs";
|
|
2065
|
+
import { execSync } from "child_process";
|
|
2066
|
+
import { createJiti as createJiti2 } from "jiti";
|
|
2067
|
+
var require_cli = __commonJS({
|
|
2068
|
+
"src/app/cli.ts"() {
|
|
2069
|
+
init_config();
|
|
2070
|
+
init_runner2();
|
|
2071
|
+
init_snap();
|
|
2072
|
+
init_argsParser();
|
|
2073
|
+
init_help();
|
|
2074
|
+
init_templateInit();
|
|
2075
|
+
init_scenarioFinder();
|
|
2076
|
+
init_actions();
|
|
2077
|
+
var rawArgs = process.argv.slice(2);
|
|
2078
|
+
if (rawArgs[0] === "live") {
|
|
2079
|
+
handleLiveCli(rawArgs.slice(1)).then((success) => process.exit(success ? 0 : 1)).catch((err) => {
|
|
2080
|
+
console.error("\u274C Live command failed:", err instanceof Error ? err.message : err);
|
|
1892
2081
|
process.exit(1);
|
|
2082
|
+
});
|
|
2083
|
+
} else {
|
|
2084
|
+
const fileConfig = loadConfig();
|
|
2085
|
+
const { isSnapCommand, isInitCommand, options } = parseCliArgs(rawArgs, fileConfig);
|
|
2086
|
+
if (isInitCommand) {
|
|
2087
|
+
initScenarioTemplate();
|
|
2088
|
+
process.exit(0);
|
|
1893
2089
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
console.log(`
|
|
1898
|
-
\u{1F528} [Build] Running build process: "${buildCmd}"...`);
|
|
1899
|
-
try {
|
|
1900
|
-
execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
1901
|
-
} catch (e) {
|
|
1902
|
-
console.error(`\u274C Build failed: ${buildCmd}`);
|
|
1903
|
-
process.exit(1);
|
|
1904
|
-
}
|
|
1905
|
-
} else if (options.mode === "preview" && !options.url && !options.start) {
|
|
1906
|
-
console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
|
|
1907
|
-
}
|
|
1908
|
-
console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
|
|
1909
|
-
const jiti = createJiti(process.cwd());
|
|
1910
|
-
let globalMocks = [];
|
|
1911
|
-
const mockCandidates = [
|
|
1912
|
-
path8.join(scenariosDir, "mocks.ts"),
|
|
1913
|
-
path8.join(scenariosDir, "mocks.js"),
|
|
1914
|
-
path8.join(process.cwd(), "mocks.ts"),
|
|
1915
|
-
path8.join(process.cwd(), "mocks.js")
|
|
1916
|
-
];
|
|
1917
|
-
for (const mocksPath of mockCandidates) {
|
|
1918
|
-
if (fs8.existsSync(mocksPath)) {
|
|
1919
|
-
try {
|
|
1920
|
-
const m = await jiti.import(mocksPath);
|
|
1921
|
-
globalMocks = m.default || m.mocks || [];
|
|
1922
|
-
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${path8.basename(mocksPath)}`);
|
|
1923
|
-
break;
|
|
1924
|
-
} catch (err) {
|
|
1925
|
-
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
2090
|
+
if (options.help) {
|
|
2091
|
+
printHelp();
|
|
2092
|
+
process.exit(0);
|
|
1928
2093
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
const scenarioModule = await jiti.import(scenarioPath);
|
|
1933
|
-
const scenario = scenarioModule.default || scenarioModule.scenario;
|
|
1934
|
-
if (!scenario || typeof scenario.run !== "function") {
|
|
1935
|
-
console.error(`\u26A0\uFE0F Skipped: file ${path8.basename(scenarioPath)} does not export a VisualScenario object by default.`);
|
|
1936
|
-
continue;
|
|
1937
|
-
}
|
|
1938
|
-
const result = await runVisualScenario({
|
|
1939
|
-
scenario,
|
|
1940
|
-
targetMode: options.mode,
|
|
2094
|
+
async function main() {
|
|
2095
|
+
if (isSnapCommand || options.url && !options.scenario && !options.all) {
|
|
2096
|
+
const snapSuccess = await runQuickSnap({
|
|
1941
2097
|
url: options.url,
|
|
1942
|
-
|
|
2098
|
+
start: options.start,
|
|
1943
2099
|
startCwd: options.startCwd,
|
|
2100
|
+
selector: options.selector,
|
|
2101
|
+
viewports: options.viewports,
|
|
2102
|
+
waitMs: options.waitMs,
|
|
2103
|
+
name: options.name,
|
|
2104
|
+
clean: options.clean,
|
|
1944
2105
|
cleanArtifacts: options.cleanArtifacts,
|
|
1945
|
-
port: options.port,
|
|
1946
2106
|
headed: options.headed,
|
|
1947
2107
|
detach: options.detach,
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2108
|
+
outDir: options.outDir,
|
|
2109
|
+
mode: options.mode,
|
|
2110
|
+
exe: options.exe,
|
|
2111
|
+
port: options.port,
|
|
2112
|
+
plugins: options.plugins,
|
|
2113
|
+
fullPage: options.fullPage
|
|
1954
2114
|
});
|
|
1955
|
-
|
|
2115
|
+
process.exit(snapSuccess ? 0 : 1);
|
|
2116
|
+
}
|
|
2117
|
+
if (!options.scenario && !options.all) {
|
|
2118
|
+
console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
|
|
2119
|
+
printHelp();
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
const scenariosDir = resolveScenariosDirectory(options.dir);
|
|
2123
|
+
const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
|
|
2124
|
+
if (scenarioPaths.length === 0) {
|
|
2125
|
+
console.error(`\u274C No scenarios found in ${scenariosDir}`);
|
|
2126
|
+
console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
const shouldBuild = options.build || fileConfig.autoBuild;
|
|
2130
|
+
if (shouldBuild) {
|
|
2131
|
+
const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
|
|
2132
|
+
console.log(`
|
|
2133
|
+
\u{1F528} [Build] Running build process: "${buildCmd}"...`);
|
|
2134
|
+
try {
|
|
2135
|
+
execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
2136
|
+
} catch (e) {
|
|
2137
|
+
console.error(`\u274C Build failed: ${buildCmd}`);
|
|
2138
|
+
process.exit(1);
|
|
2139
|
+
}
|
|
2140
|
+
} else if (options.mode === "preview" && !options.url && !options.start) {
|
|
2141
|
+
console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
|
|
2142
|
+
}
|
|
2143
|
+
console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
|
|
2144
|
+
const jiti = createJiti2(process.cwd());
|
|
2145
|
+
let globalMocks = [];
|
|
2146
|
+
const mockCandidates = [
|
|
2147
|
+
path13.join(scenariosDir, "mocks.ts"),
|
|
2148
|
+
path13.join(scenariosDir, "mocks.js"),
|
|
2149
|
+
path13.join(process.cwd(), "mocks.ts"),
|
|
2150
|
+
path13.join(process.cwd(), "mocks.js")
|
|
2151
|
+
];
|
|
2152
|
+
for (const mocksPath of mockCandidates) {
|
|
2153
|
+
if (fs12.existsSync(mocksPath)) {
|
|
2154
|
+
try {
|
|
2155
|
+
const m = await jiti.import(mocksPath);
|
|
2156
|
+
globalMocks = m.default || m.mocks || [];
|
|
2157
|
+
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${path13.basename(mocksPath)}`);
|
|
2158
|
+
break;
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
if (globalMocks.length > 0 && !options.plugins?.includes("mock-ipc")) {
|
|
2165
|
+
options.plugins = ["mock-ipc", ...options.plugins || []];
|
|
2166
|
+
}
|
|
2167
|
+
let allSuccess = true;
|
|
2168
|
+
for (const scenarioPath of scenarioPaths) {
|
|
2169
|
+
try {
|
|
2170
|
+
const scenarioModule = await jiti.import(scenarioPath);
|
|
2171
|
+
const scenario = scenarioModule.default || scenarioModule.scenario;
|
|
2172
|
+
if (!scenario || typeof scenario.run !== "function") {
|
|
2173
|
+
console.error(`\u26A0\uFE0F Skipped: file ${path13.basename(scenarioPath)} does not export a VisualScenario object by default.`);
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const scenarioPlugins = [...options.plugins || []];
|
|
2177
|
+
if (scenario.mockIpc && scenario.mockIpc.length > 0 && !scenarioPlugins.includes("mock-ipc")) {
|
|
2178
|
+
scenarioPlugins.push("mock-ipc");
|
|
2179
|
+
}
|
|
2180
|
+
const result = await runVisualScenario({
|
|
2181
|
+
scenario,
|
|
2182
|
+
targetMode: options.mode,
|
|
2183
|
+
url: options.url,
|
|
2184
|
+
startCommand: options.start,
|
|
2185
|
+
startCwd: options.startCwd,
|
|
2186
|
+
cleanArtifacts: options.cleanArtifacts,
|
|
2187
|
+
port: options.port,
|
|
2188
|
+
headed: options.headed,
|
|
2189
|
+
detach: options.detach,
|
|
2190
|
+
executablePath: options.exe,
|
|
2191
|
+
cleanPaths: options.clean,
|
|
2192
|
+
desktopEnv: fileConfig.env,
|
|
2193
|
+
wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
|
|
2194
|
+
artifactsRoot: options.outDir ? path13.resolve(process.cwd(), options.outDir) : void 0,
|
|
2195
|
+
globalMocks,
|
|
2196
|
+
plugins: scenarioPlugins
|
|
2197
|
+
});
|
|
2198
|
+
if (!result.success) {
|
|
2199
|
+
allSuccess = false;
|
|
2200
|
+
}
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
console.error(`\u274C Failed to load scenario ${path13.basename(scenarioPath)}:`, err);
|
|
1956
2203
|
allSuccess = false;
|
|
1957
2204
|
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
|
|
2205
|
+
}
|
|
2206
|
+
if (!allSuccess) {
|
|
2207
|
+
process.exit(1);
|
|
1961
2208
|
}
|
|
1962
2209
|
}
|
|
1963
|
-
|
|
2210
|
+
main().catch((err) => {
|
|
2211
|
+
console.error("Fatal error:", err);
|
|
1964
2212
|
process.exit(1);
|
|
1965
|
-
}
|
|
2213
|
+
});
|
|
1966
2214
|
}
|
|
1967
|
-
main().catch((err) => {
|
|
1968
|
-
console.error("Fatal error:", err);
|
|
1969
|
-
process.exit(1);
|
|
1970
|
-
});
|
|
1971
2215
|
}
|
|
1972
2216
|
});
|
|
1973
2217
|
export default require_cli();
|