fix(install-e2e): wait for Electron close before driver exit

This commit is contained in:
ethernet
2026-09-06 21:15:16 -04:00
parent 499edee606
commit 86110e821c
4 changed files with 69 additions and 2 deletions
+33
View File
@@ -0,0 +1,33 @@
import { EventEmitter } from 'node:events'
import { createRequire } from 'node:module'
import { expect, it, vi } from 'vitest'
const { observeProcessClose } = createRequire(import.meta.url)('../tests/install/e2e-assets/process-close.cjs')
it('waits for native close, not exit, and retains a close observed before hand-off', async () => {
const child = new EventEmitter()
const waitForClose = observeProcessClose(child)
let finished = false
const completion = waitForClose().then(() => { finished = true })
child.emit('exit', 0)
await Promise.resolve()
expect(finished).toBe(false)
child.emit('close', 0)
await completion
expect(finished).toBe(true)
await expect(waitForClose()).resolves.toBeUndefined()
})
it('fails if the launched process never closes', async () => {
vi.useFakeTimers()
try {
const waitForClose = observeProcessClose(new EventEmitter())
const completion = expect(waitForClose(2_000)).rejects.toThrow('Electron process did not close')
await vi.advanceTimersByTimeAsync(2_000)
await completion
expect(vi.getTimerCount()).toBe(0)
} finally {
vi.useRealTimers()
}
})
+10 -2
View File
@@ -21,6 +21,7 @@ const fs = require('node:fs')
const { _electron } = require('@playwright/test')
const { prepareWindowForInput } = require('./window-input.cjs')
const { observeProcessClose } = require('./process-close.cjs')
const exePath = process.argv[2]
const proofDir = process.argv[3]
@@ -93,6 +94,9 @@ async function main() {
env: { ...process.env },
timeout: 120_000
})
const child = app.process()
const waitForProcessClose = observeProcessClose(child)
log(`launched Electron pid=${child.pid}`)
// firstWindow() can grab a helper webContents (wake indicator etc.), not
// the main app window. Pick the window that actually renders UI (has a
@@ -255,7 +259,8 @@ async function main() {
// The "Updating Hermes — this window will close" overlay should appear,
// then the app quits (hand-off dwell). Screenshot the overlay while the
// window is still alive.
await page.waitForTimeout(1200)
// The app can close during the dwell. This wait must outlive its page.
await new Promise(resolve => setTimeout(resolve, 1200))
await shot(page, '05-updating-overlay')
// ── Wait for the hand-off to take over ────────────────────────────────
@@ -315,7 +320,10 @@ async function main() {
throw new Error('no hand-off within 150s of Update now (no marker, no result, app still alive)')
}
log('hand-off confirmed — detached updater owns the rest')
// A marker appears before Electron exits. Exiting this driver at that point
// lets Playwright taskkill the entire tree, including the detached updater.
await waitForProcessClose()
log('Electron process closed — detached updater owns the rest')
}
main()
@@ -0,0 +1,25 @@
// Observe at launch: a renderer can close before the native process and its
// stdio pipes. Playwright's driver exit cleanup tree-kills until that close.
function observeProcessClose(child) {
let closed = false
const completion = new Promise(resolve => child.once('close', () => {
closed = true
resolve()
}))
return async function waitForClose(timeoutMs = 120_000) {
if (closed) return
let timer
try {
await Promise.race([
completion,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Electron process did not close after update hand-off')), timeoutMs)
}),
])
} finally {
clearTimeout(timer)
}
}
}
module.exports = { observeProcessClose }
+1
View File
@@ -763,6 +763,7 @@ function Invoke-GuiUpdateDesktopRoute([string]$TargetSha) {
$driver = Join-Path $driverDir "e2e-drive-update.cjs"
Copy-Item (Join-Path $AssetsDir "drive-update.cjs") $driver -Force
Copy-Item (Join-Path $AssetsDir "window-input.cjs") (Join-Path $driverDir "window-input.cjs") -Force
Copy-Item (Join-Path $AssetsDir "process-close.cjs") (Join-Path $driverDir "process-close.cjs") -Force
Push-Location $driverDir
$prevEap = $ErrorActionPreference
$ErrorActionPreference = "Continue"