From a9de586cfb5ef16a930364ba1c62c65a7db9a25a Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 30 Oct 2026 00:21:05 -0400 Subject: [PATCH] test(cli): a force-closed browser leaves no Chrome process alive on Windows --- .../launch.windowsTree.browser.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/cli/src/browser/launch.windowsTree.browser.test.ts diff --git a/packages/cli/src/browser/launch.windowsTree.browser.test.ts b/packages/cli/src/browser/launch.windowsTree.browser.test.ts new file mode 100644 index 0000000000..49f623c47a --- /dev/null +++ b/packages/cli/src/browser/launch.windowsTree.browser.test.ts @@ -0,0 +1,53 @@ +import { execFileSync } from "node:child_process"; +import puppeteer, { type Browser } from "puppeteer-core"; +import { expect, it } from "vitest"; +import { launchManagedBrowser } from "./launch.js"; +import { ensureBrowser } from "./manager.js"; +import { processIsAlive } from "../utils/processTree.js"; + +function directChildren(pid: number): number[] { + const out = execFileSync( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + `Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}" | ForEach-Object { $_.ProcessId }`, + ], + { encoding: "utf8", windowsHide: true }, + ); + return out + .split(/\r?\n/) + .map(Number) + .filter((child) => Number.isInteger(child) && child > 0); +} + +// A browser whose graceful close never answers is ended by force; its renderer and GPU processes must go too. +it.skipIf(process.platform !== "win32")( + "a force-closed browser leaves none of its Chrome processes alive", + async () => { + const { executablePath } = await ensureBrowser(); + const hanging = { + launch: async (options: Parameters[0]) => { + const browser = await puppeteer.launch(options); + browser.close = () => new Promise(() => {}); + return browser; + }, + } as unknown as typeof puppeteer; + const launched: Browser = await launchManagedBrowser(hanging, { + executablePath, + headless: true, + }); + const page = await launched.newPage(); + await page.goto("about:blank"); + const root = launched.process()?.pid; + expect(root).toBeDefined(); + const children = directChildren(root!); + expect(children.length).toBeGreaterThan(0); + + await launched.close(); + + expect(children.filter(processIsAlive)).toEqual([]); + }, + 90_000, +);