Files
iptvnator/apps/electron-backend-e2e/src/window-controls.e2e.ts
T
4gray c0e065da17 fix(window-controls): derive window-state pushes from events so controls reappear after fullscreen (#1178)
The custom minimize/maximize/close controls stayed hidden forever after
leaving HTML-element (video player) fullscreen on Windows: window state was
polled at event time, and isFullScreen() can still report the pre-transition
value while 'leave-full-screen' fires, leaving a stale push with no later
event to correct it. The same polling on the companion flag cleared
isMaximized during fullscreen transitions and stuck the maximize/restore
glyph on the wrong icon.

attachWindowStateEvents now seeds the state once at window creation and each
event patches only the flag it names, sending a copy per push. The
enter/leave-html-full-screen variants are wired too.

Regression coverage: app-window-state.spec.ts (9 cases, 6 of which fail
against the old implementation) and an Electron E2E case that toggles HTML
element fullscreen and asserts the controls come back.
2026-07-26 01:49:08 +02:00

190 lines
6.6 KiB
TypeScript

import {
closeElectronApp,
expect,
launchElectronApp,
test,
} from './electron-test-fixtures';
// Custom window controls are only rendered on Windows/Linux; macOS keeps
// the native traffic lights.
//
// Linux CI runs Electron under xvfb, which has no window manager, so
// maximize/minimize never take effect there — those tests are skipped and
// rely on Windows CI plus local Linux/macOS runs for coverage.
const isWindowManagerlessCi =
process.platform === 'linux' && !!process.env['CI'];
test.describe('Custom window controls', () => {
test.skip(
process.platform === 'darwin',
'macOS uses native traffic lights instead of custom controls'
);
test('@electron renders the window control buttons', async ({
dataDir,
}) => {
const app = await launchElectronApp(dataDir);
try {
await expect(
app.mainWindow.getByTestId('window-minimize')
).toBeVisible();
await expect(
app.mainWindow.getByTestId('window-maximize')
).toBeVisible();
await expect(
app.mainWindow.getByTestId('window-close')
).toBeVisible();
await expect(
app.mainWindow.getByTestId('window-maximize-glyph')
).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('@electron maximize button toggles the native window state', async ({
dataDir,
}) => {
test.skip(
isWindowManagerlessCi,
'xvfb on Linux CI has no window manager'
);
const app = await launchElectronApp(dataDir);
try {
const isMaximized = () =>
app.electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
return mainWindow ? mainWindow.isMaximized() : false;
});
await app.mainWindow.getByTestId('window-maximize').click();
await expect.poll(isMaximized, { timeout: 10_000 }).toBe(true);
await expect(
app.mainWindow.getByTestId('window-restore-glyph')
).toBeVisible();
await app.mainWindow.getByTestId('window-maximize').click();
await expect.poll(isMaximized, { timeout: 10_000 }).toBe(false);
await expect(
app.mainWindow.getByTestId('window-maximize-glyph')
).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('@electron reflects window state changes triggered from the main process', async ({
dataDir,
}) => {
test.skip(
isWindowManagerlessCi,
'xvfb on Linux CI has no window manager'
);
const app = await launchElectronApp(dataDir);
try {
await app.electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.maximize();
});
await expect(
app.mainWindow.getByTestId('window-restore-glyph')
).toBeVisible({ timeout: 10_000 });
await app.electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.unmaximize();
});
await expect(
app.mainWindow.getByTestId('window-maximize-glyph')
).toBeVisible({ timeout: 10_000 });
} finally {
await closeElectronApp(app);
}
});
test('@electron controls reappear after leaving HTML element fullscreen', async ({
dataDir,
}) => {
test.skip(
isWindowManagerlessCi,
'xvfb on Linux CI has no window manager'
);
const app = await launchElectronApp(dataDir);
try {
const controls = app.mainWindow.locator('app-window-controls');
await expect(controls).not.toHaveClass(/is-hidden/);
// The video players fullscreen their player root through the
// HTML element-fullscreen API. That API needs transient user
// activation, so trigger it from a real click on a temporary
// overlay instead of calling it directly from evaluate().
await app.mainWindow.evaluate(() => {
const overlay = document.createElement('div');
overlay.id = 'e2e-fullscreen-trigger';
overlay.style.cssText =
'position: fixed; inset: 0; z-index: 2147483647;';
overlay.addEventListener('click', () => {
overlay.remove();
void document.documentElement.requestFullscreen();
});
document.body.append(overlay);
});
await app.mainWindow.locator('#e2e-fullscreen-trigger').click();
const isHtmlFullScreen = () =>
app.mainWindow.evaluate(
() => document.fullscreenElement !== null
);
await expect.poll(isHtmlFullScreen, { timeout: 10_000 }).toBe(
true
);
await expect(controls).toHaveClass(/is-hidden/, {
timeout: 10_000,
});
await app.mainWindow.evaluate(() => document.exitFullscreen());
await expect.poll(isHtmlFullScreen, { timeout: 10_000 }).toBe(
false
);
// Regression: the exit push must not carry a stale
// isFullScreen=true read mid-transition — the controls have to
// come back once fullscreen is left.
await expect(controls).not.toHaveClass(/is-hidden/, {
timeout: 10_000,
});
await expect(
app.mainWindow.getByTestId('window-minimize')
).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('@electron minimize button minimizes the window', async ({
dataDir,
}) => {
test.skip(
isWindowManagerlessCi,
'xvfb on Linux CI has no window manager'
);
const app = await launchElectronApp(dataDir);
try {
const isMinimized = () =>
app.electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
return mainWindow ? mainWindow.isMinimized() : false;
});
await app.mainWindow.getByTestId('window-minimize').click();
await expect.poll(isMinimized, { timeout: 10_000 }).toBe(true);
} finally {
await closeElectronApp(app);
}
});
});