@ranganathmk/trq 0.1.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.
@@ -0,0 +1,2525 @@
1
+ var __trq_meta_url__ = require("url").pathToFileURL(__filename).href;
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/main/main.ts
26
+ var import_electron2 = require("electron");
27
+ var import_node_path7 = __toESM(require("node:path"));
28
+
29
+ // src/main/ipc.ts
30
+ var import_node_fs6 = require("node:fs");
31
+ var import_node_path6 = __toESM(require("node:path"));
32
+ var import_electron = require("electron");
33
+
34
+ // ../core/dist/index.js
35
+ var import_module = require("module");
36
+ var import_node_path = __toESM(require("node:path"), 1);
37
+ var import_node_url = require("node:url");
38
+ var import_node_fs = require("node:fs");
39
+ var import_node_fs2 = require("node:fs");
40
+ var import_node_fs3 = require("node:fs");
41
+ var import_node_path2 = __toESM(require("node:path"), 1);
42
+ var import_puppeteer = __toESM(require("puppeteer"), 1);
43
+ var import_node_path3 = __toESM(require("node:path"), 1);
44
+ var import_node_url2 = require("node:url");
45
+ var import_puppeteer2 = __toESM(require("puppeteer"), 1);
46
+ var import_node_fs4 = require("node:fs");
47
+ var import_node_path4 = __toESM(require("node:path"), 1);
48
+ var import_node_url3 = require("node:url");
49
+ var import_puppeteer3 = __toESM(require("puppeteer"), 1);
50
+ var import_node_fs5 = require("node:fs");
51
+ var import_node_path5 = __toESM(require("node:path"), 1);
52
+ var import_node_url4 = require("node:url");
53
+ var require2 = (0, import_module.createRequire)(__trq_meta_url__);
54
+ var __defProp2 = Object.defineProperty;
55
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
56
+ var __esm = (fn, res) => function __init() {
57
+ return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
58
+ };
59
+ var __export = (target, all) => {
60
+ for (var name in all)
61
+ __defProp2(target, name, { get: all[name], enumerable: true });
62
+ };
63
+ var page_context_exports = {};
64
+ __export(page_context_exports, {
65
+ cdpPageContext: () => cdpPageContext,
66
+ puppeteerPageContext: () => puppeteerPageContext
67
+ });
68
+ function puppeteerPageContext(page, client) {
69
+ return {
70
+ client,
71
+ evaluate: (fnOrExpr, ...args) => page.evaluate(fnOrExpr, ...args),
72
+ url: () => page.url(),
73
+ goto: async (url, opts) => {
74
+ await page.goto(url, { waitUntil: "networkidle2", timeout: opts?.timeout ?? 3e4 });
75
+ },
76
+ waitForLoad: async (timeout) => {
77
+ await page.waitForNavigation({ timeout: timeout ?? 3e4 }).catch(() => {
78
+ });
79
+ }
80
+ };
81
+ }
82
+ function cdpPageContext(client, options = {}) {
83
+ let currentUrl = options.initialUrl ?? "about:blank";
84
+ let mainFrameId = null;
85
+ void client.send("Page.getFrameTree").then((tree) => {
86
+ const t = tree;
87
+ mainFrameId = t.frameTree?.frame?.id ?? null;
88
+ if (t.frameTree?.frame?.url) currentUrl = t.frameTree.frame.url;
89
+ }).catch(() => {
90
+ });
91
+ client.on("Page.frameNavigated", (raw) => {
92
+ const e = raw;
93
+ if (e.frame.parentId) return;
94
+ if (mainFrameId && e.frame.id !== mainFrameId) return;
95
+ currentUrl = e.frame.url;
96
+ });
97
+ client.on("Page.navigatedWithinDocument", (raw) => {
98
+ const e = raw;
99
+ if (mainFrameId && e.frameId !== mainFrameId) return;
100
+ currentUrl = e.url;
101
+ });
102
+ const waitForLoad = (timeout = 3e4) => new Promise((resolve) => {
103
+ const t = setTimeout(() => {
104
+ client.off?.("Page.loadEventFired", onLoad);
105
+ resolve();
106
+ }, timeout);
107
+ const onLoad = () => {
108
+ clearTimeout(t);
109
+ client.off?.("Page.loadEventFired", onLoad);
110
+ resolve();
111
+ };
112
+ client.on("Page.loadEventFired", onLoad);
113
+ });
114
+ const evaluateExpr = async (expression) => {
115
+ const r = await client.send("Runtime.evaluate", {
116
+ expression,
117
+ awaitPromise: true,
118
+ returnByValue: true
119
+ });
120
+ if (r.exceptionDetails) {
121
+ const desc = r.exceptionDetails.exception?.description ?? r.exceptionDetails.text ?? "evaluate failed";
122
+ throw new Error(desc);
123
+ }
124
+ return r.result?.value;
125
+ };
126
+ return {
127
+ client,
128
+ evaluate: (fnOrExpr, ...args) => {
129
+ if (typeof fnOrExpr === "string") {
130
+ return evaluateExpr(fnOrExpr);
131
+ }
132
+ const body = fnOrExpr.toString();
133
+ const jsonArgs = args.map((a) => JSON.stringify(a)).join(",");
134
+ const expression = `(${body})(${jsonArgs})`;
135
+ return evaluateExpr(expression);
136
+ },
137
+ url: () => currentUrl,
138
+ goto: async (url, opts) => {
139
+ const loaded = waitForLoad(opts?.timeout ?? 3e4);
140
+ await client.send("Page.navigate", { url });
141
+ await loaded;
142
+ await new Promise((r) => setTimeout(r, 200));
143
+ },
144
+ waitForLoad
145
+ };
146
+ }
147
+ var init_page_context = __esm({
148
+ "src/page-context.ts"() {
149
+ "use strict";
150
+ }
151
+ });
152
+ var Session = class _Session {
153
+ constructor(dir, manifest) {
154
+ this.dir = dir;
155
+ this.manifest = manifest;
156
+ }
157
+ writeChain = Promise.resolve();
158
+ static async create(rootDir, name, initialUrl) {
159
+ const dir = import_node_path2.default.join(rootDir, name);
160
+ if ((0, import_node_fs3.existsSync)(dir)) {
161
+ throw new Error(`Session already exists: ${name}`);
162
+ }
163
+ await import_node_fs2.promises.mkdir(dir, { recursive: true });
164
+ const now = (/* @__PURE__ */ new Date()).toISOString();
165
+ const manifest = {
166
+ id: `ses_${randomId()}`,
167
+ name,
168
+ createdAt: now,
169
+ updatedAt: now,
170
+ stepCount: 0,
171
+ initialUrl: initialUrl ?? null,
172
+ lastUrl: initialUrl ?? null
173
+ };
174
+ await import_node_fs2.promises.writeFile(import_node_path2.default.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
175
+ await import_node_fs2.promises.writeFile(import_node_path2.default.join(dir, "events.jsonl"), "");
176
+ return new _Session(dir, manifest);
177
+ }
178
+ static async load(rootDir, name) {
179
+ const dir = import_node_path2.default.join(rootDir, name);
180
+ const raw = await import_node_fs2.promises.readFile(import_node_path2.default.join(dir, "manifest.json"), "utf8");
181
+ return new _Session(dir, JSON.parse(raw));
182
+ }
183
+ static async list(rootDir) {
184
+ if (!(0, import_node_fs3.existsSync)(rootDir)) return [];
185
+ const names = await import_node_fs2.promises.readdir(rootDir);
186
+ const out = [];
187
+ for (const name of names) {
188
+ const mp = import_node_path2.default.join(rootDir, name, "manifest.json");
189
+ if (!(0, import_node_fs3.existsSync)(mp)) continue;
190
+ try {
191
+ out.push(JSON.parse(await import_node_fs2.promises.readFile(mp, "utf8")));
192
+ } catch {
193
+ }
194
+ }
195
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
196
+ }
197
+ // Builds an event ID from its 1-based position. Used by all mutations so
198
+ // IDs stay sequential — after a delete or insert, every event keeps a
199
+ // position-matching id (evt_0001..evt_000N with no gaps).
200
+ idFor(pos1Based) {
201
+ return `evt_${String(pos1Based).padStart(4, "0")}`;
202
+ }
203
+ appendEvent(event) {
204
+ const next = this.writeChain.then(async () => {
205
+ this.manifest.stepCount++;
206
+ const id = this.idFor(this.manifest.stepCount);
207
+ const stamped = { ...event, id };
208
+ await import_node_fs2.promises.appendFile(import_node_path2.default.join(this.dir, "events.jsonl"), JSON.stringify(stamped) + "\n");
209
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
210
+ if (this.manifest.initialUrl === null) this.manifest.initialUrl = event.url;
211
+ this.manifest.lastUrl = event.url;
212
+ await this.flushManifest();
213
+ return stamped;
214
+ });
215
+ this.writeChain = next.catch(() => {
216
+ });
217
+ return next;
218
+ }
219
+ // Replace fields on an existing event (matched by id). Selectors, value,
220
+ // assertion spec, etc. The id itself and the event's index are preserved.
221
+ // Use this for fixing brittle selectors, correcting an input value, etc.
222
+ async replaceEvent(id, patch) {
223
+ const next = this.writeChain.then(async () => {
224
+ const events = await this.readEvents();
225
+ const idx = events.findIndex((e) => e.id === id);
226
+ if (idx === -1) throw new Error(`No event with id ${id}`);
227
+ const merged = { ...events[idx], ...patch, id };
228
+ events[idx] = merged;
229
+ await this.writeAllEvents(events);
230
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
231
+ await this.flushManifest();
232
+ return merged;
233
+ });
234
+ this.writeChain = next.catch(() => {
235
+ });
236
+ return next;
237
+ }
238
+ // Delete an event by id. Subsequent events are renumbered so IDs stay
239
+ // sequential (evt_0001..evt_000N with no gaps). stepCount = new length.
240
+ async removeEvent(id) {
241
+ const next = this.writeChain.then(async () => {
242
+ const events = await this.readEvents();
243
+ const idx = events.findIndex((e) => e.id === id);
244
+ if (idx === -1) throw new Error(`No event with id ${id}`);
245
+ events.splice(idx, 1);
246
+ this.renumber(events);
247
+ this.manifest.stepCount = events.length;
248
+ this.manifest.lastUrl = events[events.length - 1]?.url ?? this.manifest.lastUrl;
249
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
250
+ await this.writeAllEvents(events);
251
+ await this.flushManifest();
252
+ });
253
+ this.writeChain = next.catch(() => {
254
+ });
255
+ return next;
256
+ }
257
+ // Insert a new event at the given 0-based index (so 0 means "before first",
258
+ // events.length means "at end"). The whole event list is renumbered to
259
+ // stay sequential. Returns the stamped event with its post-renumber id.
260
+ async insertEventAt(idx, event) {
261
+ const next = this.writeChain.then(async () => {
262
+ const events = await this.readEvents();
263
+ const safeIdx = Math.max(0, Math.min(idx, events.length));
264
+ const stamped = { ...event, id: "evt_xxxx" };
265
+ events.splice(safeIdx, 0, stamped);
266
+ this.renumber(events);
267
+ this.manifest.stepCount = events.length;
268
+ if (this.manifest.initialUrl === null) this.manifest.initialUrl = event.url;
269
+ this.manifest.lastUrl = events[events.length - 1].url;
270
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
271
+ await this.writeAllEvents(events);
272
+ await this.flushManifest();
273
+ return events[safeIdx];
274
+ });
275
+ this.writeChain = next.catch(() => {
276
+ });
277
+ return next;
278
+ }
279
+ renumber(events) {
280
+ for (let i = 0; i < events.length; i++) {
281
+ events[i].id = this.idFor(i + 1);
282
+ }
283
+ }
284
+ // Create a new session containing events 1..throughStep (inclusive,
285
+ // 1-based) from this session. Copies config.json. Useful for forking a
286
+ // recording at a known-good step to retry a divergent path.
287
+ async branch(rootDir, newName, throughStep) {
288
+ const events = await this.readEvents();
289
+ const target = Math.max(0, Math.min(throughStep, events.length));
290
+ const sliced = events.slice(0, target);
291
+ const branched = await _Session.create(rootDir, newName, this.manifest.initialUrl);
292
+ const lines = [];
293
+ for (let i = 0; i < sliced.length; i++) {
294
+ const id = `evt_${String(i + 1).padStart(4, "0")}`;
295
+ const e = { ...sliced[i], id };
296
+ lines.push(JSON.stringify(e));
297
+ }
298
+ await import_node_fs2.promises.writeFile(import_node_path2.default.join(branched.dir, "events.jsonl"), lines.length ? lines.join("\n") + "\n" : "");
299
+ branched.manifest.stepCount = sliced.length;
300
+ branched.manifest.lastUrl = sliced[sliced.length - 1]?.url ?? branched.manifest.initialUrl;
301
+ branched.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
302
+ await branched.flushManifest();
303
+ const cfg = await this.readConfig();
304
+ if (Object.keys(cfg).length > 0) await branched.writeConfig(cfg);
305
+ return branched;
306
+ }
307
+ async writeAllEvents(events) {
308
+ const body = events.length ? events.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
309
+ await import_node_fs2.promises.writeFile(import_node_path2.default.join(this.dir, "events.jsonl"), body);
310
+ }
311
+ async flushManifest() {
312
+ await import_node_fs2.promises.writeFile(
313
+ import_node_path2.default.join(this.dir, "manifest.json"),
314
+ JSON.stringify(this.manifest, null, 2)
315
+ );
316
+ }
317
+ async readEvents() {
318
+ const raw = await import_node_fs2.promises.readFile(import_node_path2.default.join(this.dir, "events.jsonl"), "utf8");
319
+ return raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l));
320
+ }
321
+ async readConfig() {
322
+ const p = import_node_path2.default.join(this.dir, "config.json");
323
+ if (!(0, import_node_fs3.existsSync)(p)) return {};
324
+ try {
325
+ return JSON.parse(await import_node_fs2.promises.readFile(p, "utf8"));
326
+ } catch {
327
+ return {};
328
+ }
329
+ }
330
+ async writeConfig(cfg) {
331
+ await import_node_fs2.promises.writeFile(
332
+ import_node_path2.default.join(this.dir, "config.json"),
333
+ JSON.stringify(cfg, null, 2)
334
+ );
335
+ }
336
+ };
337
+ function randomId() {
338
+ return Math.random().toString(36).slice(2, 8) + Date.now().toString(36).slice(-4);
339
+ }
340
+ async function safeSend(client, method, params) {
341
+ try {
342
+ await client.send(method, params);
343
+ } catch (err) {
344
+ console.warn(`[config] ${method} failed: ${err.message}`);
345
+ }
346
+ }
347
+ async function applyConfig(client, cfg, options = {}) {
348
+ if (cfg.viewport) {
349
+ await safeSend(client, "Emulation.setDeviceMetricsOverride", {
350
+ width: cfg.viewport.width,
351
+ height: cfg.viewport.height,
352
+ deviceScaleFactor: cfg.viewport.deviceScaleFactor ?? 1,
353
+ mobile: cfg.viewport.mobile ?? false,
354
+ // Critical: without this, Chromium expands the visible BrowserView area
355
+ // to match the device-metrics size, overlapping any UI below it. The
356
+ // visible bounds are owned by view.setBounds elsewhere; this command
357
+ // should only affect what the page reports as its viewport.
358
+ dontSetVisibleSize: true
359
+ });
360
+ }
361
+ if (cfg.locale) {
362
+ await safeSend(client, "Emulation.setLocaleOverride", { locale: cfg.locale });
363
+ }
364
+ if (cfg.timezone) {
365
+ await safeSend(client, "Emulation.setTimezoneOverride", {
366
+ timezoneId: cfg.timezone
367
+ });
368
+ }
369
+ if (cfg.userAgent) {
370
+ await safeSend(client, "Network.setUserAgentOverride", { userAgent: cfg.userAgent });
371
+ }
372
+ if (cfg.extraHeaders && Object.keys(cfg.extraHeaders).length > 0) {
373
+ await safeSend(client, "Network.enable", {});
374
+ await safeSend(client, "Network.setExtraHTTPHeaders", { headers: cfg.extraHeaders });
375
+ }
376
+ if (cfg.bypassCSP) {
377
+ await safeSend(client, "Page.setBypassCSP", { enabled: true });
378
+ }
379
+ if (cfg.permissions && cfg.permissions.length > 0) {
380
+ const params = {
381
+ permissions: cfg.permissions
382
+ };
383
+ if (options.origin) params.origin = options.origin;
384
+ await safeSend(client, "Browser.grantPermissions", params);
385
+ }
386
+ if (cfg.geolocation) {
387
+ await safeSend(client, "Emulation.setGeolocationOverride", {
388
+ latitude: cfg.geolocation.latitude,
389
+ longitude: cfg.geolocation.longitude,
390
+ accuracy: cfg.geolocation.accuracy ?? 10
391
+ });
392
+ }
393
+ }
394
+ var BINDING_NAME = "__trqEmit";
395
+ var isRecordableUrl = (url) => !!url && url !== "about:blank" && !url.startsWith("chrome://") && !url.startsWith("chrome-extension://") && !url.startsWith("devtools://") && !url.startsWith("data:");
396
+ async function broadcastStepCount(ctx) {
397
+ const n = ctx.session.manifest.stepCount;
398
+ const expr = `window.__trqSetStep && window.__trqSetStep(${n})`;
399
+ await Promise.all(
400
+ Array.from(ctx.clients).map(
401
+ (c) => c.send("Runtime.evaluate", { expression: expr }).catch(() => {
402
+ })
403
+ )
404
+ );
405
+ }
406
+ async function recordNav(ctx, url, source) {
407
+ if (!isRecordableUrl(url)) return;
408
+ if (source === "load" && isAnchorClickPrev(ctx.lastEmittedEvent)) return;
409
+ const stamped = await ctx.session.appendEvent({
410
+ type: "navigate",
411
+ timestamp: Date.now(),
412
+ url,
413
+ frame: "main",
414
+ source
415
+ });
416
+ ctx.lastEmittedEvent = stamped;
417
+ ctx.onEvent?.(stamped);
418
+ await broadcastStepCount(ctx);
419
+ }
420
+ function isAnchorClickPrev(prev) {
421
+ if (!prev || prev.type !== "click") return false;
422
+ const t = prev.target;
423
+ return t.tagName === "A" || t.attributes?.target === "_blank" || !!t.attributes?.href;
424
+ }
425
+ async function setupRecordingOnClient(client, ctx) {
426
+ ctx.clients.add(client);
427
+ client.on("disconnected", () => ctx.clients.delete(client));
428
+ await client.send("Runtime.enable");
429
+ await client.send("Page.enable");
430
+ if (ctx.config) {
431
+ await applyConfig(client, ctx.config);
432
+ }
433
+ await client.send("Runtime.addBinding", { name: BINDING_NAME }).catch(() => {
434
+ });
435
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: ctx.bootstrap });
436
+ let mainFrameId = null;
437
+ try {
438
+ const tree = await client.send("Page.getFrameTree");
439
+ mainFrameId = tree.frameTree.frame.id;
440
+ } catch {
441
+ }
442
+ let lastNavUrl = null;
443
+ client.on("Runtime.bindingCalled", async (raw) => {
444
+ const event = raw;
445
+ if (event.name !== BINDING_NAME) return;
446
+ try {
447
+ const incoming = JSON.parse(event.payload);
448
+ if (incoming.__trq === "ready") {
449
+ await broadcastStepCount(ctx);
450
+ return;
451
+ }
452
+ const stamped = await ctx.session.appendEvent(
453
+ incoming
454
+ );
455
+ ctx.lastEmittedEvent = stamped;
456
+ ctx.onEvent?.(stamped);
457
+ await broadcastStepCount(ctx);
458
+ } catch (err) {
459
+ console.error("\nFailed to handle event:", err.message);
460
+ }
461
+ });
462
+ client.on("Page.frameNavigated", async (raw) => {
463
+ const e = raw;
464
+ if (e.frame.parentId) return;
465
+ const url = e.frame.url;
466
+ if (!isRecordableUrl(url)) return;
467
+ if (url === lastNavUrl) return;
468
+ lastNavUrl = url;
469
+ await recordNav(ctx, url, "load");
470
+ });
471
+ client.on("Page.navigatedWithinDocument", async (raw) => {
472
+ const e = raw;
473
+ if (mainFrameId && e.frameId !== mainFrameId) return;
474
+ if (!isRecordableUrl(e.url)) return;
475
+ if (e.url === lastNavUrl) return;
476
+ lastNavUrl = e.url;
477
+ await recordNav(ctx, e.url, "in-document");
478
+ });
479
+ await client.send("Runtime.evaluate", { expression: ctx.bootstrap }).catch(() => {
480
+ });
481
+ await broadcastStepCount(ctx);
482
+ }
483
+ var __dirname2 = import_node_path3.default.dirname((0, import_node_url2.fileURLToPath)(__trq_meta_url__));
484
+ var BOOTSTRAP_PATH = import_node_path3.default.join(__dirname2, "bootstrap.js");
485
+ init_page_context();
486
+ var __dirname22 = import_node_path4.default.dirname((0, import_node_url3.fileURLToPath)(__trq_meta_url__));
487
+ var HELPERS_PATH = import_node_path4.default.join(__dirname22, "replay-helpers.js");
488
+ var ANIMATION_KILL_CSS = `
489
+ *, *::before, *::after {
490
+ animation-duration: 0s !important;
491
+ animation-delay: 0s !important;
492
+ transition-duration: 0s !important;
493
+ transition-delay: 0s !important;
494
+ scroll-behavior: auto !important;
495
+ }
496
+ `;
497
+ var INJECT_ANIMATION_KILL = `
498
+ (function () {
499
+ try {
500
+ const s = document.createElement('style');
501
+ s.setAttribute('data-trq', 'animkill');
502
+ s.textContent = ${JSON.stringify(ANIMATION_KILL_CSS)};
503
+ (document.head || document.documentElement).appendChild(s);
504
+ } catch (e) {}
505
+ })();
506
+ `;
507
+ function modBits(mods) {
508
+ let b = 0;
509
+ for (const m of mods) {
510
+ if (m === "Alt") b |= 1;
511
+ if (m === "Control") b |= 2;
512
+ if (m === "Meta") b |= 4;
513
+ if (m === "Shift") b |= 8;
514
+ }
515
+ return b;
516
+ }
517
+ var MOUSE_BUTTON = {
518
+ left: "left",
519
+ right: "right",
520
+ middle: "middle"
521
+ };
522
+ async function waitFor(predicate, timeout = 5e3, pollMs = 100) {
523
+ const start = Date.now();
524
+ while (Date.now() - start < timeout) {
525
+ if (await predicate()) return true;
526
+ await new Promise((r) => setTimeout(r, pollMs));
527
+ }
528
+ return false;
529
+ }
530
+ async function waitForResolvable(ctx, selectors, timeout = 5e3) {
531
+ const ok = await waitFor(
532
+ async () => await ctx.evaluate((sels) => {
533
+ const w = window;
534
+ return !!w.__trqResolve?.(sels)?.found;
535
+ }, selectors).catch(() => false),
536
+ timeout
537
+ );
538
+ if (!ok) {
539
+ throw new Error(`selector did not resolve within ${timeout}ms: ${selectors[0]?.[0] ?? "?"}`);
540
+ }
541
+ }
542
+ async function replayClick(e, ctx) {
543
+ await waitForResolvable(ctx, e.selectors);
544
+ const result = await ctx.evaluate(
545
+ (sels) => window.__trqClick(sels),
546
+ e.selectors
547
+ );
548
+ if (result.error || !result.rect) throw new Error(result.error || "no rect from click resolve");
549
+ const { rect } = result;
550
+ const x = rect.x + Math.min(e.click.offsetX, rect.w);
551
+ const y = rect.y + Math.min(e.click.offsetY, rect.h);
552
+ const button = MOUSE_BUTTON[e.click.button] ?? "left";
553
+ const modifiers = modBits(e.click.modifiers);
554
+ await ctx.client.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
555
+ await ctx.client.send("Input.dispatchMouseEvent", {
556
+ type: "mousePressed",
557
+ x,
558
+ y,
559
+ button,
560
+ modifiers,
561
+ clickCount: e.click.clickCount
562
+ });
563
+ await ctx.client.send("Input.dispatchMouseEvent", {
564
+ type: "mouseReleased",
565
+ x,
566
+ y,
567
+ button,
568
+ modifiers,
569
+ clickCount: e.click.clickCount
570
+ });
571
+ }
572
+ async function replayInput(e, ctx) {
573
+ await waitForResolvable(ctx, e.selectors);
574
+ const result = await ctx.evaluate(
575
+ (sels, val) => window.__trqSetInput(sels, val),
576
+ e.selectors,
577
+ e.value
578
+ );
579
+ if (result.error) throw new Error(result.error);
580
+ }
581
+ async function replayKey(e, ctx) {
582
+ await ctx.evaluate(
583
+ (sels) => window.__trqFocus?.(sels),
584
+ e.selectors
585
+ ).catch(() => {
586
+ });
587
+ const mods = modBits(e.modifiers);
588
+ await ctx.client.send("Input.dispatchKeyEvent", {
589
+ type: "keyDown",
590
+ key: e.key,
591
+ code: e.key,
592
+ modifiers: mods
593
+ });
594
+ await ctx.client.send("Input.dispatchKeyEvent", {
595
+ type: "keyUp",
596
+ key: e.key,
597
+ code: e.key,
598
+ modifiers: mods
599
+ });
600
+ }
601
+ async function replayAssert(e, ctx) {
602
+ await waitForResolvable(ctx, e.selectors);
603
+ const result = await ctx.evaluate(
604
+ (sels, spec) => window.__trqAssert(sels, spec),
605
+ e.selectors,
606
+ e.assertion
607
+ );
608
+ if (!result.ok) throw new Error(result.reason ?? "assertion failed");
609
+ }
610
+ async function replayNavigate(e, ctx, prev) {
611
+ const wasUserAction = prev && (prev.type === "click" || prev.type === "key");
612
+ const prevClickTarget = prev?.type === "click" ? prev.target : null;
613
+ const prevIsAnchorClick = !!prevClickTarget && (prevClickTarget.tagName === "A" || prevClickTarget.attributes?.target === "_blank" || !!prevClickTarget.attributes?.href);
614
+ if (e.source === "in-document") {
615
+ const ok = await waitFor(async () => ctx.url() === e.url, 5e3);
616
+ if (!ok) {
617
+ throw new Error(`in-document nav to ${e.url} did not occur (still at ${ctx.url()})`);
618
+ }
619
+ return;
620
+ }
621
+ if (prevIsAnchorClick) {
622
+ const before = ctx.url();
623
+ const ok = await waitFor(async () => ctx.url() !== before, 1e4);
624
+ if (!ok) {
625
+ throw new Error(`anchor click did not trigger a navigation (still at ${before})`);
626
+ }
627
+ await waitFor(
628
+ async () => await ctx.evaluate(() => document.readyState) === "complete",
629
+ 5e3
630
+ ).catch(() => false);
631
+ return;
632
+ }
633
+ if (wasUserAction) {
634
+ const ok = await waitFor(async () => ctx.url() === e.url, 5e3);
635
+ if (ok) return;
636
+ }
637
+ await ctx.goto(e.url, { timeout: 3e4 });
638
+ }
639
+ function isAnchorClick(e) {
640
+ if (e.type !== "click") return false;
641
+ const t = e.target;
642
+ return t.tagName === "A" || t.attributes?.target === "_blank" || !!t.attributes?.href;
643
+ }
644
+ function urlAreaOf(url) {
645
+ if (!url) return null;
646
+ try {
647
+ const u = new URL(url);
648
+ const segs = u.pathname.split("/").filter((s) => s.length > 0);
649
+ return { origin: u.origin, head: segs[0] ?? "" };
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ function urlInArea(url, area) {
655
+ try {
656
+ const u = new URL(url);
657
+ const head = u.pathname.split("/").filter((s) => s.length > 0)[0] ?? "";
658
+ return u.origin === area.origin && head === area.head;
659
+ } catch {
660
+ return false;
661
+ }
662
+ }
663
+ async function waitForAnchorClickConsequence(getActiveClient, fallbackClient, ctxFor, clientBefore, expectedNextUrl, nextEventSelectors, switchTimeoutMs = 3e3, totalTimeoutMs = 2e4, stableWindowMs = 1200) {
664
+ const start = Date.now();
665
+ while (Date.now() - start < switchTimeoutMs) {
666
+ const cNow = getActiveClient?.() ?? fallbackClient;
667
+ if (cNow !== clientBefore) break;
668
+ await new Promise((r) => setTimeout(r, 50));
669
+ }
670
+ const cFinal = getActiveClient?.() ?? fallbackClient;
671
+ const ctxFinal = ctxFor(cFinal);
672
+ const expectedArea = urlAreaOf(expectedNextUrl);
673
+ let lastUrl = null;
674
+ let stableSince = Date.now();
675
+ while (Date.now() - start < totalTimeoutMs) {
676
+ let ready = false;
677
+ try {
678
+ const v = await ctxFinal.evaluate(() => document.readyState);
679
+ ready = v === "complete";
680
+ } catch {
681
+ }
682
+ const u = ctxFinal.url();
683
+ if (u !== lastUrl) {
684
+ lastUrl = u;
685
+ stableSince = Date.now();
686
+ }
687
+ const inArea = expectedArea ? urlInArea(u, expectedArea) : true;
688
+ if (ready && inArea && Date.now() - stableSince >= stableWindowMs) break;
689
+ await new Promise((r) => setTimeout(r, 150));
690
+ }
691
+ if (nextEventSelectors && nextEventSelectors.length > 0) {
692
+ const remaining = Math.max(0, totalTimeoutMs - (Date.now() - start));
693
+ await waitFor(
694
+ async () => {
695
+ try {
696
+ const found = await ctxFinal.evaluate((sels) => {
697
+ const w = window;
698
+ return !!w.__trqResolve?.(sels)?.found;
699
+ }, nextEventSelectors);
700
+ return found;
701
+ } catch {
702
+ return false;
703
+ }
704
+ },
705
+ remaining
706
+ ).catch(() => false);
707
+ }
708
+ }
709
+ async function setupReplayClient(client, opts = {}) {
710
+ const helpers = await import_node_fs4.promises.readFile(HELPERS_PATH, "utf8");
711
+ await client.send("Page.enable");
712
+ await client.send("Runtime.enable");
713
+ if (opts.config) {
714
+ await applyConfig(client, opts.config);
715
+ }
716
+ if (process.env.TRQ_DEBUG_REPLAY) {
717
+ const setFlag = "window.__trqDebugResolve = true;";
718
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: setFlag });
719
+ await client.send("Runtime.evaluate", { expression: setFlag }).catch(() => {
720
+ });
721
+ }
722
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
723
+ if (opts.killAnimations !== false) {
724
+ await client.send("Page.addScriptToEvaluateOnNewDocument", {
725
+ source: INJECT_ANIMATION_KILL
726
+ });
727
+ }
728
+ await client.send("Runtime.evaluate", { expression: helpers }).catch(() => {
729
+ });
730
+ if (opts.killAnimations !== false) {
731
+ await client.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
732
+ });
733
+ }
734
+ }
735
+ async function runReplayOnClient(opts) {
736
+ let events = await opts.session.readEvents();
737
+ let total = events.length;
738
+ let target = opts.toStep != null ? Math.min(opts.toStep, total) : total;
739
+ const config = await opts.session.readConfig();
740
+ await setupReplayClient(opts.client, {
741
+ config,
742
+ killAnimations: opts.killAnimations
743
+ });
744
+ const { cdpPageContext: cdpPageContext2 } = await Promise.resolve().then(() => (init_page_context(), page_context_exports));
745
+ const ctxCache = /* @__PURE__ */ new Map();
746
+ const ctxFor = (c) => {
747
+ let cached = ctxCache.get(c);
748
+ if (!cached) {
749
+ cached = cdpPageContext2(c);
750
+ ctxCache.set(c, cached);
751
+ }
752
+ return cached;
753
+ };
754
+ const activeCtx = () => ctxFor(opts.getActiveClient?.() ?? opts.client);
755
+ if (opts.startUrl && opts.startUrl !== "about:blank") {
756
+ await activeCtx().goto(opts.startUrl).catch(() => {
757
+ });
758
+ }
759
+ let completed = 0;
760
+ let prev = null;
761
+ for (let i = 0; i < target; i++) {
762
+ if (opts.shouldCancel?.()) {
763
+ return { completed, total };
764
+ }
765
+ const e = events[i];
766
+ await opts.shouldWait?.(i, target, e);
767
+ if (opts.shouldCancel?.()) {
768
+ return { completed, total };
769
+ }
770
+ opts.onProgress?.(i + 1, target, e, "start");
771
+ const ctx = activeCtx();
772
+ if (process.env.TRQ_DEBUG_REPLAY) {
773
+ console.log(`[replay] step ${i + 1}/${target} ${e.type} @ ${ctx.url()}`);
774
+ }
775
+ try {
776
+ const clientBefore = opts.getActiveClient?.() ?? opts.client;
777
+ switch (e.type) {
778
+ case "navigate":
779
+ await replayNavigate(e, ctx, prev);
780
+ break;
781
+ case "click":
782
+ await replayClick(e, ctx);
783
+ break;
784
+ case "input":
785
+ await replayInput(e, ctx);
786
+ break;
787
+ case "key":
788
+ await replayKey(e, ctx);
789
+ break;
790
+ case "assert":
791
+ await replayAssert(e, ctx);
792
+ break;
793
+ }
794
+ if (e.type === "click" && isAnchorClick(e)) {
795
+ const next = events[i + 1];
796
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
797
+ const nextSelectors = next && "selectors" in next ? next.selectors : void 0;
798
+ await waitForAnchorClickConsequence(
799
+ opts.getActiveClient,
800
+ opts.client,
801
+ ctxFor,
802
+ clientBefore,
803
+ expectedNextUrl,
804
+ nextSelectors
805
+ );
806
+ }
807
+ const settle = opts.slow ? 400 : e.type === "click" || e.type === "key" ? 60 : 20;
808
+ if (settle) await new Promise((r) => setTimeout(r, settle));
809
+ completed++;
810
+ prev = e;
811
+ opts.onProgress?.(i + 1, target, e, "ok");
812
+ } catch (err) {
813
+ const msg = err.message;
814
+ opts.onProgress?.(i + 1, target, e, "fail", msg);
815
+ if (opts.shouldPauseOnFail) {
816
+ const action = await opts.shouldPauseOnFail(i, target, e, msg);
817
+ if (action === "retry" || action === "skip") {
818
+ events = await opts.session.readEvents();
819
+ total = events.length;
820
+ target = opts.toStep != null ? Math.min(opts.toStep, total) : total;
821
+ if (action === "retry") {
822
+ const newIdx2 = events.findIndex((ev) => ev.id === e.id);
823
+ i = (newIdx2 === -1 ? i : newIdx2) - 1;
824
+ continue;
825
+ }
826
+ const newIdx = events.findIndex((ev) => ev.id === e.id);
827
+ if (newIdx !== -1) i = newIdx;
828
+ prev = events[i] ?? e;
829
+ continue;
830
+ }
831
+ }
832
+ return {
833
+ completed,
834
+ total,
835
+ failed: { step: i + 1, reason: msg, event: e }
836
+ };
837
+ }
838
+ }
839
+ return { completed, total };
840
+ }
841
+ init_page_context();
842
+ var __dirname3 = import_node_path5.default.dirname((0, import_node_url4.fileURLToPath)(__trq_meta_url__));
843
+ var BOOTSTRAP_PATH2 = import_node_path5.default.join(__dirname3, "bootstrap.js");
844
+ var BINDING_NAME2 = "__trqEmit";
845
+ async function runResumeOnClient(opts) {
846
+ let events = await opts.session.readEvents();
847
+ let total = events.length;
848
+ const helpers = await import_node_fs5.promises.readFile(HELPERS_PATH, "utf8");
849
+ const bootstrap = await import_node_fs5.promises.readFile(BOOTSTRAP_PATH2, "utf8");
850
+ const config = await opts.session.readConfig();
851
+ const client = opts.client;
852
+ let mode = "replaying";
853
+ const broadcastStepCount2 = async () => {
854
+ const n = opts.session.manifest.stepCount;
855
+ await client.send("Runtime.evaluate", {
856
+ expression: `window.__trqSetStep && window.__trqSetStep(${n})`
857
+ }).catch(() => {
858
+ });
859
+ };
860
+ const broadcastMode = async (m) => {
861
+ await client.send("Runtime.evaluate", {
862
+ expression: `window.__trqMode = ${JSON.stringify(m)};`
863
+ }).catch(() => {
864
+ });
865
+ };
866
+ await client.send("Runtime.enable");
867
+ await client.send("Page.enable");
868
+ await applyConfig(client, config);
869
+ await client.send("Runtime.addBinding", { name: BINDING_NAME2 }).catch(() => {
870
+ });
871
+ await client.send("Page.addScriptToEvaluateOnNewDocument", {
872
+ source: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
873
+ });
874
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
875
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: bootstrap });
876
+ if (opts.killAnimations !== false) {
877
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: INJECT_ANIMATION_KILL });
878
+ }
879
+ client.on("Runtime.bindingCalled", async (raw) => {
880
+ const event = raw;
881
+ if (event.name !== BINDING_NAME2) return;
882
+ try {
883
+ const incoming = JSON.parse(event.payload);
884
+ if (incoming.__trq === "ready") {
885
+ await broadcastStepCount2();
886
+ return;
887
+ }
888
+ if (mode !== "recording") return;
889
+ const stamped = await opts.session.appendEvent(incoming);
890
+ opts.onRecordEvent?.(stamped);
891
+ await broadcastStepCount2();
892
+ } catch (err) {
893
+ console.error("\nFailed to handle event:", err.message);
894
+ }
895
+ });
896
+ await client.send("Runtime.evaluate", {
897
+ expression: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
898
+ }).catch(() => {
899
+ });
900
+ await client.send("Runtime.evaluate", { expression: helpers }).catch(() => {
901
+ });
902
+ await client.send("Runtime.evaluate", { expression: bootstrap }).catch(() => {
903
+ });
904
+ if (opts.killAnimations !== false) {
905
+ await client.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
906
+ });
907
+ }
908
+ const ctxCache = /* @__PURE__ */ new Map();
909
+ const ctxFor = (c) => {
910
+ let cached = ctxCache.get(c);
911
+ if (!cached) {
912
+ cached = cdpPageContext(c);
913
+ ctxCache.set(c, cached);
914
+ }
915
+ return cached;
916
+ };
917
+ const activeCtx = () => ctxFor(opts.getActiveClient?.() ?? client);
918
+ if (opts.startUrl && opts.startUrl !== "about:blank") {
919
+ await activeCtx().goto(opts.startUrl).catch(() => {
920
+ });
921
+ }
922
+ let prev = null;
923
+ for (let i = 0; i < total; i++) {
924
+ if (opts.shouldCancel?.()) return { replayed: i };
925
+ const e = events[i];
926
+ opts.onReplayProgress?.(i + 1, total, e, "start");
927
+ const ctx = activeCtx();
928
+ try {
929
+ const clientBefore = opts.getActiveClient?.() ?? client;
930
+ switch (e.type) {
931
+ case "navigate":
932
+ await replayNavigate(e, ctx, prev);
933
+ break;
934
+ case "click":
935
+ await replayClick(e, ctx);
936
+ break;
937
+ case "input":
938
+ await replayInput(e, ctx);
939
+ break;
940
+ case "key":
941
+ await replayKey(e, ctx);
942
+ break;
943
+ case "assert":
944
+ await replayAssert(e, ctx);
945
+ break;
946
+ }
947
+ if (e.type === "click" && isAnchorClick(e)) {
948
+ const next = events[i + 1];
949
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
950
+ const nextSelectors = next && "selectors" in next ? next.selectors : void 0;
951
+ await waitForAnchorClickConsequence(
952
+ opts.getActiveClient,
953
+ client,
954
+ ctxFor,
955
+ clientBefore,
956
+ expectedNextUrl,
957
+ nextSelectors
958
+ );
959
+ }
960
+ const settle = e.type === "click" || e.type === "key" ? 60 : 20;
961
+ await new Promise((r) => setTimeout(r, settle));
962
+ prev = e;
963
+ opts.onReplayProgress?.(i + 1, total, e, "ok");
964
+ } catch (err) {
965
+ const msg = err.message;
966
+ opts.onReplayProgress?.(i + 1, total, e, "fail", msg);
967
+ if (opts.shouldPauseOnFail) {
968
+ const action = await opts.shouldPauseOnFail(i, total, e, msg);
969
+ if (action === "retry" || action === "skip") {
970
+ events = await opts.session.readEvents();
971
+ total = events.length;
972
+ if (action === "retry") {
973
+ const newIdx2 = events.findIndex((ev) => ev.id === e.id);
974
+ i = (newIdx2 === -1 ? i : newIdx2) - 1;
975
+ continue;
976
+ }
977
+ const newIdx = events.findIndex((ev) => ev.id === e.id);
978
+ if (newIdx !== -1) i = newIdx;
979
+ prev = events[i] ?? e;
980
+ continue;
981
+ }
982
+ }
983
+ return { replayed: i, failed: { step: i + 1, reason: msg } };
984
+ }
985
+ }
986
+ mode = "recording";
987
+ await broadcastMode("recording");
988
+ await broadcastStepCount2();
989
+ opts.onRecordingStart?.();
990
+ return { replayed: total };
991
+ }
992
+ init_page_context();
993
+ var __dirname4 = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(__trq_meta_url__));
994
+ var BOOTSTRAP_PATH3 = import_node_path.default.join(__dirname4, "bootstrap.js");
995
+ async function loadBootstrap() {
996
+ return import_node_fs.promises.readFile(BOOTSTRAP_PATH3, "utf8");
997
+ }
998
+ async function loadReplayHelpers() {
999
+ return import_node_fs.promises.readFile(import_node_path.default.join(__dirname4, "replay-helpers.js"), "utf8");
1000
+ }
1001
+
1002
+ // src/main/cdp-adapter.ts
1003
+ function createCDPClient(webContents) {
1004
+ const dbg = webContents.debugger;
1005
+ if (!dbg.isAttached()) {
1006
+ dbg.attach("1.3");
1007
+ }
1008
+ const listeners = /* @__PURE__ */ new Map();
1009
+ const onMessage = (_event, method, params) => {
1010
+ if (process.env.TRQ_DEBUG_CDP) {
1011
+ console.log(`[cdp-adapter] \u2190 ${method}`);
1012
+ }
1013
+ const set = listeners.get(method);
1014
+ if (!set) return;
1015
+ for (const fn of set) {
1016
+ try {
1017
+ fn(params);
1018
+ } catch (err) {
1019
+ console.error(`[cdp-adapter] listener for ${method} threw:`, err);
1020
+ }
1021
+ }
1022
+ };
1023
+ const onDetach = (_event, reason) => {
1024
+ const set = listeners.get("disconnected");
1025
+ if (!set) return;
1026
+ for (const fn of set) {
1027
+ try {
1028
+ fn({ reason });
1029
+ } catch (err) {
1030
+ console.error("[cdp-adapter] disconnected listener threw:", err);
1031
+ }
1032
+ }
1033
+ };
1034
+ dbg.on("message", onMessage);
1035
+ dbg.on("detach", onDetach);
1036
+ return {
1037
+ send: (method, params) => dbg.sendCommand(method, params ?? {}),
1038
+ on: (event, listener) => {
1039
+ let set = listeners.get(event);
1040
+ if (!set) {
1041
+ set = /* @__PURE__ */ new Set();
1042
+ listeners.set(event, set);
1043
+ }
1044
+ set.add(listener);
1045
+ },
1046
+ off: (event, listener) => {
1047
+ listeners.get(event)?.delete(listener);
1048
+ },
1049
+ detach: () => {
1050
+ dbg.removeListener("message", onMessage);
1051
+ dbg.removeListener("detach", onDetach);
1052
+ listeners.clear();
1053
+ if (dbg.isAttached()) dbg.detach();
1054
+ },
1055
+ get attached() {
1056
+ return dbg.isAttached();
1057
+ }
1058
+ };
1059
+ }
1060
+
1061
+ // src/main/ipc.ts
1062
+ function emit(win, channel, payload) {
1063
+ if (win.isDestroyed()) return;
1064
+ win.webContents.send(channel, payload);
1065
+ }
1066
+ function broadcastStatus(state) {
1067
+ emit(state.win, "session:status", {
1068
+ mode: state.mode,
1069
+ session: state.session?.manifest.name ?? null,
1070
+ stepCount: state.session?.manifest.stepCount ?? 0
1071
+ });
1072
+ }
1073
+ async function readEvents(dir) {
1074
+ const raw = await import_node_fs6.promises.readFile(import_node_path6.default.join(dir, "events.jsonl"), "utf8").catch(() => "");
1075
+ if (!raw.trim()) return [];
1076
+ return raw.trim().split("\n").map((line) => JSON.parse(line));
1077
+ }
1078
+ async function loadBlankSafe(view) {
1079
+ try {
1080
+ await view.webContents.loadURL("about:blank");
1081
+ } catch (err) {
1082
+ if (err.errno !== -3) throw err;
1083
+ }
1084
+ }
1085
+ var nextTabId = 1;
1086
+ function tabSummaries(state) {
1087
+ return state.tabs.map((t) => ({
1088
+ id: t.id,
1089
+ url: t.url,
1090
+ title: t.title,
1091
+ active: t.id === state.activeTabId
1092
+ }));
1093
+ }
1094
+ function broadcastTabs(state) {
1095
+ emit(state.win, "tabs:state", { tabs: tabSummaries(state) });
1096
+ }
1097
+ function wireUpTab(state, tab) {
1098
+ const wc = tab.view.webContents;
1099
+ wc.on("did-navigate", (_e, url) => {
1100
+ tab.url = url;
1101
+ broadcastTabs(state);
1102
+ if (tab.id === state.activeTabId) state.invalidateLayout();
1103
+ });
1104
+ wc.on("did-navigate-in-page", (_e, url) => {
1105
+ tab.url = url;
1106
+ broadcastTabs(state);
1107
+ });
1108
+ wc.on("page-title-updated", (_e, title) => {
1109
+ tab.title = title;
1110
+ broadcastTabs(state);
1111
+ });
1112
+ wc.on("did-finish-load", () => {
1113
+ if (tab.id === state.activeTabId) state.invalidateLayout();
1114
+ if (state.mode === "recording" && tab.client && state.recordingContext) {
1115
+ const bootstrap = state.recordingContext.bootstrap;
1116
+ const client = tab.client;
1117
+ void (async () => {
1118
+ try {
1119
+ await client.send("Runtime.evaluate", {
1120
+ expression: `(() => {
1121
+ if (document.getElementById('__trq_overlay')) return;
1122
+ window.__trqInstalled = false;
1123
+ })()`
1124
+ });
1125
+ await client.send("Runtime.evaluate", { expression: bootstrap });
1126
+ } catch {
1127
+ }
1128
+ })();
1129
+ }
1130
+ });
1131
+ wc.on("did-stop-loading", () => {
1132
+ if (tab.id === state.activeTabId) state.invalidateLayout();
1133
+ });
1134
+ wc.on("console-message", (_e, level, message) => {
1135
+ if (tab.id !== state.activeTabId) return;
1136
+ const tag = ["log", "warn", "error"][level] ?? "log";
1137
+ console.log(`[page:${tag}] ${message}`);
1138
+ });
1139
+ wc.setWindowOpenHandler(({ url }) => {
1140
+ void createTab(state, url);
1141
+ return { action: "deny" };
1142
+ });
1143
+ }
1144
+ async function createTab(state, url) {
1145
+ const view = new import_electron.BrowserView({
1146
+ webPreferences: { contextIsolation: true, nodeIntegration: false }
1147
+ });
1148
+ const tab = {
1149
+ id: `tab_${nextTabId++}`,
1150
+ view,
1151
+ url,
1152
+ title: "",
1153
+ client: null
1154
+ };
1155
+ state.tabs.push(tab);
1156
+ wireUpTab(state, tab);
1157
+ if (state.mode === "recording" && state.recordingContext) {
1158
+ try {
1159
+ await loadBlankSafe(view);
1160
+ const client = createCDPClient(view.webContents);
1161
+ tab.client = client;
1162
+ await setupRecordingOnClient(client, state.recordingContext);
1163
+ } catch (err) {
1164
+ console.warn("[createTab] failed to attach recording:", err.message);
1165
+ }
1166
+ }
1167
+ if (state.inspectActive && state.inspectAssets) {
1168
+ try {
1169
+ await loadBlankSafe(view);
1170
+ const client = createCDPClient(view.webContents);
1171
+ tab.client = client;
1172
+ const { bootstrap, helpers, config } = state.inspectAssets;
1173
+ await setupInspectOnClient(state, client, bootstrap, helpers, config);
1174
+ } catch (err) {
1175
+ console.warn("[createTab] failed to attach inspect:", err.message);
1176
+ }
1177
+ }
1178
+ if (state.mode === "replaying" && state.session) {
1179
+ try {
1180
+ await loadBlankSafe(view);
1181
+ const client = createCDPClient(view.webContents);
1182
+ tab.client = client;
1183
+ const config = await state.session.readConfig();
1184
+ await setupReplayClient(client, { config });
1185
+ } catch (err) {
1186
+ console.warn("[createTab] failed to attach replay client:", err.message);
1187
+ }
1188
+ }
1189
+ await switchToTab(state, tab.id);
1190
+ view.webContents.loadURL(url).catch(() => {
1191
+ });
1192
+ return tab;
1193
+ }
1194
+ async function switchToTab(state, id) {
1195
+ const next = state.tabs.find((t) => t.id === id);
1196
+ if (!next) return;
1197
+ for (const t of state.tabs) {
1198
+ try {
1199
+ state.win.removeBrowserView(t.view);
1200
+ } catch {
1201
+ }
1202
+ }
1203
+ state.win.addBrowserView(next.view);
1204
+ state.activeTabId = id;
1205
+ state.view = next.view;
1206
+ state.recordingClient = next.client;
1207
+ state.invalidateLayout();
1208
+ broadcastTabs(state);
1209
+ }
1210
+ async function closeAuxiliaryTabs(state) {
1211
+ const primary = state.tabs[0];
1212
+ if (!primary) return;
1213
+ if (state.activeTabId !== primary.id) {
1214
+ await switchToTab(state, primary.id);
1215
+ }
1216
+ for (const t of state.tabs.slice(1)) {
1217
+ try {
1218
+ t.client?.detach();
1219
+ } catch {
1220
+ }
1221
+ try {
1222
+ state.win.removeBrowserView(t.view);
1223
+ } catch {
1224
+ }
1225
+ try {
1226
+ t.view.webContents.destroy?.();
1227
+ } catch {
1228
+ }
1229
+ }
1230
+ state.tabs = [primary];
1231
+ broadcastTabs(state);
1232
+ }
1233
+ async function closeTab(state, id) {
1234
+ const idx = state.tabs.findIndex((t) => t.id === id);
1235
+ if (idx === -1) return;
1236
+ const tab = state.tabs[idx];
1237
+ if (state.tabs.length === 1) {
1238
+ try {
1239
+ tab.client?.detach();
1240
+ } catch {
1241
+ }
1242
+ tab.client = null;
1243
+ tab.url = "about:blank";
1244
+ tab.title = "";
1245
+ await loadBlankSafe(tab.view);
1246
+ broadcastTabs(state);
1247
+ return;
1248
+ }
1249
+ try {
1250
+ tab.client?.detach();
1251
+ } catch {
1252
+ }
1253
+ try {
1254
+ state.win.removeBrowserView(tab.view);
1255
+ } catch {
1256
+ }
1257
+ try {
1258
+ tab.view.webContents.destroy?.();
1259
+ } catch {
1260
+ }
1261
+ state.tabs.splice(idx, 1);
1262
+ if (state.activeTabId === id) {
1263
+ const neighbor = state.tabs[Math.max(0, idx - 1)] ?? state.tabs[state.tabs.length - 1];
1264
+ await switchToTab(state, neighbor.id);
1265
+ } else {
1266
+ broadcastTabs(state);
1267
+ }
1268
+ }
1269
+ function detachDebugger(state) {
1270
+ try {
1271
+ state.recordingClient?.detach();
1272
+ } catch {
1273
+ }
1274
+ state.recordingClient = null;
1275
+ try {
1276
+ const dbg = state.view.webContents.debugger;
1277
+ if (dbg.isAttached()) dbg.detach();
1278
+ } catch {
1279
+ }
1280
+ }
1281
+ async function clearBrowserState(state) {
1282
+ try {
1283
+ await state.view.webContents.session.clearStorageData({
1284
+ storages: [
1285
+ "cookies",
1286
+ "localstorage",
1287
+ "sessionstorage",
1288
+ "indexdb",
1289
+ "cachestorage",
1290
+ "serviceworkers",
1291
+ "shadercache",
1292
+ "websql"
1293
+ ]
1294
+ });
1295
+ await state.view.webContents.session.clearCache();
1296
+ } catch (err) {
1297
+ console.warn("[clearBrowserState] failed:", err.message);
1298
+ }
1299
+ }
1300
+ async function startRecording(state, name) {
1301
+ if (state.inspectActive) {
1302
+ await stopInspect(state);
1303
+ }
1304
+ if (state.mode !== "idle") {
1305
+ throw new Error(`Cannot start recording while ${state.mode}`);
1306
+ }
1307
+ await closeAuxiliaryTabs(state);
1308
+ await clearBrowserState(state);
1309
+ const session = await Session.load(state.sessionsDir, name);
1310
+ const config = await session.readConfig();
1311
+ state.lockedViewport = config.viewport ? { width: config.viewport.width, height: config.viewport.height } : null;
1312
+ const bootstrap = await loadBootstrap();
1313
+ detachDebugger(state);
1314
+ await loadBlankSafe(state.view);
1315
+ const client = createCDPClient(state.view.webContents);
1316
+ const ctx = {
1317
+ session,
1318
+ bootstrap,
1319
+ clients: /* @__PURE__ */ new Set(),
1320
+ onEvent: (e) => {
1321
+ try {
1322
+ emit(state.win, "session:event", e);
1323
+ broadcastStatus(state);
1324
+ } catch {
1325
+ }
1326
+ },
1327
+ config
1328
+ };
1329
+ await setupRecordingOnClient(client, ctx);
1330
+ state.session = session;
1331
+ state.recordingClient = client;
1332
+ state.recordingContext = ctx;
1333
+ const activeTab = state.tabs.find((t) => t.id === state.activeTabId);
1334
+ if (activeTab) activeTab.client = client;
1335
+ state.mode = "recording";
1336
+ broadcastStatus(state);
1337
+ emit(state.win, "sessions:changed", void 0);
1338
+ const startUrl = session.manifest.initialUrl;
1339
+ if (startUrl && startUrl !== "about:blank") {
1340
+ state.view.webContents.loadURL(startUrl).catch(() => {
1341
+ });
1342
+ }
1343
+ }
1344
+ async function setupInspectOnClient(state, client, bootstrap, helpers, config) {
1345
+ await client.send("Runtime.enable");
1346
+ await client.send("Page.enable");
1347
+ if (config) {
1348
+ await applyConfig(client, config);
1349
+ }
1350
+ await client.send("Runtime.addBinding", { name: "__trqEmit" }).catch(() => {
1351
+ });
1352
+ await client.send("Page.addScriptToEvaluateOnNewDocument", {
1353
+ source: 'window.__trqMode = "idle"; window.__trqInspect = true;'
1354
+ });
1355
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
1356
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: bootstrap });
1357
+ client.on("Runtime.bindingCalled", (raw) => {
1358
+ const e = raw;
1359
+ if (e.name !== "__trqEmit") return;
1360
+ try {
1361
+ const payload = JSON.parse(e.payload);
1362
+ if (payload.__trq === "hover" && payload.selectors && payload.target) {
1363
+ emit(state.win, "inspect:hover", {
1364
+ selectors: payload.selectors,
1365
+ target: payload.target
1366
+ });
1367
+ }
1368
+ } catch {
1369
+ }
1370
+ });
1371
+ await client.send("Runtime.evaluate", {
1372
+ expression: 'window.__trqMode = "idle"; window.__trqInspect = true;'
1373
+ }).catch(() => {
1374
+ });
1375
+ await client.send("Runtime.evaluate", { expression: helpers }).catch(() => {
1376
+ });
1377
+ await client.send("Runtime.evaluate", { expression: bootstrap }).catch(() => {
1378
+ });
1379
+ }
1380
+ async function startInspect(state, sessionName) {
1381
+ if (state.inspectActive) return;
1382
+ if (state.mode !== "idle") {
1383
+ throw new Error(`Cannot inspect while ${state.mode}`);
1384
+ }
1385
+ if (!sessionName) {
1386
+ throw new Error("Inspect requires a selected session");
1387
+ }
1388
+ let session;
1389
+ try {
1390
+ session = await Session.load(state.sessionsDir, sessionName);
1391
+ } catch {
1392
+ throw new Error(`Session not found: ${sessionName}`);
1393
+ }
1394
+ const config = await session.readConfig();
1395
+ state.lockedViewport = config.viewport ? { width: config.viewport.width, height: config.viewport.height } : null;
1396
+ await closeAuxiliaryTabs(state);
1397
+ detachDebugger(state);
1398
+ await clearBrowserState(state);
1399
+ await loadBlankSafe(state.view);
1400
+ const bootstrap = await loadBootstrap();
1401
+ const helpers = await loadReplayHelpers();
1402
+ state.inspectAssets = { bootstrap, helpers, config };
1403
+ const client = createCDPClient(state.view.webContents);
1404
+ state.recordingClient = client;
1405
+ const activeTab = state.tabs.find((t) => t.id === state.activeTabId);
1406
+ if (activeTab) activeTab.client = client;
1407
+ await setupInspectOnClient(state, client, bootstrap, helpers, config);
1408
+ state.inspectActive = true;
1409
+ state.cancelReplay = false;
1410
+ state.runToEnd = false;
1411
+ state.replayCursor = null;
1412
+ state.stepResolver = null;
1413
+ state.mode = "paused";
1414
+ emit(state.win, "inspect:state", { active: true });
1415
+ broadcastStatus(state);
1416
+ state.inspectorOpen = true;
1417
+ state.invalidateLayout();
1418
+ state.inspectLoopPromise = (async () => {
1419
+ try {
1420
+ await runReplayOnClient({
1421
+ client,
1422
+ session,
1423
+ startUrl: session.manifest.initialUrl ?? void 0,
1424
+ shouldCancel: () => state.cancelReplay,
1425
+ // Follow into tabs spawned during the inspect step-through. createTab's
1426
+ // inspect branch above attaches a CDP client + injects helpers, then
1427
+ // switchToTab updates state.recordingClient — this getter picks it up.
1428
+ getActiveClient: () => state.recordingClient ?? client,
1429
+ shouldWait: async (idx, _total, e) => {
1430
+ state.replayCursor = {
1431
+ idx,
1432
+ total: _total,
1433
+ eventId: e.id,
1434
+ eventType: e.type
1435
+ };
1436
+ emit(state.win, "replay:cursor", {
1437
+ idx,
1438
+ total: _total,
1439
+ eventId: e.id,
1440
+ eventType: e.type
1441
+ });
1442
+ if (state.runToEnd) return;
1443
+ await new Promise((resolve) => {
1444
+ state.stepResolver = resolve;
1445
+ });
1446
+ state.stepResolver = null;
1447
+ },
1448
+ onProgress: (idx, total, e, status, detail) => {
1449
+ emit(state.win, "replay:progress", {
1450
+ current: idx,
1451
+ total,
1452
+ eventType: e.type,
1453
+ status,
1454
+ detail
1455
+ });
1456
+ }
1457
+ });
1458
+ } catch (err) {
1459
+ console.error("[inspect-debug] loop error:", err.message);
1460
+ } finally {
1461
+ const stillOurs = state.recordingClient === client;
1462
+ state.replayCursor = null;
1463
+ state.stepResolver = null;
1464
+ state.runToEnd = false;
1465
+ emit(state.win, "replay:cursor", { idx: 0, total: 0, eventId: null, eventType: null });
1466
+ if (stillOurs) {
1467
+ try {
1468
+ await client.send("Runtime.evaluate", {
1469
+ expression: `(() => {
1470
+ window.__trqInspect = false;
1471
+ window.__trqMode = 'idle';
1472
+ for (const id of ['__trq_overlay', '__trq_highlight', '__trq_assert_menu']) {
1473
+ document.getElementById(id)?.remove();
1474
+ }
1475
+ window.__trqInstalled = false;
1476
+ })()`
1477
+ });
1478
+ } catch {
1479
+ }
1480
+ detachDebugger(state);
1481
+ exitInspectLayout(state);
1482
+ state.inspectActive = false;
1483
+ state.cancelReplay = false;
1484
+ state.lockedViewport = null;
1485
+ state.mode = "idle";
1486
+ emit(state.win, "inspect:state", { active: false });
1487
+ broadcastStatus(state);
1488
+ }
1489
+ }
1490
+ })();
1491
+ }
1492
+ async function stopInspect(state) {
1493
+ if (!state.inspectActive) return;
1494
+ state.cancelReplay = true;
1495
+ state.stepResolver?.();
1496
+ state.stepResolver = null;
1497
+ if (state.inspectLoopPromise) {
1498
+ await state.inspectLoopPromise;
1499
+ state.inspectLoopPromise = null;
1500
+ }
1501
+ }
1502
+ function stepInspect(state) {
1503
+ if (state.mode !== "paused") return;
1504
+ const r = state.stepResolver;
1505
+ if (!r) return;
1506
+ state.stepResolver = null;
1507
+ r();
1508
+ }
1509
+ function continueInspect(state) {
1510
+ if (state.mode !== "paused") return;
1511
+ state.runToEnd = true;
1512
+ const r = state.stepResolver;
1513
+ if (r) {
1514
+ state.stepResolver = null;
1515
+ r();
1516
+ }
1517
+ }
1518
+ function exitInspectLayout(state) {
1519
+ state.inspectorOpen = false;
1520
+ state.inspectorCollapsed = false;
1521
+ state.invalidateLayout();
1522
+ }
1523
+ async function highlightSelector(state, selectors) {
1524
+ if (!state.inspectActive || !state.recordingClient) return false;
1525
+ try {
1526
+ const r = await state.recordingClient.send("Runtime.evaluate", {
1527
+ expression: `window.__trqHighlight && window.__trqHighlight(${JSON.stringify(selectors)})`,
1528
+ returnByValue: true
1529
+ });
1530
+ return r.result?.value ?? false;
1531
+ } catch {
1532
+ return false;
1533
+ }
1534
+ }
1535
+ async function stopRecording(state) {
1536
+ if (state.mode !== "recording") return;
1537
+ for (const t of state.tabs) {
1538
+ try {
1539
+ t.client?.detach();
1540
+ } catch {
1541
+ }
1542
+ t.client = null;
1543
+ }
1544
+ detachDebugger(state);
1545
+ state.recordingContext = null;
1546
+ state.lockedViewport = null;
1547
+ state.mode = "idle";
1548
+ broadcastStatus(state);
1549
+ emit(state.win, "sessions:changed", void 0);
1550
+ }
1551
+ function pauseAtFailure(state, idx, total, eventId, reason) {
1552
+ state.pauseFailure = { step: idx + 1, total, eventId, reason };
1553
+ state.invalidateLayout();
1554
+ emit(state.win, "pause:failure", state.pauseFailure);
1555
+ console.log(`[pause] step ${idx + 1}/${total}: ${reason}`);
1556
+ return new Promise((resolve) => {
1557
+ state.pauseResolver = (action) => {
1558
+ state.pauseFailure = null;
1559
+ state.pauseResolver = null;
1560
+ state.invalidateLayout();
1561
+ emit(state.win, "pause:failure", null);
1562
+ resolve(action);
1563
+ };
1564
+ });
1565
+ }
1566
+ async function play(state, name) {
1567
+ if (state.inspectActive) {
1568
+ await stopInspect(state);
1569
+ }
1570
+ if (state.mode !== "idle") throw new Error(`Cannot play while ${state.mode}`);
1571
+ await closeAuxiliaryTabs(state);
1572
+ const session = await Session.load(state.sessionsDir, name);
1573
+ const config = await session.readConfig();
1574
+ state.lockedViewport = config.viewport ? { width: config.viewport.width, height: config.viewport.height } : null;
1575
+ state.session = session;
1576
+ state.mode = "replaying";
1577
+ state.cancelReplay = false;
1578
+ broadcastStatus(state);
1579
+ detachDebugger(state);
1580
+ await clearBrowserState(state);
1581
+ await loadBlankSafe(state.view);
1582
+ const client = createCDPClient(state.view.webContents);
1583
+ state.recordingClient = client;
1584
+ const activeTab = state.tabs.find((t) => t.id === state.activeTabId);
1585
+ if (activeTab) activeTab.client = client;
1586
+ try {
1587
+ const result = await runReplayOnClient({
1588
+ client,
1589
+ session,
1590
+ startUrl: session.manifest.initialUrl ?? void 0,
1591
+ shouldCancel: () => state.cancelReplay,
1592
+ // Follow into new tabs as the recording did. createTab during replay
1593
+ // mode attaches a CDP client + injects helpers, and switchToTab
1594
+ // updates state.recordingClient — so this getter naturally picks up
1595
+ // the active tab's client between events.
1596
+ getActiveClient: () => state.recordingClient ?? client,
1597
+ shouldPauseOnFail: (idx, total, e, reason) => pauseAtFailure(state, idx, total, e.id, reason),
1598
+ onProgress: (idx, total, e, status, detail) => {
1599
+ emit(state.win, "replay:progress", {
1600
+ current: idx,
1601
+ total,
1602
+ eventType: e.type,
1603
+ status,
1604
+ detail
1605
+ });
1606
+ if (status === "start") {
1607
+ emit(state.win, "log:line", { level: "info", text: `\u25B8 ${idx}/${total} ${e.type}` });
1608
+ } else if (status === "fail") {
1609
+ emit(state.win, "log:line", { level: "error", text: `\u2717 step ${idx}: ${detail ?? "failed"}` });
1610
+ }
1611
+ }
1612
+ });
1613
+ const summary = result.failed ? `[replay] \u2717 step ${result.failed.step}/${result.total}: ${result.failed.reason}` : `[replay] \u2713 ${result.completed}/${result.total}`;
1614
+ console.log(summary);
1615
+ emit(state.win, "log:line", {
1616
+ level: result.failed ? "error" : "info",
1617
+ text: summary
1618
+ });
1619
+ } finally {
1620
+ state.mode = "idle";
1621
+ broadcastStatus(state);
1622
+ }
1623
+ }
1624
+ async function resume(state, name) {
1625
+ if (state.inspectActive) {
1626
+ await stopInspect(state);
1627
+ }
1628
+ if (state.mode !== "idle") throw new Error(`Cannot resume while ${state.mode}`);
1629
+ await closeAuxiliaryTabs(state);
1630
+ const session = await Session.load(state.sessionsDir, name);
1631
+ const config = await session.readConfig();
1632
+ const bootstrap = await loadBootstrap();
1633
+ state.lockedViewport = config.viewport ? { width: config.viewport.width, height: config.viewport.height } : null;
1634
+ state.session = session;
1635
+ state.mode = "replaying";
1636
+ state.cancelReplay = false;
1637
+ broadcastStatus(state);
1638
+ detachDebugger(state);
1639
+ await clearBrowserState(state);
1640
+ await loadBlankSafe(state.view);
1641
+ const client = createCDPClient(state.view.webContents);
1642
+ state.recordingClient = client;
1643
+ const activeTab = state.tabs.find((t) => t.id === state.activeTabId);
1644
+ if (activeTab) activeTab.client = client;
1645
+ try {
1646
+ const result = await runResumeOnClient({
1647
+ client,
1648
+ session,
1649
+ startUrl: session.manifest.initialUrl ?? void 0,
1650
+ shouldCancel: () => state.cancelReplay,
1651
+ getActiveClient: () => state.recordingClient ?? client,
1652
+ shouldPauseOnFail: (idx, total, e, reason) => pauseAtFailure(state, idx, total, e.id, reason),
1653
+ onReplayProgress: (idx, total, e, status, detail) => {
1654
+ emit(state.win, "replay:progress", {
1655
+ current: idx,
1656
+ total,
1657
+ eventType: e.type,
1658
+ status,
1659
+ detail
1660
+ });
1661
+ if (status === "start") {
1662
+ emit(state.win, "log:line", { level: "info", text: `\u25B8 ${idx}/${total} ${e.type}` });
1663
+ } else if (status === "fail") {
1664
+ emit(state.win, "log:line", { level: "error", text: `\u2717 step ${idx}: ${detail ?? "failed"}` });
1665
+ }
1666
+ },
1667
+ onRecordingStart: () => {
1668
+ emit(state.win, "log:line", {
1669
+ level: "info",
1670
+ text: "\u25B6 replay complete \u2014 now recording. close the window or click Stop to finish."
1671
+ });
1672
+ state.mode = "recording";
1673
+ broadcastStatus(state);
1674
+ const ctx = {
1675
+ session: state.session,
1676
+ bootstrap,
1677
+ clients: /* @__PURE__ */ new Set([client]),
1678
+ onEvent: (e) => {
1679
+ emit(state.win, "session:event", e);
1680
+ broadcastStatus(state);
1681
+ },
1682
+ config
1683
+ };
1684
+ state.recordingContext = ctx;
1685
+ void Promise.all(
1686
+ state.tabs.slice(1).filter((t) => !!t.client).map((t) => setupRecordingOnClient(t.client, ctx))
1687
+ ).catch((err) => {
1688
+ console.warn("[resume] aux-tab recording setup failed:", err.message);
1689
+ });
1690
+ },
1691
+ onRecordEvent: (e) => {
1692
+ emit(state.win, "session:event", e);
1693
+ broadcastStatus(state);
1694
+ }
1695
+ });
1696
+ if (result.failed) {
1697
+ const summary = `[resume] \u2717 step ${result.failed.step}/${result.total}: ${result.failed.reason}`;
1698
+ console.log(summary);
1699
+ emit(state.win, "log:line", { level: "error", text: summary });
1700
+ state.mode = "idle";
1701
+ broadcastStatus(state);
1702
+ } else {
1703
+ console.log(`[resume] \u2713 replay phase complete (${result.completed}/${result.total}) \u2014 recording`);
1704
+ }
1705
+ } catch (err) {
1706
+ state.mode = "idle";
1707
+ broadcastStatus(state);
1708
+ throw err;
1709
+ }
1710
+ }
1711
+ function handlers(state) {
1712
+ return {
1713
+ "sessions:list": async () => Session.list(state.sessionsDir),
1714
+ "sessions:create": async (s, { name, startUrl }) => {
1715
+ const session = await Session.create(s.sessionsDir, name, startUrl);
1716
+ await clearBrowserState(s);
1717
+ emit(s.win, "sessions:changed", void 0);
1718
+ return session.manifest;
1719
+ },
1720
+ "sessions:delete": async (s, { name }) => {
1721
+ if (s.session?.manifest.name === name && s.mode === "recording") {
1722
+ await stopRecording(s);
1723
+ }
1724
+ await import_node_fs6.promises.rm(import_node_path6.default.join(s.sessionsDir, name), { recursive: true, force: true });
1725
+ emit(s.win, "sessions:changed", void 0);
1726
+ },
1727
+ "sessions:events": async (s, { name }) => readEvents(import_node_path6.default.join(s.sessionsDir, name)),
1728
+ "recording:start": async (s, { name }) => startRecording(s, name),
1729
+ "recording:stop": async (s) => stopRecording(s),
1730
+ "replay:play": async (s, { name }) => play(s, name),
1731
+ "replay:resume": async (s, { name }) => resume(s, name),
1732
+ "replay:stop": async (s) => {
1733
+ s.cancelReplay = true;
1734
+ },
1735
+ // Pause-at-failure resolvers. Each unblocks the loop's awaited promise
1736
+ // with the chosen action. No-op if the loop isn't currently paused.
1737
+ "pause:retry": async (s) => {
1738
+ s.pauseResolver?.("retry");
1739
+ },
1740
+ "pause:skip": async (s) => {
1741
+ s.pauseResolver?.("skip");
1742
+ },
1743
+ "pause:stop": async (s) => {
1744
+ s.pauseResolver?.("stop");
1745
+ },
1746
+ "pause:editFailed": async (s) => {
1747
+ if (!s.pauseFailure || !s.session) return null;
1748
+ return { sessionName: s.session.manifest.name, eventId: s.pauseFailure.eventId };
1749
+ },
1750
+ // Run the page-side scorer against the failed event's recorded target.
1751
+ // Only valid while paused — the live page is in the state it was at the
1752
+ // moment of failure, so DOM candidates are meaningful.
1753
+ "events:suggestSelectors": async (s, { eventId }) => {
1754
+ if (!s.pauseFailure || !s.session || !s.recordingClient) return [];
1755
+ if (eventId !== s.pauseFailure.eventId) return [];
1756
+ const events = await readEvents(import_node_path6.default.join(s.sessionsDir, s.session.manifest.name));
1757
+ const ev = events.find((e) => e.id === eventId);
1758
+ const target = ev?.target;
1759
+ if (!target) return [];
1760
+ try {
1761
+ const r = await s.recordingClient.send("Runtime.evaluate", {
1762
+ expression: `JSON.stringify(window.__trqSuggest && window.__trqSuggest(${JSON.stringify(target)}, 3) || [])`,
1763
+ returnByValue: true
1764
+ });
1765
+ const raw = r.result?.value ?? "[]";
1766
+ return JSON.parse(raw);
1767
+ } catch {
1768
+ return [];
1769
+ }
1770
+ },
1771
+ // Activate the page-side click-to-capture mode. The picked envelope
1772
+ // comes back through the __trqEmit binding; we forward it to the
1773
+ // renderer as 'pick:result'. Caller controls UI (drawer minimize, etc).
1774
+ "events:pickFromPage": async (s) => {
1775
+ if (!s.pauseFailure || !s.recordingClient) return;
1776
+ const client = s.recordingClient;
1777
+ await client.send("Runtime.addBinding", { name: "__trqEmit" }).catch(() => {
1778
+ });
1779
+ const handler = (raw) => {
1780
+ const e = raw;
1781
+ if (e.name !== "__trqEmit") return;
1782
+ try {
1783
+ const p = JSON.parse(e.payload);
1784
+ if (p.__trq === "pick") {
1785
+ emit(s.win, "pick:result", p);
1786
+ client.off?.("Runtime.bindingCalled", handler);
1787
+ } else if (p.__trq === "pick-cancel") {
1788
+ emit(s.win, "pick:result", null);
1789
+ client.off?.("Runtime.bindingCalled", handler);
1790
+ }
1791
+ } catch {
1792
+ }
1793
+ };
1794
+ client.on("Runtime.bindingCalled", handler);
1795
+ s.win.setBrowserView(s.view);
1796
+ s.win.emit("resize");
1797
+ await client.send("Runtime.evaluate", {
1798
+ expression: "window.__trqStartPick && window.__trqStartPick()"
1799
+ }).catch(() => {
1800
+ });
1801
+ },
1802
+ "events:cancelPick": async (s) => {
1803
+ if (!s.recordingClient) return;
1804
+ await s.recordingClient.send("Runtime.evaluate", {
1805
+ expression: "window.__trqStopPick && window.__trqStopPick()"
1806
+ }).catch(() => {
1807
+ });
1808
+ },
1809
+ "view:navigate": async (s, { url }) => {
1810
+ await s.view.webContents.loadURL(url);
1811
+ },
1812
+ "view:setVisible": async (s, { visible }) => {
1813
+ if (visible) {
1814
+ s.win.setBrowserView(s.view);
1815
+ s.win.emit("resize");
1816
+ } else {
1817
+ s.win.setBrowserView(null);
1818
+ }
1819
+ },
1820
+ "view:layout": async (s, { sidebarW, eventsH }) => {
1821
+ s.layout.sidebarW = sidebarW;
1822
+ s.layout.eventsH = eventsH;
1823
+ s.invalidateLayout();
1824
+ },
1825
+ "config:get": async (s, { name }) => {
1826
+ const session = await Session.load(s.sessionsDir, name);
1827
+ return session.readConfig();
1828
+ },
1829
+ "config:set": async (s, { name, config }) => {
1830
+ const session = await Session.load(s.sessionsDir, name);
1831
+ await session.writeConfig(config);
1832
+ },
1833
+ "inspect:start": async (s, args) => startInspect(s, args?.name),
1834
+ "inspect:stop": async (s) => stopInspect(s),
1835
+ "inspect:highlight": async (s, { selectors }) => highlightSelector(s, selectors),
1836
+ "inspect:step": async (s) => stepInspect(s),
1837
+ "inspect:continue": async (s) => continueInspect(s),
1838
+ "inspect:toggleCollapsed": async (s) => {
1839
+ s.inspectorCollapsed = !s.inspectorCollapsed;
1840
+ s.invalidateLayout();
1841
+ emit(s.win, "inspect:state", { active: s.inspectActive, collapsed: s.inspectorCollapsed });
1842
+ return { collapsed: s.inspectorCollapsed };
1843
+ },
1844
+ "tabs:list": async (s) => tabSummaries(s),
1845
+ "tabs:switch": async (s, { id }) => switchToTab(s, id),
1846
+ "tabs:close": async (s, { id }) => closeTab(s, id),
1847
+ "events:replace": async (s, { name, id, patch }) => {
1848
+ if (s.mode === "recording" && s.session?.manifest.name === name) {
1849
+ throw new Error("Cannot edit events while recording");
1850
+ }
1851
+ const session = await Session.load(s.sessionsDir, name);
1852
+ const stamped = await session.replaceEvent(id, patch);
1853
+ emit(s.win, "sessions:changed", void 0);
1854
+ return stamped;
1855
+ },
1856
+ "events:delete": async (s, { name, id }) => {
1857
+ if (s.mode === "recording" && s.session?.manifest.name === name) {
1858
+ throw new Error("Cannot delete events while recording");
1859
+ }
1860
+ const session = await Session.load(s.sessionsDir, name);
1861
+ await session.removeEvent(id);
1862
+ emit(s.win, "sessions:changed", void 0);
1863
+ },
1864
+ "events:insertAt": async (s, { name, idx, event }) => {
1865
+ if (s.mode === "recording" && s.session?.manifest.name === name) {
1866
+ throw new Error("Cannot insert events while recording");
1867
+ }
1868
+ const session = await Session.load(s.sessionsDir, name);
1869
+ const stamped = await session.insertEventAt(idx, event);
1870
+ emit(s.win, "sessions:changed", void 0);
1871
+ return stamped;
1872
+ },
1873
+ "sessions:branch": async (s, { name, newName, throughStep }) => {
1874
+ const session = await Session.load(s.sessionsDir, name);
1875
+ const branched = await session.branch(s.sessionsDir, newName, throughStep);
1876
+ emit(s.win, "sessions:changed", void 0);
1877
+ return branched.manifest;
1878
+ },
1879
+ "ui:eventContextMenu": async (s, { sessionName, eventId, eventIdx, total }) => {
1880
+ const send = (action) => {
1881
+ emit(s.win, "ui:contextMenuChoice", { sessionName, eventId, eventIdx, action });
1882
+ };
1883
+ const menu = import_electron.Menu.buildFromTemplate([
1884
+ { label: "Edit\u2026", click: () => send("edit") },
1885
+ { label: "Delete", click: () => send("delete") },
1886
+ { type: "separator" },
1887
+ { label: "Insert event above\u2026", click: () => send("insertAbove") },
1888
+ { label: "Insert event below\u2026", click: () => send("insertBelow") },
1889
+ { type: "separator" },
1890
+ {
1891
+ label: `Branch from here (steps 1\u2026${eventIdx + 1} of ${total})\u2026`,
1892
+ click: () => send("branch")
1893
+ }
1894
+ ]);
1895
+ menu.popup({ window: s.win });
1896
+ }
1897
+ };
1898
+ }
1899
+ function registerIpc(state) {
1900
+ const h = handlers(state);
1901
+ for (const channel of Object.keys(h)) {
1902
+ import_electron.ipcMain.handle(channel, (_evt, args) => h[channel](state, args));
1903
+ }
1904
+ broadcastStatus(state);
1905
+ }
1906
+
1907
+ // src/main/main.ts
1908
+ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
1909
+ var SESSIONS_DIR = process.env.TRQ_SESSIONS_DIR ? import_node_path7.default.resolve(process.env.TRQ_SESSIONS_DIR) : import_node_path7.default.join(process.cwd(), "sessions");
1910
+ var PRELOAD = import_node_path7.default.join(__dirname, "../preload/preload.cjs");
1911
+ var HEADER_H = 32 + 32;
1912
+ var PAUSE_BANNER_H = 36;
1913
+ var STATUS_BAR_H = 22;
1914
+ var PAGE_PADDING = 8;
1915
+ var SIDEBAR_W_DEFAULT = 48 + 240;
1916
+ var EVENTS_H_DEFAULT = 180;
1917
+ var INSPECTOR_W = 280;
1918
+ var INSPECTOR_W_COLLAPSED = 28;
1919
+ async function createWindow() {
1920
+ const win = new import_electron2.BrowserWindow({
1921
+ width: 1280,
1922
+ height: 820,
1923
+ backgroundColor: "#0a0a0a",
1924
+ webPreferences: {
1925
+ preload: PRELOAD,
1926
+ contextIsolation: true,
1927
+ nodeIntegration: false
1928
+ }
1929
+ });
1930
+ const view = new import_electron2.BrowserView({
1931
+ webPreferences: { contextIsolation: true, nodeIntegration: false }
1932
+ });
1933
+ win.setBrowserView(view);
1934
+ const primaryTab = {
1935
+ id: "tab_0",
1936
+ view,
1937
+ url: "about:blank",
1938
+ title: "",
1939
+ client: null
1940
+ };
1941
+ const state = {
1942
+ sessionsDir: SESSIONS_DIR,
1943
+ win,
1944
+ layout: { sidebarW: SIDEBAR_W_DEFAULT, eventsH: EVENTS_H_DEFAULT },
1945
+ view,
1946
+ tabs: [primaryTab],
1947
+ activeTabId: primaryTab.id,
1948
+ mode: "idle",
1949
+ session: null,
1950
+ recordingClient: null,
1951
+ recordingContext: null,
1952
+ cancelReplay: false,
1953
+ inspectActive: false,
1954
+ pauseFailure: null,
1955
+ pauseResolver: null,
1956
+ inspectorOpen: false,
1957
+ inspectorCollapsed: false,
1958
+ inspectAssets: null,
1959
+ stepResolver: null,
1960
+ runToEnd: false,
1961
+ replayCursor: null,
1962
+ inspectLoopPromise: null,
1963
+ lockedViewport: null,
1964
+ // Replaced below after sizeView is defined.
1965
+ invalidateLayout: () => {
1966
+ }
1967
+ };
1968
+ let inSizeView = false;
1969
+ const sizeView = () => {
1970
+ if (inSizeView) return;
1971
+ inSizeView = true;
1972
+ try {
1973
+ const active = state.view;
1974
+ const [w, h] = win.getContentSize();
1975
+ const rightReserve = state.inspectorOpen ? state.inspectorCollapsed ? INSPECTOR_W_COLLAPSED : INSPECTOR_W : 0;
1976
+ const left = state.layout.sidebarW;
1977
+ const eventsH = state.layout.eventsH;
1978
+ const bannerH = state.pauseFailure ? PAUSE_BANNER_H : 0;
1979
+ const bounds = {
1980
+ x: left + PAGE_PADDING,
1981
+ y: HEADER_H + bannerH + PAGE_PADDING,
1982
+ width: Math.max(0, w - left - rightReserve - 2 * PAGE_PADDING),
1983
+ height: Math.max(0, h - HEADER_H - bannerH - eventsH - STATUS_BAR_H - 2 * PAGE_PADDING)
1984
+ };
1985
+ active.setBounds(bounds);
1986
+ if (process.env.TRQ_DEBUG_LAYOUT) {
1987
+ console.log(`[layout] window=${w}x${h} inspectorOpen=${state.inspectorOpen} view=${JSON.stringify(bounds)}`);
1988
+ }
1989
+ if (process.platform === "darwin") {
1990
+ win.setContentSize(w, h + 1);
1991
+ win.setContentSize(w, h);
1992
+ }
1993
+ if (state.recordingClient && bounds.width > 0 && bounds.height > 0) {
1994
+ const lock = state.lockedViewport;
1995
+ const params = lock ? {
1996
+ width: lock.width,
1997
+ height: lock.height,
1998
+ // Use min(ratio_w, ratio_h) so neither dimension overflows.
1999
+ // Capped at 1 so a small viewport doesn't get upscaled.
2000
+ scale: Math.min(1, bounds.width / lock.width, bounds.height / lock.height),
2001
+ deviceScaleFactor: 1,
2002
+ mobile: false,
2003
+ dontSetVisibleSize: true
2004
+ } : {
2005
+ width: bounds.width,
2006
+ height: bounds.height,
2007
+ deviceScaleFactor: 1,
2008
+ mobile: false,
2009
+ dontSetVisibleSize: true
2010
+ };
2011
+ if (process.env.TRQ_DEBUG_LAYOUT) {
2012
+ console.log(`[layout] setDeviceMetricsOverride ${JSON.stringify(params)}`);
2013
+ }
2014
+ state.recordingClient.send("Emulation.setDeviceMetricsOverride", params).catch((err) => {
2015
+ if (process.env.TRQ_DEBUG_LAYOUT) {
2016
+ console.log(`[layout] FAILED: ${err.message}`);
2017
+ }
2018
+ });
2019
+ } else if (process.env.TRQ_DEBUG_LAYOUT) {
2020
+ console.log(`[layout] skipped: client=${!!state.recordingClient}`);
2021
+ }
2022
+ } finally {
2023
+ inSizeView = false;
2024
+ }
2025
+ };
2026
+ state.invalidateLayout = sizeView;
2027
+ sizeView();
2028
+ win.setMaxListeners(20);
2029
+ win.on("resize", sizeView);
2030
+ wireUpTab(state, primaryTab);
2031
+ registerIpc(state);
2032
+ win.webContents.on("console-message", (_e, level, message, line, source) => {
2033
+ const tag = ["log", "warn", "error"][level] ?? "log";
2034
+ console.log(`[renderer:${tag}] ${message} (${source}:${line})`);
2035
+ });
2036
+ win.webContents.on("preload-error", (_e, file, err) => {
2037
+ console.error(`[preload-error] ${file}:`, err);
2038
+ });
2039
+ await win.loadFile(import_node_path7.default.join(__dirname, "../renderer/index.html"));
2040
+ await view.webContents.loadURL("about:blank");
2041
+ if (process.env.TRQ_DEVTOOLS) {
2042
+ win.webContents.openDevTools({ mode: "detach" });
2043
+ }
2044
+ const autoTest = process.env.TRQ_AUTO_TEST;
2045
+ if (autoTest === "1") {
2046
+ runAutoTest(win, view).catch((err) => {
2047
+ console.error("[auto-test] failed:", err);
2048
+ import_electron2.app.exit(1);
2049
+ });
2050
+ } else if (autoTest === "config") {
2051
+ runConfigAutoTest(win, view).catch((err) => {
2052
+ console.error("[config-test] failed:", err);
2053
+ import_electron2.app.exit(1);
2054
+ });
2055
+ } else if (autoTest === "play") {
2056
+ runPlayAutoTest(win, view).catch((err) => {
2057
+ console.error("[play-test] failed:", err);
2058
+ import_electron2.app.exit(1);
2059
+ });
2060
+ } else if (autoTest === "inspect") {
2061
+ runInspectAutoTest(win, view).catch((err) => {
2062
+ console.error("[inspect-test] failed:", err);
2063
+ import_electron2.app.exit(1);
2064
+ });
2065
+ } else if (autoTest === "debug") {
2066
+ runDebugAutoTest(win, view).catch((err) => {
2067
+ console.error("[debug-test] failed:", err);
2068
+ import_electron2.app.exit(1);
2069
+ });
2070
+ } else if (autoTest === "responsive") {
2071
+ runResponsiveAutoTest(win, view).catch((err) => {
2072
+ console.error("[responsive-test] failed:", err);
2073
+ import_electron2.app.exit(1);
2074
+ });
2075
+ } else if (autoTest === "tabs") {
2076
+ runTabsAutoTest(win, state).catch((err) => {
2077
+ console.error("[tabs-test] failed:", err);
2078
+ import_electron2.app.exit(1);
2079
+ });
2080
+ } else if (autoTest === "scaledViewport") {
2081
+ runScaledViewportAutoTest(win, state).catch((err) => {
2082
+ console.error("[scaledviewport-test] failed:", err);
2083
+ import_electron2.app.exit(1);
2084
+ });
2085
+ }
2086
+ const cleanup = () => {
2087
+ try {
2088
+ const dbg = view.webContents.debugger;
2089
+ if (dbg.isAttached()) dbg.detach();
2090
+ } catch {
2091
+ }
2092
+ };
2093
+ win.on("closed", cleanup);
2094
+ import_electron2.app.on("before-quit", cleanup);
2095
+ }
2096
+ async function runAutoTest(win, view) {
2097
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? `gui-smoketest-${Date.now()}`;
2098
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2099
+ console.log(`[auto-test] creating session ${name}`);
2100
+ await win.webContents.executeJavaScript(
2101
+ `window.trq.invoke('sessions:create', { name: ${JSON.stringify(name)}, startUrl: 'https://example.com' })`
2102
+ );
2103
+ console.log("[auto-test] starting recording");
2104
+ await win.webContents.executeJavaScript(
2105
+ `window.trq.invoke('recording:start', { name: ${JSON.stringify(name)} })`
2106
+ );
2107
+ await wait(500);
2108
+ console.log("[auto-test] navigating BrowserView to example.com");
2109
+ await win.webContents.executeJavaScript(
2110
+ `window.trq.invoke('view:navigate', { url: 'https://example.com' })`
2111
+ );
2112
+ await wait(3e3);
2113
+ console.log("[auto-test] locating link to click");
2114
+ const linkBox = await view.webContents.executeJavaScript(`
2115
+ (() => {
2116
+ const links = Array.from(document.querySelectorAll('a'));
2117
+ console.log('[page] found', links.length, 'links:', links.map((a) => a.textContent));
2118
+ const a = links[0];
2119
+ if (!a) return null;
2120
+ const r = a.getBoundingClientRect();
2121
+ return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), text: a.textContent };
2122
+ })()
2123
+ `);
2124
+ console.log("[auto-test] linkBox:", linkBox);
2125
+ if (!linkBox) {
2126
+ console.log("[auto-test] WARN: link not found, skipping click");
2127
+ } else {
2128
+ await view.webContents.sendInputEvent({ type: "mouseMove", x: linkBox.x, y: linkBox.y });
2129
+ await view.webContents.sendInputEvent({ type: "mouseDown", x: linkBox.x, y: linkBox.y, button: "left", clickCount: 1 });
2130
+ await view.webContents.sendInputEvent({ type: "mouseUp", x: linkBox.x, y: linkBox.y, button: "left", clickCount: 1 });
2131
+ }
2132
+ await wait(2500);
2133
+ console.log("[auto-test] stopping recording");
2134
+ await win.webContents.executeJavaScript(
2135
+ `window.trq.invoke('recording:stop', undefined)`
2136
+ );
2137
+ await wait(500);
2138
+ const events = await win.webContents.executeJavaScript(
2139
+ `window.trq.invoke('sessions:events', { name: ${JSON.stringify(name)} })`
2140
+ );
2141
+ console.log(`[auto-test] captured ${events.length} events:`);
2142
+ for (const e of events) console.log(` ${e.id} ${e.type}`);
2143
+ console.log("[auto-test] PASS \u2014 quitting");
2144
+ import_electron2.app.quit();
2145
+ }
2146
+ async function runConfigAutoTest(win, view) {
2147
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? `cfg-smoketest-${Date.now()}`;
2148
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2149
+ const CONFIG = {
2150
+ viewport: { width: 800, height: 600, deviceScaleFactor: 1, mobile: false },
2151
+ permissions: ["geolocation"],
2152
+ geolocation: { latitude: 52.52, longitude: 13.405, accuracy: 5 },
2153
+ locale: "de-DE",
2154
+ timezone: "Europe/Berlin"
2155
+ };
2156
+ console.log(`[config-test] creating session ${name}`);
2157
+ await win.webContents.executeJavaScript(
2158
+ `window.trq.invoke('sessions:create', { name: ${JSON.stringify(name)}, startUrl: 'about:blank' })`
2159
+ );
2160
+ console.log("[config-test] writing config:", CONFIG);
2161
+ await win.webContents.executeJavaScript(
2162
+ `window.trq.invoke('config:set', { name: ${JSON.stringify(name)}, config: ${JSON.stringify(CONFIG)} })`
2163
+ );
2164
+ console.log("[config-test] starting recording");
2165
+ await win.webContents.executeJavaScript(
2166
+ `window.trq.invoke('recording:start', { name: ${JSON.stringify(name)} })`
2167
+ );
2168
+ await wait(600);
2169
+ console.log("[config-test] navigating to https://example.com");
2170
+ await win.webContents.executeJavaScript(
2171
+ `window.trq.invoke('view:navigate', { url: 'https://example.com' })`
2172
+ );
2173
+ await wait(2500);
2174
+ const probe = await view.webContents.executeJavaScript(`
2175
+ (async () => {
2176
+ const out = {
2177
+ innerWidth: window.innerWidth,
2178
+ innerHeight: window.innerHeight,
2179
+ locale: (new Intl.DateTimeFormat()).resolvedOptions().locale,
2180
+ timezone: (new Intl.DateTimeFormat()).resolvedOptions().timeZone,
2181
+ geo: null,
2182
+ geoErr: null,
2183
+ };
2184
+ try {
2185
+ out.geo = await new Promise((resolve, reject) => {
2186
+ navigator.geolocation.getCurrentPosition(
2187
+ (pos) => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude, acc: pos.coords.accuracy }),
2188
+ (err) => reject(err.message || String(err)),
2189
+ { timeout: 4000 },
2190
+ );
2191
+ });
2192
+ } catch (e) { out.geoErr = String(e); }
2193
+ return out;
2194
+ })()
2195
+ `);
2196
+ console.log("[config-test] probe result:", JSON.stringify(probe, null, 2));
2197
+ const checks = [
2198
+ ["viewport.width", probe.innerWidth, CONFIG.viewport.width],
2199
+ ["viewport.height", probe.innerHeight, CONFIG.viewport.height],
2200
+ ["locale", probe.locale, CONFIG.locale],
2201
+ ["timezone", probe.timezone, CONFIG.timezone],
2202
+ ["geo.lat", probe.geo?.lat, CONFIG.geolocation.latitude],
2203
+ ["geo.lng", probe.geo?.lng, CONFIG.geolocation.longitude]
2204
+ ];
2205
+ let pass = true;
2206
+ for (const [field, actual, expected] of checks) {
2207
+ const ok = actual === expected;
2208
+ console.log(` [${ok ? "\u2713" : "\u2717"}] ${field}: ${JSON.stringify(actual)} (expected ${JSON.stringify(expected)})`);
2209
+ if (!ok) pass = false;
2210
+ }
2211
+ console.log("[config-test] stopping recording");
2212
+ await win.webContents.executeJavaScript(
2213
+ `window.trq.invoke('recording:stop', undefined)`
2214
+ );
2215
+ await wait(400);
2216
+ console.log(`[config-test] ${pass ? "PASS" : "FAIL"}`);
2217
+ import_electron2.app.exit(pass ? 0 : 1);
2218
+ }
2219
+ async function runPlayAutoTest(win, view) {
2220
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? "gui-smoketest";
2221
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2222
+ const timeoutMs = +(process.env.TRQ_AUTO_TEST_TIMEOUT ?? "120000");
2223
+ await win.webContents.executeJavaScript(`
2224
+ (() => {
2225
+ window.trq.on('session:status', (s) => {
2226
+ console.log('[status] mode=' + s.mode);
2227
+ });
2228
+ window.trq.on('replay:progress', (p) => {
2229
+ console.log('[progress] ' + p.current + '/' + p.total + ' ' + p.eventType + ' (' + p.status + (p.detail ? ' \u2014 ' + p.detail : '') + ')');
2230
+ });
2231
+ return null;
2232
+ })()
2233
+ `);
2234
+ const runOne = async (label) => {
2235
+ console.log(`[${label}] invoke replay:play`);
2236
+ const invokePromise = win.webContents.executeJavaScript(
2237
+ `window.trq.invoke('replay:play', { name: ${JSON.stringify(name)} }).then(() => '__OK__').catch((e) => '__ERR__: ' + e.message)`
2238
+ );
2239
+ const result = await Promise.race([
2240
+ invokePromise,
2241
+ new Promise((resolve) => setTimeout(() => resolve("__TIMEOUT__"), timeoutMs))
2242
+ ]);
2243
+ console.log(`[${label}] result: ${result} / final URL: ${view.webContents.getURL()}`);
2244
+ return result;
2245
+ };
2246
+ const a = await runOne("play-1");
2247
+ if (a !== "__OK__") {
2248
+ import_electron2.app.exit(1);
2249
+ return;
2250
+ }
2251
+ console.log("[play-test] waiting 2s before play-2\u2026");
2252
+ await wait(2e3);
2253
+ const b = await runOne("play-2");
2254
+ import_electron2.app.exit(a === "__OK__" && b === "__OK__" ? 0 : 1);
2255
+ }
2256
+ async function runInspectAutoTest(win, view) {
2257
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2258
+ console.log("[inspect-test] starting inspect mode");
2259
+ await win.webContents.executeJavaScript(
2260
+ `window.trq.invoke('inspect:start', undefined)`
2261
+ );
2262
+ await wait(500);
2263
+ console.log("[inspect-test] navigating BrowserView to example.com");
2264
+ await win.webContents.executeJavaScript(
2265
+ `window.trq.invoke('view:navigate', { url: 'https://example.com' })`
2266
+ );
2267
+ await wait(3e3);
2268
+ let hoverCount = 0;
2269
+ await win.webContents.executeJavaScript(`
2270
+ (() => {
2271
+ window.__inspectHovers = [];
2272
+ window.trq.on('inspect:hover', (h) => {
2273
+ window.__inspectHovers.push(h);
2274
+ console.log('[hover] ' + JSON.stringify({
2275
+ tag: h.target.tagName,
2276
+ firstSelector: h.selectors && h.selectors[0] && h.selectors[0][0],
2277
+ }));
2278
+ });
2279
+ return null;
2280
+ })()
2281
+ `);
2282
+ const linkBox = await view.webContents.executeJavaScript(`
2283
+ (() => {
2284
+ const a = document.querySelector('a');
2285
+ if (!a) return null;
2286
+ const r = a.getBoundingClientRect();
2287
+ return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), text: a.textContent };
2288
+ })()
2289
+ `);
2290
+ console.log("[inspect-test] target link:", linkBox);
2291
+ if (!linkBox) {
2292
+ import_electron2.app.exit(1);
2293
+ return;
2294
+ }
2295
+ await view.webContents.sendInputEvent({ type: "mouseMove", x: 20, y: 20 });
2296
+ await wait(80);
2297
+ await view.webContents.sendInputEvent({ type: "mouseMove", x: linkBox.x, y: linkBox.y });
2298
+ await wait(800);
2299
+ const hovers = await win.webContents.executeJavaScript("window.__inspectHovers");
2300
+ console.log(`[inspect-test] received ${hovers.length} hover events`);
2301
+ if (hovers.length > 0) {
2302
+ const last = hovers[hovers.length - 1];
2303
+ console.log("[inspect-test] last hover selectors:");
2304
+ for (const s of last.selectors) console.log(" - " + s[0]);
2305
+ }
2306
+ hoverCount = hovers.length;
2307
+ if (hovers.length > 0) {
2308
+ const sels = hovers[hovers.length - 1].selectors;
2309
+ const ok = await win.webContents.executeJavaScript(
2310
+ `window.trq.invoke('inspect:highlight', { selectors: ${JSON.stringify(sels)} })`
2311
+ );
2312
+ console.log("[inspect-test] highlight returned:", ok);
2313
+ await wait(1e3);
2314
+ }
2315
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:stop', undefined)`);
2316
+ console.log(`[inspect-test] ${hoverCount > 0 ? "PASS" : "FAIL"}`);
2317
+ import_electron2.app.exit(hoverCount > 0 ? 0 : 1);
2318
+ }
2319
+ async function runDebugAutoTest(win, view) {
2320
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? "assert-test";
2321
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2322
+ await win.webContents.executeJavaScript(`
2323
+ (() => {
2324
+ window.__cursors = [];
2325
+ window.__statuses = [];
2326
+ window.trq.on('replay:cursor', (c) => {
2327
+ window.__cursors.push(c);
2328
+ console.log('[cursor] ' + c.idx + '/' + c.total + ' ' + (c.eventId ?? '-') + ' ' + (c.eventType ?? '-'));
2329
+ });
2330
+ window.trq.on('session:status', (s) => {
2331
+ window.__statuses.push(s);
2332
+ console.log('[status] mode=' + s.mode);
2333
+ });
2334
+ return null;
2335
+ })()
2336
+ `);
2337
+ console.log(`[debug-test] starting inspect on session: ${name}`);
2338
+ await win.webContents.executeJavaScript(
2339
+ `window.trq.invoke('inspect:start', { name: ${JSON.stringify(name)} })`
2340
+ );
2341
+ let cursors = [];
2342
+ for (let i = 0; i < 30; i++) {
2343
+ cursors = await win.webContents.executeJavaScript("window.__cursors");
2344
+ if (cursors.length > 0) break;
2345
+ await wait(200);
2346
+ }
2347
+ if (cursors.length === 0) {
2348
+ console.log("[debug-test] FAIL \u2014 no cursor received after inspect:start");
2349
+ import_electron2.app.exit(1);
2350
+ return;
2351
+ }
2352
+ const first = cursors[0];
2353
+ console.log(`[debug-test] first cursor: idx=${first.idx} total=${first.total} eventType=${first.eventType}`);
2354
+ console.log("[debug-test] step-over");
2355
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:step', undefined)`);
2356
+ await wait(2500);
2357
+ cursors = await win.webContents.executeJavaScript("window.__cursors");
2358
+ console.log(`[debug-test] cursors so far: ${cursors.length}`);
2359
+ if (cursors.length < 2) {
2360
+ console.log("[debug-test] FAIL \u2014 no second cursor after step");
2361
+ import_electron2.app.exit(1);
2362
+ return;
2363
+ }
2364
+ console.log(`[debug-test] second cursor: idx=${cursors[1].idx} eventType=${cursors[1].eventType}`);
2365
+ console.log("[debug-test] run-to-end");
2366
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:continue', undefined)`);
2367
+ let modeIdle = false;
2368
+ for (let i = 0; i < 60; i++) {
2369
+ const statuses = await win.webContents.executeJavaScript("window.__statuses");
2370
+ if (statuses.find((s) => s.mode === "idle" && statuses.indexOf(s) > 0)) {
2371
+ modeIdle = true;
2372
+ break;
2373
+ }
2374
+ await wait(500);
2375
+ }
2376
+ console.log(`[debug-test] mode returned to idle: ${modeIdle}`);
2377
+ console.log(`[debug-test] ${modeIdle ? "PASS" : "FAIL"}`);
2378
+ import_electron2.app.exit(modeIdle ? 0 : 1);
2379
+ }
2380
+ async function runResponsiveAutoTest(win, view) {
2381
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? "assert-test";
2382
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2383
+ console.log(`[responsive-test] starting inspect on session: ${name}`);
2384
+ await win.webContents.executeJavaScript(
2385
+ `window.trq.invoke('inspect:start', { name: ${JSON.stringify(name)} })`
2386
+ );
2387
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:step', undefined)`);
2388
+ await wait(2500);
2389
+ const expanded = await view.webContents.executeJavaScript(`
2390
+ ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight })
2391
+ `);
2392
+ console.log(`[responsive-test] expanded inspector \u2192 page reports ${expanded.innerWidth}x${expanded.innerHeight}`);
2393
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:toggleCollapsed', undefined)`);
2394
+ await wait(500);
2395
+ const collapsed = await view.webContents.executeJavaScript(`
2396
+ ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight })
2397
+ `);
2398
+ console.log(`[responsive-test] collapsed inspector \u2192 page reports ${collapsed.innerWidth}x${collapsed.innerHeight}`);
2399
+ await win.webContents.executeJavaScript(`window.trq.invoke('inspect:toggleCollapsed', undefined)`);
2400
+ await wait(500);
2401
+ const reExpanded = await view.webContents.executeJavaScript(`
2402
+ ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight })
2403
+ `);
2404
+ console.log(`[responsive-test] re-expanded \u2192 page reports ${reExpanded.innerWidth}x${reExpanded.innerHeight}`);
2405
+ const tolerance = 4;
2406
+ const expectedExpanded = 1280 - SIDEBAR_W - INSPECTOR_W;
2407
+ const expectedCollapsed = 1280 - SIDEBAR_W - INSPECTOR_W_COLLAPSED;
2408
+ const pass = Math.abs(expanded.innerWidth - expectedExpanded) < tolerance && Math.abs(collapsed.innerWidth - expectedCollapsed) < tolerance && Math.abs(reExpanded.innerWidth - expectedExpanded) < tolerance;
2409
+ console.log(`[responsive-test] expected expanded=${expectedExpanded}, collapsed=${expectedCollapsed}`);
2410
+ console.log(`[responsive-test] ${pass ? "PASS" : "FAIL"}`);
2411
+ import_electron2.app.exit(pass ? 0 : 1);
2412
+ }
2413
+ async function runTabsAutoTest(win, state) {
2414
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? `tabs-smoketest-${Date.now()}`;
2415
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2416
+ console.log(`[tabs-test] creating session ${name}`);
2417
+ await win.webContents.executeJavaScript(
2418
+ `window.trq.invoke('sessions:create', { name: ${JSON.stringify(name)}, startUrl: 'about:blank' })`
2419
+ );
2420
+ console.log("[tabs-test] starting recording");
2421
+ await win.webContents.executeJavaScript(
2422
+ `window.trq.invoke('recording:start', { name: ${JSON.stringify(name)} })`
2423
+ );
2424
+ await wait(600);
2425
+ const dataUrl = "data:text/html," + encodeURIComponent(
2426
+ '<!doctype html><meta charset=utf8><title>Tab Test</title><body style="font: 16px sans-serif; padding: 20px"><a id="link" href="https://example.com" target="_blank">open in new tab</a></body>'
2427
+ );
2428
+ console.log("[tabs-test] navigating active tab to data: URL");
2429
+ await win.webContents.executeJavaScript(
2430
+ `window.trq.invoke('view:navigate', { url: ${JSON.stringify(dataUrl)} })`
2431
+ );
2432
+ await wait(1500);
2433
+ const linkBox = await state.view.webContents.executeJavaScript(`
2434
+ (() => {
2435
+ const a = document.getElementById('link');
2436
+ if (!a) return null;
2437
+ const r = a.getBoundingClientRect();
2438
+ return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
2439
+ })()
2440
+ `);
2441
+ if (!linkBox) {
2442
+ console.log("[tabs-test] FAIL \u2014 link not found");
2443
+ import_electron2.app.exit(1);
2444
+ return;
2445
+ }
2446
+ console.log(`[tabs-test] clicking link at ${linkBox.x},${linkBox.y}`);
2447
+ await state.view.webContents.sendInputEvent({ type: "mouseMove", x: linkBox.x, y: linkBox.y });
2448
+ await state.view.webContents.sendInputEvent({ type: "mouseDown", x: linkBox.x, y: linkBox.y, button: "left", clickCount: 1 });
2449
+ await state.view.webContents.sendInputEvent({ type: "mouseUp", x: linkBox.x, y: linkBox.y, button: "left", clickCount: 1 });
2450
+ await wait(3e3);
2451
+ console.log(`[tabs-test] tabs.length = ${state.tabs.length}`);
2452
+ console.log(`[tabs-test] active tab url = ${state.view.webContents.getURL()}`);
2453
+ const sawNewTab = state.tabs.length === 2;
2454
+ const newTabUrl = state.tabs[1]?.view.webContents.getURL() ?? "";
2455
+ const newTabLoadedExample = newTabUrl.includes("example.com");
2456
+ console.log(`[tabs-test] sawNewTab=${sawNewTab} newTabLoadedExample=${newTabLoadedExample}`);
2457
+ console.log("[tabs-test] stopping recording");
2458
+ await win.webContents.executeJavaScript(
2459
+ `window.trq.invoke('recording:stop', undefined)`
2460
+ );
2461
+ await wait(500);
2462
+ const pass = sawNewTab && newTabLoadedExample;
2463
+ console.log(`[tabs-test] ${pass ? "PASS" : "FAIL"}`);
2464
+ import_electron2.app.exit(pass ? 0 : 1);
2465
+ }
2466
+ async function runScaledViewportAutoTest(win, state) {
2467
+ const name = process.env.TRQ_AUTO_TEST_SESSION ?? `scaledvp-smoketest-${Date.now()}`;
2468
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2469
+ console.log(`[scaledviewport-test] creating session ${name}`);
2470
+ await win.webContents.executeJavaScript(
2471
+ `window.trq.invoke('sessions:create', { name: ${JSON.stringify(name)}, startUrl: 'about:blank' })`
2472
+ );
2473
+ const LOCKED = { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false };
2474
+ console.log(`[scaledviewport-test] writing config.viewport = ${LOCKED.width}x${LOCKED.height}`);
2475
+ await win.webContents.executeJavaScript(
2476
+ `window.trq.invoke('config:set', { name: ${JSON.stringify(name)}, config: { viewport: ${JSON.stringify(LOCKED)} } })`
2477
+ );
2478
+ console.log("[scaledviewport-test] starting recording");
2479
+ await win.webContents.executeJavaScript(
2480
+ `window.trq.invoke('recording:start', { name: ${JSON.stringify(name)} })`
2481
+ );
2482
+ await wait(800);
2483
+ const dataUrl = "data:text/html," + encodeURIComponent(
2484
+ '<!doctype html><meta charset=utf8><title>Scaled VP Test</title><style>body{margin:0} #m{width:1920px;height:1080px;background:linear-gradient(90deg,#f00,#0f0,#00f)}</style><body><div id="m">marker</div></body>'
2485
+ );
2486
+ await win.webContents.executeJavaScript(
2487
+ `window.trq.invoke('view:navigate', { url: ${JSON.stringify(dataUrl)} })`
2488
+ );
2489
+ await wait(1500);
2490
+ const viewBounds = state.view.getBounds();
2491
+ console.log(`[scaledviewport-test] BrowserView bounds: ${viewBounds.width}x${viewBounds.height}`);
2492
+ const probe = await state.view.webContents.executeJavaScript(`
2493
+ ({
2494
+ innerWidth: window.innerWidth,
2495
+ innerHeight: window.innerHeight,
2496
+ scrollWidth: document.documentElement.scrollWidth,
2497
+ scrollHeight: document.documentElement.scrollHeight,
2498
+ })
2499
+ `);
2500
+ console.log(`[scaledviewport-test] page reports: ${JSON.stringify(probe)}`);
2501
+ await win.webContents.executeJavaScript(`window.trq.invoke('recording:stop', undefined)`);
2502
+ await wait(400);
2503
+ const expectedScale = Math.min(1, viewBounds.width / LOCKED.width, viewBounds.height / LOCKED.height);
2504
+ const checks = [
2505
+ ["innerWidth (page should see locked width)", probe.innerWidth, LOCKED.width],
2506
+ ["innerHeight (page should see locked height)", probe.innerHeight, LOCKED.height],
2507
+ ["view bounds preserved (BrowserView did NOT grow to locked size)", viewBounds.width < LOCKED.width, true]
2508
+ ];
2509
+ let pass = true;
2510
+ for (const [label, actual, expected] of checks) {
2511
+ const ok = actual === expected;
2512
+ console.log(` [${ok ? "\u2713" : "\u2717"}] ${label}: ${JSON.stringify(actual)} (expected ${JSON.stringify(expected)})`);
2513
+ if (!ok) pass = false;
2514
+ }
2515
+ console.log(`[scaledviewport-test] expected scale \u2248 ${expectedScale.toFixed(3)}`);
2516
+ console.log(`[scaledviewport-test] ${pass ? "PASS" : "FAIL"}`);
2517
+ import_electron2.app.exit(pass ? 0 : 1);
2518
+ }
2519
+ import_electron2.app.whenReady().then(createWindow);
2520
+ import_electron2.app.on("window-all-closed", () => {
2521
+ if (process.platform !== "darwin") import_electron2.app.quit();
2522
+ });
2523
+ import_electron2.app.on("activate", () => {
2524
+ if (import_electron2.BrowserWindow.getAllWindows().length === 0) createWindow();
2525
+ });