dev3000 0.0.171 → 0.0.174

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.
@@ -10,6 +10,18 @@ export interface CDPConnection {
10
10
  sessionId: string | null;
11
11
  nextId: number;
12
12
  }
13
+ export interface CDPTargetInfo {
14
+ id?: string;
15
+ type?: string;
16
+ title?: string;
17
+ url?: string;
18
+ webSocketDebuggerUrl?: string;
19
+ }
20
+ export declare function getLoadingHtmlCandidates(currentDir: string, execPath?: string): string[];
21
+ export declare function selectCDPTarget(targets: CDPTargetInfo[], options?: {
22
+ appServerPort?: string;
23
+ initialAppUrl?: string;
24
+ }): CDPTargetInfo;
13
25
  export declare class CDPMonitor {
14
26
  private browser;
15
27
  private connection;
@@ -36,7 +48,9 @@ export declare class CDPMonitor {
36
48
  private framework?;
37
49
  private reactTrackingEnabled;
38
50
  private lastReactSnapshotLogTime;
39
- constructor(profileDir: string, screenshotDir: string, logger: (source: string, message: string) => void, debug?: boolean, browserPath?: string, pluginReactScan?: boolean, appServerPort?: string, initialAppUrl?: string, debugPort?: number, headless?: boolean, framework?: "nextjs" | "svelte" | "other");
51
+ private pendingCommands;
52
+ private navigationTimeoutMs;
53
+ constructor(profileDir: string, screenshotDir: string, logger: (source: string, message: string) => void, debug?: boolean, browserPath?: string, pluginReactScan?: boolean, appServerPort?: string, initialAppUrl?: string, navigationTimeoutMs?: number, debugPort?: number, headless?: boolean, framework?: "nextjs" | "svelte" | "other");
40
54
  private resolveReactDevToolsExtensionPath;
41
55
  private debugLog;
42
56
  private runCommand;
@@ -68,8 +82,9 @@ export declare class CDPMonitor {
68
82
  private enableCDPDomains;
69
83
  private setupEventHandlers;
70
84
  private onCDPEvent;
85
+ private rejectPendingCDPCommands;
71
86
  private handleCDPMessage;
72
- navigateToUrl(url: string): Promise<void>;
87
+ navigateToUrl(url: string, timeoutMs?: number): Promise<void>;
73
88
  navigateToApp(port: string, useHttps?: boolean): Promise<void>;
74
89
  private setupInteractionTracking;
75
90
  private startInteractionPolling;
@@ -98,6 +113,7 @@ export declare class CDPMonitor {
98
113
  * Call this before killing the app server to prevent CDP reconnection loops.
99
114
  */
100
115
  prepareShutdown(): void;
116
+ private waitForBrowserExit;
101
117
  shutdown(): Promise<void>;
102
118
  private killInstanceChromeProcesses;
103
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-monitor.d.ts","sourceRoot":"","sources":["../src/cdp-monitor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cdp-monitor.d.ts","sourceRoot":"","sources":["../src/cdp-monitor.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,98 @@ const EMBEDDED_LOADING_HTML = `<!DOCTYPE html>
43
43
  </div>
44
44
  </body>
45
45
  </html>`;
46
+ const DEFAULT_CDP_COMMAND_TIMEOUT_MS = 10000;
47
+ const DEFAULT_NAVIGATION_TIMEOUT_MS = 60000;
48
+ export function getLoadingHtmlCandidates(currentDir, execPath = process.execPath) {
49
+ const candidates = [join(currentDir, "src/loading.html"), join(currentDir, "loading.html")];
50
+ const packageRoot = dirname(dirname(execPath));
51
+ candidates.push(join(packageRoot, "src/loading.html"));
52
+ candidates.push(join(packageRoot, "loading.html"));
53
+ candidates.push(join(process.cwd(), "src/loading.html"));
54
+ return candidates;
55
+ }
56
+ function isD3kLoadingPageUrl(url) {
57
+ if (!url?.startsWith("file://")) {
58
+ return false;
59
+ }
60
+ try {
61
+ const pathname = decodeURIComponent(new URL(url).pathname);
62
+ return pathname.includes("/dev3000-loading-") && pathname.endsWith("/loading.html");
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ function matchesAppServerPort(url, appServerPort) {
69
+ if (!url || !appServerPort) {
70
+ return false;
71
+ }
72
+ try {
73
+ const parsed = new URL(url);
74
+ const hostname = parsed.hostname;
75
+ const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
76
+ return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") && port === appServerPort;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ function matchesInitialAppUrl(url, initialAppUrl) {
83
+ if (!url || !initialAppUrl) {
84
+ return false;
85
+ }
86
+ try {
87
+ const actual = new URL(url);
88
+ const expected = new URL(initialAppUrl);
89
+ const actualPort = actual.port || (actual.protocol === "https:" ? "443" : "80");
90
+ const expectedPort = expected.port || (expected.protocol === "https:" ? "443" : "80");
91
+ return (actual.protocol === expected.protocol &&
92
+ actual.hostname === expected.hostname &&
93
+ actualPort === expectedPort &&
94
+ actual.pathname === expected.pathname);
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ function formatTargetForLog(target) {
101
+ return `${target.type || "unknown"}:${target.url || "<no-url>"}`;
102
+ }
103
+ export function selectCDPTarget(targets, options = {}) {
104
+ const debuggableTargets = targets.filter((target) => target.webSocketDebuggerUrl);
105
+ if (debuggableTargets.length === 0) {
106
+ throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
107
+ }
108
+ const contextualMatches = debuggableTargets
109
+ .map((target) => ({
110
+ target,
111
+ score: (matchesInitialAppUrl(target.url, options.initialAppUrl) ? 100 : 0) +
112
+ (matchesAppServerPort(target.url, options.appServerPort) ? 70 : 0) +
113
+ (isD3kLoadingPageUrl(target.url) ? 90 : 0) +
114
+ (target.type === "page" ? 10 : 0)
115
+ }))
116
+ .sort((left, right) => right.score - left.score);
117
+ const bestContextualScore = contextualMatches[0]?.score || 0;
118
+ const minimumContextualScore = 70;
119
+ if (bestContextualScore >= minimumContextualScore) {
120
+ return contextualMatches[0].target;
121
+ }
122
+ const pageTarget = debuggableTargets.find((target) => target.type === "page");
123
+ if (!options.appServerPort && !options.initialAppUrl) {
124
+ return pageTarget || debuggableTargets[0];
125
+ }
126
+ if (debuggableTargets.length === 1 && debuggableTargets[0]?.url === "about:blank") {
127
+ return debuggableTargets[0];
128
+ }
129
+ const expectedDetails = [
130
+ options.initialAppUrl ? `url ${options.initialAppUrl}` : null,
131
+ options.appServerPort ? `port ${options.appServerPort}` : null,
132
+ "d3k loading page"
133
+ ]
134
+ .filter(Boolean)
135
+ .join(", ");
136
+ throw new Error(`CDP target mismatch on port ${options.appServerPort || "unknown"}; expected ${expectedDetails}, found ${debuggableTargets.map(formatTargetForLog).join(", ")}`);
137
+ }
46
138
  export class CDPMonitor {
47
139
  browser = null;
48
140
  connection = null;
@@ -69,7 +161,9 @@ export class CDPMonitor {
69
161
  framework; // Framework hint from project detection
70
162
  reactTrackingEnabled = false;
71
163
  lastReactSnapshotLogTime = 0;
72
- constructor(profileDir, screenshotDir, logger, debug = false, browserPath, pluginReactScan = false, appServerPort, initialAppUrl, debugPort, headless = false, framework) {
164
+ pendingCommands = new Map();
165
+ navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS;
166
+ constructor(profileDir, screenshotDir, logger, debug = false, browserPath, pluginReactScan = false, appServerPort, initialAppUrl, navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS, debugPort, headless = false, framework) {
73
167
  this.profileDir = profileDir;
74
168
  this.screenshotDir = screenshotDir;
75
169
  this.appServerPort = appServerPort;
@@ -78,6 +172,7 @@ export class CDPMonitor {
78
172
  this.browserPath = browserPath;
79
173
  this.pluginReactScan = pluginReactScan;
80
174
  this.initialAppUrl = initialAppUrl;
175
+ this.navigationTimeoutMs = navigationTimeoutMs;
81
176
  this.headless = headless;
82
177
  this.framework = framework;
83
178
  this.reactTrackingEnabled = framework === "nextjs";
@@ -269,11 +364,7 @@ export class CDPMonitor {
269
364
  const currentDir = dirname(currentFile);
270
365
  let loadingHtml;
271
366
  try {
272
- const loadingHtmlCandidates = [
273
- join(currentDir, "src/loading.html"),
274
- join(currentDir, "loading.html"),
275
- join(process.cwd(), "src/loading.html")
276
- ];
367
+ const loadingHtmlCandidates = getLoadingHtmlCandidates(currentDir);
277
368
  const loadingHtmlPath = loadingHtmlCandidates.find((path) => existsSync(path));
278
369
  if (!loadingHtmlPath) {
279
370
  throw new Error("No loading.html found in expected locations");
@@ -442,6 +533,10 @@ export class CDPMonitor {
442
533
  chromeArgs.push("--disable-setuid-sandbox");
443
534
  chromeArgs.push("--disable-gpu");
444
535
  chromeArgs.push("--disable-dev-shm-usage");
536
+ chromeArgs.push("--disable-background-timer-throttling");
537
+ chromeArgs.push("--disable-backgrounding-occluded-windows");
538
+ chromeArgs.push("--disable-renderer-backgrounding");
539
+ chromeArgs.push("--window-size=1920,1080");
445
540
  this.debugLog("Launching Chrome in headless mode");
446
541
  }
447
542
  else {
@@ -538,15 +633,15 @@ export class CDPMonitor {
538
633
  try {
539
634
  // Get the WebSocket URL from Chrome's debug endpoint
540
635
  const targetsResponse = await fetch(`http://localhost:${this.debugPort}/json`);
541
- let targets = await targetsResponse.json();
542
- this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((t) => ({ type: t.type, url: t.url })))}`);
636
+ let targets = (await targetsResponse.json());
637
+ this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((target) => ({ type: target.type ?? "unknown", url: target.url ?? "" })))}`);
543
638
  if (targets.length === 0) {
544
639
  this.debugLog("No debuggable targets found; creating a blank page target");
545
640
  const newTargetResponse = await fetch(`http://localhost:${this.debugPort}/json/new?about:blank`, {
546
641
  method: "PUT"
547
642
  });
548
643
  if (newTargetResponse.ok) {
549
- const createdTarget = await newTargetResponse.json();
644
+ const createdTarget = (await newTargetResponse.json());
550
645
  targets = [createdTarget];
551
646
  this.debugLog(`Created page target: ${createdTarget.id || "unknown"} - ${createdTarget.url || ""}`);
552
647
  }
@@ -554,19 +649,14 @@ export class CDPMonitor {
554
649
  this.debugLog(`Failed to create page target: HTTP ${newTargetResponse.status}`);
555
650
  }
556
651
  }
557
- // Find the first page target (tab) - prefer 'page' type but accept any target with a webSocketDebuggerUrl
558
- let pageTarget = targets.find((target) => target.type === "page");
559
- // Fallback: if no 'page' type found, try to use any target with a debugger URL
560
- if (!pageTarget && targets.length > 0) {
561
- pageTarget = targets.find((target) => target.webSocketDebuggerUrl);
562
- if (pageTarget) {
563
- this.debugLog(`No 'page' type target found, using target of type '${pageTarget.type}' instead`);
564
- }
565
- }
566
- if (!pageTarget) {
567
- throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
568
- }
652
+ const pageTarget = selectCDPTarget(targets, {
653
+ appServerPort: this.appServerPort,
654
+ initialAppUrl: this.initialAppUrl
655
+ });
569
656
  const wsUrl = pageTarget.webSocketDebuggerUrl;
657
+ if (!wsUrl) {
658
+ throw new Error(`Selected CDP target does not have a websocket URL: ${formatTargetForLog(pageTarget)}`);
659
+ }
570
660
  this.cdpUrl = wsUrl; // Store the CDP URL
571
661
  this.debugLog(`Found page target: ${pageTarget.title || "Unknown"} - ${pageTarget.url}`);
572
662
  this.debugLog(`Got CDP WebSocket URL: ${wsUrl}`);
@@ -599,6 +689,7 @@ export class CDPMonitor {
599
689
  });
600
690
  ws.on("close", (code, reason) => {
601
691
  this.debugLog(`WebSocket closed with code ${code}, reason: ${reason}`);
692
+ this.rejectPendingCDPCommands(new Error(`CDP connection closed before response (code=${code})`));
602
693
  if (!this.isShuttingDown) {
603
694
  this.logger("browser", `[CDP] Connection lost unexpectedly (code: ${code}, reason: ${reason})`);
604
695
  this.logger("browser", "[CDP] CDP connection lost - check for Chrome crash or server issues");
@@ -682,7 +773,7 @@ export class CDPMonitor {
682
773
  throw err;
683
774
  }
684
775
  }
685
- async sendCDPCommand(method, params = {}) {
776
+ async sendCDPCommand(method, params = {}, timeoutMs = DEFAULT_CDP_COMMAND_TIMEOUT_MS) {
686
777
  if (!this.connection) {
687
778
  throw new Error("No CDP connection available");
688
779
  }
@@ -693,42 +784,32 @@ export class CDPMonitor {
693
784
  method,
694
785
  params
695
786
  };
696
- const messageHandler = (data) => {
697
- try {
698
- const message = JSON.parse(data.toString());
699
- if (message.id === id) {
700
- this.connection?.ws.removeListener("message", messageHandler);
701
- if (message.error) {
702
- reject(new Error(message.error.message));
703
- }
704
- else {
705
- resolve(message.result);
706
- }
707
- }
708
- }
709
- catch (error) {
710
- this.connection?.ws.removeListener("message", messageHandler);
711
- reject(error);
712
- }
713
- };
714
- this.connection?.ws.on("message", messageHandler);
715
- // Command timeout
716
787
  const timeout = setTimeout(() => {
717
- this.connection?.ws.removeListener("message", messageHandler);
788
+ this.pendingCommands.delete(id);
789
+ this.debugLog(`CDP command #${id} timed out after ${timeoutMs}ms: ${method}`);
718
790
  reject(new Error(`CDP command timeout: ${method}`));
719
- }, 10000);
720
- // Clear timeout if command succeeds/fails
721
- const originalResolve = resolve;
722
- const originalReject = reject;
723
- resolve = (value) => {
724
- clearTimeout(timeout);
725
- originalResolve(value);
726
- };
727
- reject = (reason) => {
728
- clearTimeout(timeout);
729
- originalReject(reason);
730
- };
731
- this.connection?.ws.send(JSON.stringify(command));
791
+ }, timeoutMs);
792
+ this.pendingCommands.set(id, {
793
+ method,
794
+ startedAt: Date.now(),
795
+ timeout,
796
+ resolve,
797
+ reject: (error) => reject(error)
798
+ });
799
+ this.debugLog(`Sending CDP command #${id}: ${method} ${JSON.stringify(params)}`);
800
+ this.connection?.ws.send(JSON.stringify(command), (error) => {
801
+ if (!error) {
802
+ return;
803
+ }
804
+ const pending = this.pendingCommands.get(id);
805
+ if (!pending) {
806
+ return;
807
+ }
808
+ clearTimeout(pending.timeout);
809
+ this.pendingCommands.delete(id);
810
+ this.debugLog(`Failed to send CDP command #${id}: ${method}: ${String(error)}`);
811
+ reject(error instanceof Error ? error : new Error(String(error)));
812
+ });
732
813
  });
733
814
  }
734
815
  getBundleTypeLabel(bundleType) {
@@ -808,39 +889,39 @@ export class CDPMonitor {
808
889
  }
809
890
  async enableCDPDomains() {
810
891
  const domains = [
811
- "Runtime", // Console logs, exceptions
812
- "Network", // Network requests/responses
813
- "Page", // Page events, navigation
814
- "DOM", // DOM mutations
815
- "Performance", // Performance metrics
816
- "Security", // Security events
817
- "Log", // Browser console logs
818
- "Target" // Target events (window/tab creation/destruction)
892
+ { name: "Runtime", required: true },
893
+ { name: "Network", required: true },
894
+ { name: "Page", required: true },
895
+ { name: "DOM", required: true },
896
+ { name: "Performance", required: false },
897
+ { name: "Security", required: false },
898
+ { name: "Log", required: true },
899
+ { name: "Target", required: false }
819
900
  // Note: Input domain is for dispatching events, not monitoring them - we use JS injection instead
820
901
  ];
821
902
  for (const domain of domains) {
822
903
  try {
823
- this.debugLog(`Enabling CDP domain: ${domain}`);
824
- await this.sendCDPCommand(`${domain}.enable`);
825
- this.debugLog(`Successfully enabled CDP domain: ${domain}`);
904
+ this.debugLog(`Enabling CDP domain: ${domain.name}`);
905
+ await this.sendCDPCommand(`${domain.name}.enable`, {}, 3000);
906
+ this.debugLog(`Successfully enabled CDP domain: ${domain.name}`);
826
907
  if (this.debug) {
827
- this.logger("browser", `[CDP] Enabled ${domain} domain`);
908
+ this.logger("browser", `[CDP] Enabled ${domain.name} domain`);
828
909
  }
829
910
  }
830
911
  catch (error) {
831
- this.debugLog(`Failed to enable CDP domain ${domain}: ${error}`);
832
- // Only log CDP errors when debug mode is enabled
912
+ this.debugLog(`Failed to enable CDP domain ${domain.name}: ${error}`);
913
+ if (domain.required) {
914
+ throw new Error(`Failed to enable required CDP domain ${domain.name}: ${String(error)}`);
915
+ }
833
916
  if (this.debug) {
834
- this.logger("browser", `[CDP] Failed to enable ${domain}: ${error}`);
917
+ this.logger("browser", `[CDP] Failed to enable optional ${domain.name}: ${error}`);
835
918
  }
836
- // Continue with other domains instead of throwing
837
919
  }
838
920
  }
839
- this.debugLog("Enabling runtime for console and exception capture");
840
- await this.sendCDPCommand("Runtime.enable");
921
+ this.debugLog("Setting async call stack depth for console and exception capture");
841
922
  await this.sendCDPCommand("Runtime.setAsyncCallStackDepth", {
842
923
  maxDepth: 32
843
- });
924
+ }, 3000);
844
925
  this.debugLog("CDP domains enabled successfully");
845
926
  // Set viewport for headless mode to ensure consistent CLS measurements
846
927
  // Without this, headless Chrome defaults to 800x600 which can cause
@@ -859,6 +940,18 @@ export class CDPMonitor {
859
940
  catch (error) {
860
941
  this.debugLog(`Failed to set viewport: ${error}`);
861
942
  }
943
+ try {
944
+ await this.sendCDPCommand("Page.bringToFront", {});
945
+ }
946
+ catch (error) {
947
+ this.debugLog(`Failed to bring headless page to front: ${error}`);
948
+ }
949
+ try {
950
+ await this.sendCDPCommand("Emulation.setFocusEmulationEnabled", { enabled: true });
951
+ }
952
+ catch (error) {
953
+ this.debugLog(`Failed to enable focus emulation: ${error}`);
954
+ }
862
955
  }
863
956
  }
864
957
  setupEventHandlers() {
@@ -1089,8 +1182,34 @@ export class CDPMonitor {
1089
1182
  onCDPEvent(method, handler) {
1090
1183
  this.eventHandlers.set(method, handler);
1091
1184
  }
1185
+ rejectPendingCDPCommands(error) {
1186
+ for (const [id, pending] of this.pendingCommands) {
1187
+ clearTimeout(pending.timeout);
1188
+ this.pendingCommands.delete(id);
1189
+ pending.reject(error);
1190
+ }
1191
+ }
1092
1192
  handleCDPMessage(message) {
1193
+ if (typeof message.id === "number") {
1194
+ const pending = this.pendingCommands.get(message.id);
1195
+ if (!pending) {
1196
+ this.debugLog(`Received CDP response for unknown command #${message.id}`);
1197
+ return;
1198
+ }
1199
+ clearTimeout(pending.timeout);
1200
+ this.pendingCommands.delete(message.id);
1201
+ const duration = Date.now() - pending.startedAt;
1202
+ if (message.error) {
1203
+ this.debugLog(`Received CDP error for #${message.id} (${pending.method}) after ${duration}ms: ${message.error.message}`);
1204
+ pending.reject(new Error(message.error.message || `CDP command failed: ${pending.method}`));
1205
+ return;
1206
+ }
1207
+ this.debugLog(`Received CDP response for #${message.id} (${pending.method}) after ${duration}ms`);
1208
+ pending.resolve(message.result || {});
1209
+ return;
1210
+ }
1093
1211
  if (message.method) {
1212
+ this.debugLog(`Received CDP event: ${message.method}`);
1094
1213
  const handler = this.eventHandlers.get(message.method);
1095
1214
  if (handler) {
1096
1215
  const event = {
@@ -1103,7 +1222,7 @@ export class CDPMonitor {
1103
1222
  }
1104
1223
  }
1105
1224
  }
1106
- async navigateToUrl(url) {
1225
+ async navigateToUrl(url, timeoutMs = this.navigationTimeoutMs) {
1107
1226
  if (!this.connection) {
1108
1227
  throw new Error("No CDP connection available");
1109
1228
  }
@@ -1113,7 +1232,7 @@ export class CDPMonitor {
1113
1232
  try {
1114
1233
  const result = await this.sendCDPCommand("Page.navigate", {
1115
1234
  url
1116
- });
1235
+ }, timeoutMs);
1117
1236
  const navigationTime = Date.now() - navigationStartTime;
1118
1237
  this.debugLog(`Navigation command sent successfully (${navigationTime}ms)`);
1119
1238
  this.debugLog(`Navigation result: ${JSON.stringify(result)}`);
@@ -1635,10 +1754,39 @@ export class CDPMonitor {
1635
1754
  this.isShuttingDown = true;
1636
1755
  this.debugLog("Shutdown signaled - reconnection attempts will be blocked");
1637
1756
  }
1757
+ async waitForBrowserExit(timeoutMs) {
1758
+ const browser = this.browser;
1759
+ if (!browser) {
1760
+ return true;
1761
+ }
1762
+ if (browser.exitCode !== null || browser.killed) {
1763
+ return true;
1764
+ }
1765
+ return await new Promise((resolve) => {
1766
+ const onExit = () => {
1767
+ clearTimeout(timer);
1768
+ resolve(true);
1769
+ };
1770
+ const timer = setTimeout(() => {
1771
+ browser.removeListener("exit", onExit);
1772
+ resolve(false);
1773
+ }, timeoutMs);
1774
+ browser.once("exit", onExit);
1775
+ });
1776
+ }
1638
1777
  async shutdown() {
1639
1778
  this.isShuttingDown = true;
1640
- // Try to close the page first, then the tab
1779
+ let browserClosedCleanly = false;
1780
+ // Ask the browser process to exit first so Chrome doesn't think it crashed.
1641
1781
  if (this.connection?.sessionId) {
1782
+ try {
1783
+ await this.sendCDPCommand("Browser.close");
1784
+ this.debugLog("Sent Browser.close command");
1785
+ browserClosedCleanly = await this.waitForBrowserExit(2000);
1786
+ }
1787
+ catch (_e) {
1788
+ this.debugLog("Browser.close failed, trying page/tab close fallback");
1789
+ }
1642
1790
  try {
1643
1791
  // Try to close the page
1644
1792
  await this.sendCDPCommand("Page.close");
@@ -1667,6 +1815,9 @@ export class CDPMonitor {
1667
1815
  catch (_e) {
1668
1816
  this.debugLog("Failed to close tab via CDP, will force close Chrome");
1669
1817
  }
1818
+ if (!browserClosedCleanly) {
1819
+ browserClosedCleanly = await this.waitForBrowserExit(1500);
1820
+ }
1670
1821
  }
1671
1822
  // Close CDP connection
1672
1823
  if (this.connection) {
@@ -1678,7 +1829,11 @@ export class CDPMonitor {
1678
1829
  }
1679
1830
  this.connection = null;
1680
1831
  }
1681
- // Kill only the Chrome processes for THIS instance
1832
+ if (browserClosedCleanly) {
1833
+ this.chromePids.clear();
1834
+ return;
1835
+ }
1836
+ // Kill only the Chrome processes for THIS instance as a fallback.
1682
1837
  await this.killInstanceChromeProcesses();
1683
1838
  }
1684
1839
  async killInstanceChromeProcesses() {