diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 7190a00bd..fe56b10ce 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -12,6 +12,7 @@ jobs: electron-e2e-tests: name: Electron E2E on ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 45 env: IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS: '1' NX_SKIP_NX_CACHE: true diff --git a/apps/electron-backend-e2e/src/electron-test-fixtures.ts b/apps/electron-backend-e2e/src/electron-test-fixtures.ts index c1f8e57bf..fb45b4925 100644 --- a/apps/electron-backend-e2e/src/electron-test-fixtures.ts +++ b/apps/electron-backend-e2e/src/electron-test-fixtures.ts @@ -40,6 +40,10 @@ export const defaultStalkerPortalName = 'Mock Stalker Portal'; export const defaultXtreamUsername = 'user1'; export const defaultXtreamPassword = 'pass1'; export const defaultStalkerMacAddress = '00:1A:79:00:00:01'; +const electronAppCloseTimeoutMs = Number( + process.env['IPTVNATOR_E2E_CLOSE_TIMEOUT_MS'] ?? '10000' +); +const electronAppKillWaitMs = 2000; export type PortalProvider = 'stalker' | 'xtream'; @@ -170,12 +174,57 @@ export async function closeElectronApp( app: LaunchedElectronApp ): Promise { try { - await app.electronApp.close(); + const closePromise = app.electronApp.close(); + const closed = await waitForPromiseWithTimeout( + closePromise, + electronAppCloseTimeoutMs + ); + + if (closed) { + return; + } + + console.warn( + `Electron app did not close within ${electronAppCloseTimeoutMs}ms; killing process` + ); + const childProcess = app.electronApp.process(); + + if (!childProcess.killed) { + childProcess.kill(); + } + + await waitForPromiseWithTimeout( + closePromise.catch(() => undefined), + electronAppKillWaitMs + ); } catch (error) { console.warn('Failed to close Electron app cleanly:', error); } } +async function waitForPromiseWithTimeout( + promise: Promise, + timeoutMs: number +): Promise { + let timeoutId: NodeJS.Timeout | undefined; + + try { + return await Promise.race([ + promise.then(() => true), + new Promise((resolvePromise) => { + timeoutId = setTimeout( + () => resolvePromise(false), + timeoutMs + ); + }), + ]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } +} + function assertPackagedRendererBuildIsElectronSafe(): void { if (!existsSync(packagedRendererIndexPath)) { throw new Error(