From 973423a2b591bd18d88d7e987efaca5540f34eb6 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 19 Apr 2026 23:09:17 +0200 Subject: [PATCH] test(e2e): enhance Electron E2E tests with new build configurations and clear favorites functionality Entire-Checkpoint: c6e522b4276c --- apps/electron-backend-e2e/project.json | 2 +- .../src/electron-test-fixtures.ts | 75 +++++++++++++++++-- .../electron-backend-e2e/src/favorites.e2e.ts | 67 +++++++++++++++++ apps/electron-backend/project.json | 21 ++++++ apps/remote-control-web/project.json | 6 ++ apps/web/project.json | 12 +++ nx.json | 12 +++ 7 files changed, 187 insertions(+), 8 deletions(-) diff --git a/apps/electron-backend-e2e/project.json b/apps/electron-backend-e2e/project.json index 644eae15f..4d777d241 100644 --- a/apps/electron-backend-e2e/project.json +++ b/apps/electron-backend-e2e/project.json @@ -10,7 +10,7 @@ ], "targets": { "e2e": { - "dependsOn": ["electron-backend:build"] + "dependsOn": ["electron-backend:build-e2e"] } } } diff --git a/apps/electron-backend-e2e/src/electron-test-fixtures.ts b/apps/electron-backend-e2e/src/electron-test-fixtures.ts index b06856276..2622c1330 100644 --- a/apps/electron-backend-e2e/src/electron-test-fixtures.ts +++ b/apps/electron-backend-e2e/src/electron-test-fixtures.ts @@ -23,6 +23,10 @@ export const electronMainPath = join( workspaceRoot, 'dist/apps/electron-backend/main.js' ); +export const packagedRendererIndexPath = join( + workspaceRoot, + 'dist/apps/web/index.html' +); export const m3uFixturePath = join( workspaceRoot, 'apps/web-e2e/src/fixtures/test.m3u' @@ -124,6 +128,7 @@ export async function launchElectronApp( `Electron build not found at ${electronMainPath}. Run the build before executing the E2E suite.` ); } + assertPackagedRendererBuildIsElectronSafe(); const args = [electronMainPath]; @@ -164,6 +169,24 @@ export async function closeElectronApp( } } +function assertPackagedRendererBuildIsElectronSafe(): void { + if (!existsSync(packagedRendererIndexPath)) { + throw new Error( + `Renderer build not found at ${packagedRendererIndexPath}. Run pnpm nx run electron-backend:build-e2e before executing the Electron E2E suite.` + ); + } + + const indexHtml = readFileSync(packagedRendererIndexPath, 'utf8'); + const baseHrefMatch = indexHtml.match(/'; + + if (baseHref !== './') { + throw new Error( + `Renderer build at ${packagedRendererIndexPath} is not file-safe for packaged Electron. Found base href ${JSON.stringify(baseHref)}. Run pnpm nx run electron-backend:build-e2e or use an Electron E2E Nx target so dist/apps/web is rebuilt with the electron-e2e configuration.` + ); + } +} + async function findMainWindow(app: ElectronApplication): Promise { await new Promise((resolvePromise) => setTimeout(resolvePromise, 2000)); @@ -183,14 +206,37 @@ async function findMainWindow(app: ElectronApplication): Promise { async function waitForAppReady(page: Page): Promise { await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('app-root', { timeout: 30000 }); - await page.waitForFunction( - () => { - const appRoot = document.querySelector('app-root'); + try { + await page.waitForFunction( + () => { + const appRoot = document.querySelector('app-root'); - return Boolean(appRoot && appRoot.innerHTML.trim().length > 0); - }, - { timeout: 30000 } - ); + return Boolean(appRoot && appRoot.innerHTML.trim().length > 0); + }, + { timeout: 30000 } + ); + } catch (error) { + const diagnostics = await page.evaluate(() => ({ + appRootLength: + document.querySelector('app-root')?.innerHTML.trim().length ?? 0, + baseHref: + document + .querySelector('base') + ?.getAttribute('href') ?? '', + readyState: document.readyState, + title: document.title, + url: location.href, + })); + + const reason = + error instanceof Error ? error.message : 'unknown startup error'; + + throw new Error( + `Electron app did not render visible app-root within 30000ms. ${reason}. Diagnostics: ${JSON.stringify( + diagnostics + )}` + ); + } } export async function openAddPlaylistDialog(page: Page): Promise { @@ -665,6 +711,21 @@ export async function switchUnifiedCollectionContent( await clickButtonToggleOption(toggleGroup, contentLabel); } +export async function clearCurrentUnifiedCollection( + page: Page +): Promise { + await page + .getByRole('button', { + name: /Clear .* (favorites|recently viewed)/i, + }) + .click(); + + const dialog = page.locator('mat-dialog-container').last(); + await expect(dialog).toBeVisible(); + await dialog.getByRole('button', { name: /^Yes$/i }).click(); + await expect(dialog).toBeHidden(); +} + async function clickButtonToggleOption( toggleGroup: Locator, label: string diff --git a/apps/electron-backend-e2e/src/favorites.e2e.ts b/apps/electron-backend-e2e/src/favorites.e2e.ts index a03457a4f..d6882734d 100644 --- a/apps/electron-backend-e2e/src/favorites.e2e.ts +++ b/apps/electron-backend-e2e/src/favorites.e2e.ts @@ -3,6 +3,7 @@ import { addStalkerPortal, addXtreamPortal, channelItemByTitle, + clearCurrentUnifiedCollection, clickCategoryById, clickGridListCardByTitle, clickCategoryByNameExact, @@ -105,6 +106,72 @@ test.describe('Electron Favorites', () => { } }); + test('keeps playlist M3U favorites cleared after navigating away and back', async ({ + dataDir, + }) => { + const playlistTitle = 'm3u-clear-favorites-source.m3u'; + const playlistSourceTitle = 'm3u-clear-favorites-source'; + const filePath = writeTemporaryM3uFile(dataDir, playlistTitle, [ + { + groupTitle: 'News', + name: 'M3U Clear Favorite One', + url: 'https://streams.example.test/m3u-clear-favorite-one.m3u8', + }, + { + groupTitle: 'News', + name: 'M3U Clear Favorite Two', + url: 'https://streams.example.test/m3u-clear-favorite-two.m3u8', + }, + ]); + const app = await launchElectronApp(dataDir); + + try { + await importM3uPlaylistFromNativeDialog(app, filePath); + await waitForM3uCatalog(app.mainWindow); + await toggleFavoriteForChannel( + app.mainWindow, + 'M3U Clear Favorite One' + ); + await toggleFavoriteForChannel( + app.mainWindow, + 'M3U Clear Favorite Two' + ); + await openPlaylistFavorites(app.mainWindow); + + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite One') + ).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite Two') + ).toHaveCount(1); + + await clearCurrentUnifiedCollection(app.mainWindow); + + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite One') + ).toHaveCount(0); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite Two') + ).toHaveCount(0); + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, playlistSourceTitle) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + await openPlaylistFavorites(app.mainWindow); + + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite One') + ).toHaveCount(0); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Clear Favorite Two') + ).toHaveCount(0); + } finally { + await closeElectronApp(app); + } + }); + test('shows Xtream live, movie, and series favorites in playlist and all-playlists scope, and preserves them after restart', async ({ dataDir, request, diff --git a/apps/electron-backend/project.json b/apps/electron-backend/project.json index 06a0bbf51..a925fd5be 100644 --- a/apps/electron-backend/project.json +++ b/apps/electron-backend/project.json @@ -41,6 +41,27 @@ } } }, + "build-e2e": { + "dependsOn": [ + "electron-backend:build-worker", + { + "projects": ["web"], + "target": "build-e2e" + }, + { + "projects": ["remote-control-web"], + "target": "build-e2e" + } + ], + "executor": "nx-electron:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/electron-backend", + "main": "apps/electron-backend/src/main.ts", + "tsConfig": "apps/electron-backend/tsconfig.app.json", + "assets": ["apps/electron-backend/src/assets"] + } + }, "serve": { "executor": "nx:run-commands", "options": { diff --git a/apps/remote-control-web/project.json b/apps/remote-control-web/project.json index 437e5542f..53cf78907 100644 --- a/apps/remote-control-web/project.json +++ b/apps/remote-control-web/project.json @@ -57,6 +57,12 @@ }, "defaultConfiguration": "production" }, + "build-e2e": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm nx run remote-control-web:build:development" + } + }, "serve": { "continuous": true, "executor": "@angular/build:dev-server", diff --git a/apps/web/project.json b/apps/web/project.json index e5de67e89..bf423df83 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -102,10 +102,22 @@ "optimization": false, "extractLicenses": false, "sourceMap": true + }, + "electron-e2e": { + "baseHref": "./", + "optimization": false, + "extractLicenses": false, + "sourceMap": true } }, "defaultConfiguration": "production" }, + "build-e2e": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm nx run web:build:electron-e2e" + } + }, "serve": { "continuous": true, "executor": "@angular/build:dev-server", diff --git a/nx.json b/nx.json index 6e0773a20..89dd5aceb 100644 --- a/nx.json +++ b/nx.json @@ -50,6 +50,18 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "e2e-ci--src/*.e2e.ts": { + "dependsOn": [ + { + "projects": ["electron-backend"], + "target": "build-e2e" + }, + { + "projects": ["stalker-mock-server", "xtream-mock-server"], + "target": "serve" + } + ] } }, "plugins": [