From c980ced90aefa9720fc1bdf0ae0110d31ced5d85 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 28 Sep 2026 21:56:17 +0200 Subject: [PATCH] test(e2e): check the playback display lock is released while hidden Review follow-ups for #1724: - Add an E2E that plays the webm fixture in the unautomated launch, records the main process's prevent-display-sleep blockers, and asserts the keep-awake lock is taken while visible, released when the window is hidden, and taken again when it is shown. The visibility tests alone would still pass if the renderer gate or the bridge stopped updating powerSaveBlocker. - Validate CDP replies before dispatch (CodeQL js/unvalidated-dynamic-method-call): only a numeric id with a pending settle function is called. Co-Authored-By: Claude Opus 5.5 --- .../src/electron-unautomated-launch.ts | 15 ++++-- .../src/window-visibility.e2e.ts | 52 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/apps/electron-backend-e2e/src/electron-unautomated-launch.ts b/apps/electron-backend-e2e/src/electron-unautomated-launch.ts index ee29ff715..7abb8ad19 100644 --- a/apps/electron-backend-e2e/src/electron-unautomated-launch.ts +++ b/apps/electron-backend-e2e/src/electron-unautomated-launch.ts @@ -55,9 +55,18 @@ async function openCdpSocket(url: string): Promise { ); }); socket.addEventListener('message', (event) => { - const message = JSON.parse(String(event.data)); - pending.get(message.id)?.(message); - pending.delete(message.id); + const message = JSON.parse(String(event.data)) as Record< + string, + unknown + >; + // Only replies to our own numbered requests settle a promise; events + // and unknown ids are ignored. + const id = message['id']; + if (typeof id !== 'number') return; + const settle = pending.get(id); + if (typeof settle !== 'function') return; + pending.delete(id); + settle(message); }); return { send: (method, params = {}) => diff --git a/apps/electron-backend-e2e/src/window-visibility.e2e.ts b/apps/electron-backend-e2e/src/window-visibility.e2e.ts index 5810607c1..ae323c541 100644 --- a/apps/electron-backend-e2e/src/window-visibility.e2e.ts +++ b/apps/electron-backend-e2e/src/window-visibility.e2e.ts @@ -1,3 +1,5 @@ +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { expect, test } from './electron-test-fixtures'; import { launchUnautomatedElectronApp, @@ -9,6 +11,10 @@ import { const isWindowManagerlessCi = process.platform === 'linux' && !!process.env['CI']; +const videoFixtureUrl = pathToFileURL( + join(__dirname, '../../web-e2e/src/fixtures/playback/episode.webm') +).href; + const visibility = (app: UnautomatedElectronApp) => app.evaluateInPage('document.visibilityState'); @@ -92,4 +98,50 @@ test.describe('Main window visibility', () => { await app.close(); } }); + + test('@electron releases the playback display lock while the window is hidden', async ({ + dataDir, + }) => { + const app = await launchUnautomatedElectronApp(dataDir); + try { + await waitUntilShown(app); + // Record the display-sleep blockers the keep-awake service holds. + await app.evaluateInMain(` + const blocker = electron.powerSaveBlocker; + const active = (globalThis.__e2eDisplayBlockers = new Set()); + const start = blocker.start.bind(blocker); + const stop = blocker.stop.bind(blocker); + blocker.start = (type) => { + const id = start(type); + if (type === 'prevent-display-sleep') active.add(id); + return id; + }; + blocker.stop = (id) => { + active.delete(id); + return stop(id); + }; + `); + const heldBlockers = () => + app.evaluateInMain( + 'return globalThis.__e2eDisplayBlockers.size;' + ); + await app.evaluateInPage(`(() => { + const video = document.createElement('video'); + video.muted = true; + video.loop = true; + video.src = ${JSON.stringify(videoFixtureUrl)}; + document.body.append(video); + return video.play().then(() => true); + })()`); + await expect.poll(heldBlockers, { timeout: 10_000 }).toBe(1); + + await setWindowState(app, 'hide'); + await expect.poll(heldBlockers, { timeout: 10_000 }).toBe(0); + + await setWindowState(app, 'show'); + await expect.poll(heldBlockers, { timeout: 10_000 }).toBe(1); + } finally { + await app.close(); + } + }); });