mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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.
243 lines
7.8 KiB
TypeScript
243 lines
7.8 KiB
TypeScript
/**
|
|
* Regression coverage for the WINDOW:STATE_CHANGED pushes that drive the
|
|
* renderer-drawn window controls on Windows/Linux.
|
|
*
|
|
* The handlers must never re-read window state at event time: on Windows,
|
|
* isFullScreen() can still report true while 'leave-full-screen' fires for
|
|
* HTML-element fullscreen (video player) exits, and isMaximized() reports
|
|
* false while the window is fullscreen. Polling pushed a stale
|
|
* `isFullScreen: true` on exit — leaving the custom window controls hidden
|
|
* forever — and cleared the companion `isMaximized`, sticking the
|
|
* maximize/restore glyph on the wrong icon. Instead the state is seeded once
|
|
* at attach time and each event patches only the flag it names.
|
|
*/
|
|
|
|
jest.mock('electron', () => ({
|
|
app: {
|
|
getPath: jest.fn(() => '/tmp'),
|
|
isPackaged: false,
|
|
isReady: jest.fn(() => false),
|
|
on: jest.fn(),
|
|
},
|
|
BrowserWindow: jest.fn(),
|
|
Menu: {
|
|
buildFromTemplate: jest.fn(),
|
|
},
|
|
screen: {
|
|
getPrimaryDisplay: jest.fn(),
|
|
},
|
|
session: {
|
|
defaultSession: {
|
|
clearStorageData: jest.fn(),
|
|
},
|
|
},
|
|
shell: {
|
|
openExternal: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('./services/store.service', () => ({
|
|
store: {
|
|
get: jest.fn(),
|
|
set: jest.fn(),
|
|
},
|
|
WINDOW_BOUNDS: 'windowBounds',
|
|
}));
|
|
|
|
import { WINDOW_STATE_CHANGED } from '@iptvnator/shared/interfaces';
|
|
import App from './app';
|
|
|
|
type MockStateWindow = {
|
|
isDestroyed: jest.Mock<boolean, []>;
|
|
isFullScreen: jest.Mock<boolean, []>;
|
|
isMaximized: jest.Mock<boolean, []>;
|
|
on: jest.Mock<void, [string, () => void]>;
|
|
webContents: {
|
|
send: jest.Mock<void, [string, unknown]>;
|
|
};
|
|
};
|
|
|
|
function createMockStateWindow(): MockStateWindow {
|
|
return {
|
|
isDestroyed: jest.fn<boolean, []>().mockReturnValue(false),
|
|
isFullScreen: jest.fn<boolean, []>().mockReturnValue(false),
|
|
isMaximized: jest.fn<boolean, []>().mockReturnValue(false),
|
|
on: jest.fn<void, [string, () => void]>(),
|
|
webContents: {
|
|
send: jest.fn<void, [string, unknown]>(),
|
|
},
|
|
};
|
|
}
|
|
|
|
function attachWindowStateEvents(win: MockStateWindow): void {
|
|
(
|
|
App as unknown as {
|
|
attachWindowStateEvents: (win: MockStateWindow) => void;
|
|
}
|
|
).attachWindowStateEvents(win);
|
|
}
|
|
|
|
function fireWindowEvent(win: MockStateWindow, eventName: string): void {
|
|
const handlers = win.on.mock.calls
|
|
.filter(([name]) => name === eventName)
|
|
.map(([, handler]) => handler);
|
|
|
|
expect(handlers).toHaveLength(1);
|
|
handlers[0]();
|
|
}
|
|
|
|
describe('window state change pushes', () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(
|
|
process,
|
|
'platform'
|
|
);
|
|
|
|
function setProcessPlatform(platform: NodeJS.Platform): void {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: platform,
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
setProcessPlatform('win32');
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, 'platform', originalPlatform);
|
|
}
|
|
});
|
|
|
|
it('registers no window-state listeners on macOS', () => {
|
|
setProcessPlatform('darwin');
|
|
const win = createMockStateWindow();
|
|
|
|
attachWindowStateEvents(win);
|
|
|
|
expect(win.on).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('pushes isFullScreen=false on leave-full-screen even while the window still reports fullscreen', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
// Windows still reports the old state while the transition runs.
|
|
win.isFullScreen.mockReturnValue(true);
|
|
|
|
fireWindowEvent(win, 'leave-full-screen');
|
|
|
|
expect(win.webContents.send).toHaveBeenCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: false, isFullScreen: false }
|
|
);
|
|
});
|
|
|
|
it('pushes isFullScreen=false on leave-html-full-screen even while the window still reports fullscreen', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
win.isFullScreen.mockReturnValue(true);
|
|
|
|
fireWindowEvent(win, 'leave-html-full-screen');
|
|
|
|
expect(win.webContents.send).toHaveBeenCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: false, isFullScreen: false }
|
|
);
|
|
});
|
|
|
|
it('pushes isFullScreen=true on enter events before the window reports fullscreen', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
win.isFullScreen.mockReturnValue(false);
|
|
|
|
fireWindowEvent(win, 'enter-full-screen');
|
|
fireWindowEvent(win, 'enter-html-full-screen');
|
|
|
|
expect(win.webContents.send).toHaveBeenCalledTimes(2);
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: false, isFullScreen: true }
|
|
);
|
|
});
|
|
|
|
it('derives isMaximized from the maximize/unmaximize events instead of polling', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
|
|
win.isMaximized.mockReturnValue(false);
|
|
fireWindowEvent(win, 'maximize');
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: true, isFullScreen: false }
|
|
);
|
|
|
|
win.isMaximized.mockReturnValue(true);
|
|
fireWindowEvent(win, 'unmaximize');
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: false, isFullScreen: false }
|
|
);
|
|
});
|
|
|
|
it('keeps the maximized flag across a fullscreen round-trip while the window misreports it', () => {
|
|
const win = createMockStateWindow();
|
|
win.isMaximized.mockReturnValue(true);
|
|
attachWindowStateEvents(win);
|
|
// Windows reports a fullscreen window as not maximized, so polling
|
|
// the companion flag here would clear it and leave the glyph on
|
|
// "maximize" after the controls come back.
|
|
win.isMaximized.mockReturnValue(false);
|
|
|
|
fireWindowEvent(win, 'enter-html-full-screen');
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: true, isFullScreen: true }
|
|
);
|
|
|
|
fireWindowEvent(win, 'leave-html-full-screen');
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: true, isFullScreen: false }
|
|
);
|
|
});
|
|
|
|
it('keeps the fullscreen flag when a maximize event arrives mid-transition', () => {
|
|
const win = createMockStateWindow();
|
|
win.isFullScreen.mockReturnValue(true);
|
|
attachWindowStateEvents(win);
|
|
win.isFullScreen.mockReturnValue(false);
|
|
|
|
fireWindowEvent(win, 'maximize');
|
|
|
|
expect(win.webContents.send).toHaveBeenLastCalledWith(
|
|
WINDOW_STATE_CHANGED,
|
|
{ isMaximized: true, isFullScreen: true }
|
|
);
|
|
});
|
|
|
|
it('sends an independent payload per push', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
|
|
fireWindowEvent(win, 'enter-full-screen');
|
|
const firstPayload = win.webContents.send.mock.calls[0][1];
|
|
fireWindowEvent(win, 'leave-full-screen');
|
|
|
|
// The renderer must not see the earlier push mutate under it.
|
|
expect(firstPayload).toEqual({
|
|
isMaximized: false,
|
|
isFullScreen: true,
|
|
});
|
|
});
|
|
|
|
it('skips pushes for destroyed windows', () => {
|
|
const win = createMockStateWindow();
|
|
attachWindowStateEvents(win);
|
|
win.isDestroyed.mockReturnValue(true);
|
|
|
|
fireWindowEvent(win, 'leave-full-screen');
|
|
|
|
expect(win.webContents.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|