Skip to content

feat(electron): add timeout option to electronApp.close() for force-kill escalation #40586

@SebTardif

Description

@SebTardif

Feature Request

Is your feature request related to a problem?

electronApp.close() currently waits indefinitely for the Electron process to exit. If the application has before-quit handlers that prevent shutdown, leaky IPC handlers, or child processes that keep it alive, close() hangs forever until the test-level timeout kills everything.

This is a well-documented pain point:

PR #11336 rightfully removed the old hardcoded 30s timeout (which was inconsistent with other close() methods). But now there is no way for users to specify a grace period with force-kill escalation.

The force-kill infrastructure already exists in processLauncher.ts (killProcess() → SIGKILL / taskkill /T /F) but is only reachable via OS signals (SIGINT/SIGTERM), not through the public close() API.

Describe the solution

Add an opt-in timeout option to electronApp.close():

// Default: wait forever (unchanged behavior)
await electronApp.close();

// With timeout: force-kill if app does not exit within 10s
await electronApp.close({ timeout: 10_000 });

When specified:

  1. Initiate graceful shutdown (existing flow: browser.close()app.quit()worker._disconnect())
  2. Start a timer
  3. If the process exits within the timeout, clear the timer (no force-kill)
  4. If the timer fires first, call the existing kill() from processLauncher (SIGKILL/taskkill)
  5. Wait for the process to fully exit

This is different from the removed behavior (#11336) because:

  • Opt-in: no timeout by default (backward compatible)
  • Force-kills instead of throwing: the old timeout threw an error and left the process running
  • Uses existing infrastructure: wires the kill function from launchProcess into ElectronApplication

Real-world use case

We maintain E2E tests for a VS Code extension using Playwright Electron. Our test teardown was flaky because close() would occasionally hang when the Electron app did not exit cleanly. We had to build a 3-layer workaround: app.quit()app.close() wrapped in Promise.race with manual timer → SIGKILL fallback. This feature would replace that pattern with a single API call.

I have a PR ready if you are open to this.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions