test(e2e): enhance Electron E2E tests with new build configurations and clear favorites functionality

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-19 23:09:17 +02:00
1 parent 8ca2e927a2
commit 973423a2b5
7 files changed
+187 -8

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
],
"targets": {
"e2e": {
"dependsOn": ["electron-backend:build"]
"dependsOn": ["electron-backend:build-e2e"]
}
}
}
@@ -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(/<base\s+href="([^"]*)"/i);
const baseHref = baseHrefMatch?.[1] ?? '<missing>';
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<Page> {
await new Promise((resolvePromise) => setTimeout(resolvePromise, 2000));
@@ -183,14 +206,37 @@ async function findMainWindow(app: ElectronApplication): Promise<Page> {
async function waitForAppReady(page: Page): Promise<void> {
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') ?? '<missing>',
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<void> {
@@ -665,6 +711,21 @@ export async function switchUnifiedCollectionContent(
await clickButtonToggleOption(toggleGroup, contentLabel);
}
export async function clearCurrentUnifiedCollection(
page: Page
): Promise<void> {
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
@@ -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,
+21
View File
@@ -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": {
+6
View File
@@ -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",
+12
View File
@@ -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",
+12
View File
@@ -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": [