Files
iptvnator/apps/electron-backend-e2e/src/window-visibility.e2e.ts
T
b315f8564d fix(electron): show the main window once its document has loaded; enforce J1 IPC and mutation counters (#1828)
* fix(electron): show the main window once its document has loaded; enforce J1 IPC and mutation counters

Re-lands #1788, which merged into #1782's branch after #1782 had already
reached master, so none of it is on master.

The hidden main window was shown on ready-to-show only. On Linux under
X11, when the startup scripts run before the window's first frame, the
next frame comes about a second later: nothing is on screen and the
splash's requestAnimationFrame waits, so J1's first card came ~940 ms
after load instead of ~480 ms in most runner launches (18 bridge calls /
1,031-1,033 DOM mutations instead of 15 / 558).

The window is now shown at ready-to-show or the main frame's
did-finish-load, whichever comes first, with the splash colour as its
background so showing before the first paint does not flash. The journey
gate keeps the app's did-finish-load listeners away from its about:blank
detour, as it already does for ready-to-show.

Three dispatched runs on this branch (37192092882, 37192097790,
37192103151) read 15 calls and 558 mutations in all 18 iterations,
stable: true. Both become baselines (slack 0), and the Performance
journeys job checks them with check-journey-ratchet.mjs --only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(perf): record the evidence PR of the J1 runtime baselines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: 4gray <fourgray@proton.me>
2026-10-06 10:34:08 +02:00

149 lines
5.2 KiB
TypeScript

import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { expect, test } from './electron-test-fixtures';
import {
launchUnautomatedElectronApp,
type UnautomatedElectronApp,
} from './electron-unautomated-launch';
// Linux CI runs Electron under xvfb without a window manager, so a minimize
// request never takes effect there (see window-controls.e2e.ts).
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<string>('document.visibilityState');
const setWindowState = (
app: UnautomatedElectronApp,
action: 'hide' | 'show' | 'minimize' | 'restore'
) =>
app.evaluateInMain(
`electron.BrowserWindow.getAllWindows()[0].${action}();`
);
/**
* The app creates its window with `show: false` and shows it at
* `ready-to-show` or `did-finish-load`, whichever comes first; a `hide()`
* sent earlier would be undone by that `show()`.
*/
async function waitUntilShown(app: UnautomatedElectronApp): Promise<void> {
await expect
.poll(
() =>
app.evaluateInMain<boolean>(
'return electron.BrowserWindow.getAllWindows()[0]?.isVisible() ?? false;'
),
{ timeout: 30_000 }
)
.toBe(true);
await expect.poll(() => visibility(app)).toBe('visible');
}
/**
* The renderer must see a hidden or minimized window as hidden: idle timers
* pause on `visibilitychange`, and the playback keep-awake gate releases the
* display for a minimized window. A main window created with
* `backgroundThrottling: false` keeps reporting `visible` in both cases.
*
* Launched without Playwright: its focus emulation pins every page it
* attaches to as visible (see `electron-unautomated-launch.ts`).
*/
test.describe('Main window visibility', () => {
test('@electron reports a hidden window as hidden and a shown one as visible', async ({
dataDir,
}) => {
const app = await launchUnautomatedElectronApp(dataDir);
try {
await waitUntilShown(app);
await setWindowState(app, 'hide');
await expect
.poll(() => visibility(app), { timeout: 10_000 })
.toBe('hidden');
await setWindowState(app, 'show');
await expect
.poll(() => visibility(app), { timeout: 10_000 })
.toBe('visible');
} finally {
await app.close();
}
});
test('@electron reports a minimized window as hidden until it is restored', async ({
dataDir,
}) => {
test.skip(
isWindowManagerlessCi,
'xvfb on Linux CI has no window manager'
);
const app = await launchUnautomatedElectronApp(dataDir);
try {
await waitUntilShown(app);
await setWindowState(app, 'minimize');
await expect
.poll(() => visibility(app), { timeout: 10_000 })
.toBe('hidden');
await setWindowState(app, 'restore');
await expect
.poll(() => visibility(app), { timeout: 10_000 })
.toBe('visible');
} finally {
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<number>(
'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();
}
});
});