function exitOnSignal
exitOnSignal(unnamed 0?: { once?: (
signal: string,
handler: () => void
) => void
; exit?: (code: number) => void; }
): void

Turns a termination signal into an ordinary exit, so process.on('exit') handlers still run.

Node's DEFAULT handling of these signals terminates the process without running exit listeners. That is the whole problem: playwright kills the browser it launched from a synchronous process.on('exit') handler, and the pre-launched Chrome is reaped from another. Killed by a signal with no listener, neither fires, and the browser outlives the run.

On CI that compounds. A supervisor kills a slow run, its browser survives, the leftover browsers compete for the machine, the next run is slower, and more get killed — which is what a Firefox-on-Windows lane looked like when every test took four times its usual duration and the ones that fell off the end reported page.goto: Timeout 60000ms exceeded.

Exiting rather than cleaning up is deliberate, and is why playwright's own handleSIGTERM stays off: its handler starts an ASYNC graceful close that can hang, and a process being told to stop has no time to spend. process.exit runs the synchronous handlers and goes.

Windows gets nothing from this and cannot: child.kill() there is TerminateProcess, which no handler in this process can intercept. Whoever does the killing has to take the tree down — see the test runner's own spawnCapture.

import { exitOnSignal } from './exit-on-signal.ts';

const exits: number[] = [];
const signals = new Map<string, () => void>();
exitOnSignal({
  once: (signal, handler) => void signals.set(signal, handler),
  exit: (code) => void exits.push(code),
});
signals.get('SIGTERM')!();
exits; // [143] — 128 + 15, the conventional code for a SIGTERM death

Parameters

optional
unnamed 0: { once?: (
signal: string,
handler: () => void
) => void
; exit?: (code: number) => void; }

Return Type

void

Usage

import { exitOnSignal } from "lib/utils/exit-on-signal.ts";