mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(electron): load renderer after ipc bootstrap
This commit is contained in:
1 parent
f7eb34ddba
commit
f1fc5ec7b2
3 files changed
+112
-11
No files matched your search
@@ -37,28 +37,49 @@ import {
|
||||
isTrustedRendererNavigationUrl,
|
||||
} from './app';
|
||||
import App from './app';
|
||||
import { app as electronApp } from 'electron';
|
||||
import { app as electronApp, BrowserWindow, screen } from 'electron';
|
||||
import { store } from './services/store.service';
|
||||
|
||||
type MockMainWindow = {
|
||||
center: jest.Mock<void, []>;
|
||||
getNormalBounds: jest.Mock<object, []>;
|
||||
loadFile: jest.Mock<Promise<void>, [string]>;
|
||||
loadURL: jest.Mock<Promise<void>, [string]>;
|
||||
on: jest.Mock<void, [string, (...args: unknown[]) => void]>;
|
||||
once: jest.Mock<void, [string, (...args: unknown[]) => void]>;
|
||||
setMenu: jest.Mock<void, [unknown]>;
|
||||
show: jest.Mock<void, []>;
|
||||
webContents: {
|
||||
on: jest.Mock<void, [string, (...args: unknown[]) => void]>;
|
||||
openDevTools: jest.Mock<void, []>;
|
||||
setWindowOpenHandler: jest.Mock<void, [unknown]>;
|
||||
};
|
||||
};
|
||||
|
||||
function createMockMainWindow(): MockMainWindow {
|
||||
return {
|
||||
center: jest.fn<void, []>(),
|
||||
getNormalBounds: jest.fn<object, []>().mockReturnValue({}),
|
||||
loadFile: jest.fn<Promise<void>, [string]>().mockResolvedValue(),
|
||||
loadURL: jest.fn<Promise<void>, [string]>().mockResolvedValue(),
|
||||
on: jest.fn<void, [string, (...args: unknown[]) => void]>(),
|
||||
once: jest.fn<void, [string, (...args: unknown[]) => void]>(),
|
||||
setMenu: jest.fn<void, [unknown]>(),
|
||||
show: jest.fn<void, []>(),
|
||||
webContents: {
|
||||
on: jest.fn<void, [string, (...args: unknown[]) => void]>(),
|
||||
openDevTools: jest.fn<void, []>(),
|
||||
setWindowOpenHandler: jest.fn<void, [unknown]>(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type AppInternals = {
|
||||
mainWindow: MockMainWindow;
|
||||
loadedMainWindow: MockMainWindow | null;
|
||||
mainWindow: MockMainWindow | null;
|
||||
mainWindowLoadPromise: Promise<void> | null;
|
||||
onReady: () => void;
|
||||
rendererLoadingEnabled: boolean;
|
||||
loadMainWindow: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -70,7 +91,16 @@ describe('Electron app security helpers', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete process.env.ELECTRON_IS_DEV;
|
||||
const appInternals = getAppInternals();
|
||||
appInternals.loadedMainWindow = null;
|
||||
appInternals.mainWindow = null;
|
||||
appInternals.mainWindowLoadPromise = null;
|
||||
appInternals.rendererLoadingEnabled = false;
|
||||
(electronApp as unknown as { isPackaged: boolean }).isPackaged = false;
|
||||
(screen.getPrimaryDisplay as jest.Mock).mockReturnValue({
|
||||
workAreaSize: { height: 720, width: 1280 },
|
||||
});
|
||||
(store.get as jest.Mock).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it('creates an explicitly hardened BrowserWindow webPreferences object', () => {
|
||||
@@ -175,6 +205,31 @@ describe('Electron app security helpers', () => {
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('defers renderer loading until the main process explicitly enables it', async () => {
|
||||
const appInternals = getAppInternals();
|
||||
const mainWindow = createMockMainWindow();
|
||||
(BrowserWindow as unknown as jest.Mock).mockReturnValue(mainWindow);
|
||||
|
||||
appInternals.onReady();
|
||||
|
||||
expect(BrowserWindow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
show: false,
|
||||
webPreferences: expect.objectContaining({
|
||||
preload: expect.stringContaining('main.preload.js'),
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(mainWindow.loadURL).not.toHaveBeenCalled();
|
||||
expect(mainWindow.loadFile).not.toHaveBeenCalled();
|
||||
|
||||
await appInternals.loadMainWindow();
|
||||
|
||||
expect(mainWindow.loadURL).toHaveBeenCalledWith(
|
||||
'http://localhost:4200'
|
||||
);
|
||||
});
|
||||
|
||||
it('clears only service worker registrations and cache storage', async () => {
|
||||
await clearElectronServiceWorkerStorage();
|
||||
|
||||
|
||||
@@ -198,9 +198,12 @@ function attachWindowTrace(mainWindow: Electron.BrowserWindow): void {
|
||||
export default class App {
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
static mainWindow: Electron.BrowserWindow;
|
||||
static mainWindow: Electron.BrowserWindow | null = null;
|
||||
static application: Electron.App;
|
||||
static BrowserWindow;
|
||||
private static loadedMainWindow: Electron.BrowserWindow | null = null;
|
||||
private static mainWindowLoadPromise: Promise<void> | null = null;
|
||||
private static rendererLoadingEnabled = false;
|
||||
|
||||
private static shouldOpenDevTools() {
|
||||
return process.env.ELECTRON_OPEN_DEVTOOLS === '1';
|
||||
@@ -230,15 +233,21 @@ export default class App {
|
||||
App.mainWindow = null;
|
||||
}
|
||||
|
||||
private static startMainWindowLoad(): void {
|
||||
void App.loadMainWindow().catch((error) => {
|
||||
console.error('Failed to load main window:', error);
|
||||
});
|
||||
}
|
||||
|
||||
private static onReady() {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
if (rendererAppName) {
|
||||
App.initMainWindow();
|
||||
void App.loadMainWindow().catch((error) => {
|
||||
console.error('Failed to load main window:', error);
|
||||
});
|
||||
if (App.rendererLoadingEnabled) {
|
||||
App.startMainWindowLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +257,9 @@ export default class App {
|
||||
if (App.mainWindow === null) {
|
||||
App.onReady();
|
||||
}
|
||||
if (App.rendererLoadingEnabled) {
|
||||
App.startMainWindowLoad();
|
||||
}
|
||||
}
|
||||
|
||||
private static handleRendererNavigation(
|
||||
@@ -363,6 +375,8 @@ export default class App {
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
App.mainWindow = null;
|
||||
App.loadedMainWindow = null;
|
||||
App.mainWindowLoadPromise = null;
|
||||
});
|
||||
|
||||
App.mainWindow.on('close', () => {
|
||||
@@ -409,22 +423,49 @@ export default class App {
|
||||
});
|
||||
}
|
||||
|
||||
private static async loadMainWindow(): Promise<void> {
|
||||
private static async loadMainWindowContent(
|
||||
mainWindow: Electron.BrowserWindow
|
||||
): Promise<void> {
|
||||
// load the index.html of the app.
|
||||
if (App.isDevelopmentMode()) {
|
||||
const loadPromise = App.mainWindow.loadURL(
|
||||
const loadPromise = mainWindow.loadURL(
|
||||
`http://localhost:${rendererAppPort}`
|
||||
);
|
||||
if (App.shouldOpenDevTools()) {
|
||||
App.mainWindow.webContents.openDevTools();
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
await loadPromise;
|
||||
} else {
|
||||
await clearElectronServiceWorkerStorage();
|
||||
await App.mainWindow.loadFile(getPackagedRendererIndexPath());
|
||||
await mainWindow.loadFile(getPackagedRendererIndexPath());
|
||||
}
|
||||
}
|
||||
|
||||
static async loadMainWindow(): Promise<void> {
|
||||
App.rendererLoadingEnabled = true;
|
||||
|
||||
if (!rendererAppName || !App.mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (App.loadedMainWindow === App.mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!App.mainWindowLoadPromise) {
|
||||
const mainWindow = App.mainWindow;
|
||||
App.mainWindowLoadPromise = App.loadMainWindowContent(mainWindow)
|
||||
.then(() => {
|
||||
App.loadedMainWindow = mainWindow;
|
||||
})
|
||||
.finally(() => {
|
||||
App.mainWindowLoadPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
await App.mainWindowLoadPromise;
|
||||
}
|
||||
|
||||
static main(app: Electron.App, browserWindow: typeof BrowserWindow) {
|
||||
// we pass the Electron.App object and the
|
||||
// Electron.BrowserWindow into this function
|
||||
|
||||
@@ -112,7 +112,6 @@ export default class Main {
|
||||
DatabaseEvents.bootstrapDatabaseEvents();
|
||||
EpgEvents.bootstrapEpgEvents();
|
||||
RemoteControlEvents.bootstrapRemoteControlEvents();
|
||||
void appUpdateService.checkForUpdatesOnStartup();
|
||||
|
||||
// Set main window for downloads and reset stale downloads
|
||||
if (App.mainWindow) {
|
||||
@@ -124,6 +123,12 @@ export default class Main {
|
||||
trace('startup', 'reset-stale-downloads:done');
|
||||
}
|
||||
|
||||
// Load the renderer only after IPC handlers are registered. On slower
|
||||
// Linux CI hosts the renderer can otherwise invoke Electron bridge IPC
|
||||
// before the main process has installed handlers.
|
||||
await App.loadMainWindow();
|
||||
void appUpdateService.checkForUpdatesOnStartup();
|
||||
|
||||
if (isStartupTraceEnabled()) {
|
||||
trace('startup', 'bootstrap-events:done');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user