From 7f92dd359c485ee0264c6ae7e1830c0b48beb477 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 18 Apr 2026 10:17:13 +0200 Subject: [PATCH] test(e2e): streamline expect statements and improve inline detail tests for favorites and recent items - Simplified expect statements for channel items and content cards in favorites and recent tests. - Added new tests for opening Xtream favorite movies and series inline from favorites without switching playlists. - Enhanced existing tests to ensure consistent behavior when navigating back from detail views. - Introduced utility function expectInlineCollectionDetail to reduce code duplication in inline detail checks. - Improved error handling for back button clicks in detail views. Entire-Checkpoint: c6e522b4276c --- .../src/dashboard-activation.e2e.ts | 77 +++- .../src/electron-test-fixtures.ts | 193 ++++++++-- .../electron-backend-e2e/src/favorites.e2e.ts | 345 ++++++++++++++---- apps/electron-backend-e2e/src/recent.e2e.ts | 321 +++++++++++++--- 4 files changed, 754 insertions(+), 182 deletions(-) diff --git a/apps/electron-backend-e2e/src/dashboard-activation.e2e.ts b/apps/electron-backend-e2e/src/dashboard-activation.e2e.ts index 0c2422e68..b36c41409 100644 --- a/apps/electron-backend-e2e/src/dashboard-activation.e2e.ts +++ b/apps/electron-backend-e2e/src/dashboard-activation.e2e.ts @@ -6,6 +6,7 @@ import { closeElectronApp, defaultXtreamPassword, defaultXtreamUsername, + expectPathname, expect, goToDashboard, launchElectronApp, @@ -23,7 +24,7 @@ import { } from './portal-mock-fixtures'; test.describe('Dashboard Activation', () => { - test('opens live favorites in the collection route and movies/series in source detail routes from the dashboard', async ({ + test('opens live favorites in the collection route and movies/series in global collection detail views from the dashboard', async ({ dataDir, request, }) => { @@ -67,6 +68,7 @@ test.describe('Dashboard Activation', () => { vodFixture.categoryName ); const movieTitle = await clickFirstGridListCard(app.mainWindow); + await addCurrentDetailToFavorites(app.mainWindow); await playCurrentDetail(app.mainWindow); await goBackFromDetail(app.mainWindow); @@ -97,32 +99,44 @@ test.describe('Dashboard Activation', () => { await app.mainWindow.goBack(); await app.mainWindow.waitForURL(/\/workspace\/dashboard$/); + await dashboardActivityItemByTitle( + app.mainWindow, + 'Global Favorites', + movieTitle + ).click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/global-favorites$/, + title: movieTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname(app.mainWindow, /\/workspace\/dashboard$/); + await dashboardActivityItemByTitle( app.mainWindow, 'Recently Watched', movieTitle ).click(); - await app.mainWindow.waitForURL( - /\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+$/ - ); - await expect( - app.mainWindow.locator('app-content-hero') - ).toContainText(movieTitle); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/global-recent$/, + title: movieTitle, + }); await app.mainWindow.goBack(); - await app.mainWindow.waitForURL(/\/workspace\/dashboard$/); + await expectPathname(app.mainWindow, /\/workspace\/dashboard$/); await dashboardActivityItemByTitle( app.mainWindow, 'Recently Watched', seriesTitle ).click(); - await app.mainWindow.waitForURL( - /\/workspace\/xtreams\/[^/]+\/series\/[^/]+\/[^/]+$/ - ); - await expect( - app.mainWindow.locator('app-content-hero') - ).toContainText(seriesTitle); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/global-recent$/, + title: seriesTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname(app.mainWindow, /\/workspace\/dashboard$/); } finally { await closeElectronApp(app); } @@ -154,7 +168,40 @@ async function goBackFromDetail(page: Page): Promise { .first(); await expect(backButton).toBeVisible({ timeout: 20000 }); - await backButton.click(); + try { + await backButton.click({ timeout: 5000 }); + } catch { + await backButton.evaluate((button: HTMLButtonElement) => + button.click() + ); + } +} + +async function addCurrentDetailToFavorites(page: Page): Promise { + const addButton = page.locator('button.favorite-btn').first(); + + await expect(addButton).toBeVisible({ timeout: 20000 }); + await addButton.click(); + await expect( + page.locator('button.favorite-btn--active').first() + ).toBeVisible({ + timeout: 20000, + }); +} + +async function expectInlineCollectionDetail( + page: Page, + params: { + pathname: RegExp; + title: string; + } +): Promise { + await expectPathname(page, params.pathname); + await expect(page.locator('app-workspace-context-panel')).toHaveCount(0); + await expect(page.locator('app-content-hero')).toContainText(params.title); + await expect( + page.locator('app-content-hero .hero__back-button').first() + ).toBeVisible({ timeout: 20000 }); } async function playCurrentDetail(page: Page): Promise { diff --git a/apps/electron-backend-e2e/src/electron-test-fixtures.ts b/apps/electron-backend-e2e/src/electron-test-fixtures.ts index a952d9c04..b06856276 100644 --- a/apps/electron-backend-e2e/src/electron-test-fixtures.ts +++ b/apps/electron-backend-e2e/src/electron-test-fixtures.ts @@ -195,6 +195,13 @@ async function waitForAppReady(page: Page): Promise { export async function openAddPlaylistDialog(page: Page): Promise { await page.getByRole('button', { name: 'Add playlist' }).click(); + await expect(page.locator('mat-dialog-container').last()).toBeVisible(); +} + +async function getActiveDialog(page: Page): Promise { + const dialog = page.locator('mat-dialog-container').last(); + await expect(dialog).toBeVisible(); + return dialog; } export async function stubNativePlaylistFileDialog( @@ -214,15 +221,22 @@ export async function importM3uPlaylistFromNativeDialog( app: LaunchedElectronApp, filePath: string ): Promise { - await stubNativePlaylistFileDialog(app.electronApp, filePath); await openAddPlaylistDialog(app.mainWindow); - await app.mainWindow - .locator('mat-dialog-container mat-button-toggle[value="file"]') - .click(); - await app.mainWindow.locator('mat-dialog-container .file-upload').click(); - await app.mainWindow.waitForSelector('mat-dialog-container', { - state: 'detached', - }); + const dialog = await getActiveDialog(app.mainWindow); + await clickDialogCategoryOption(dialog, /^m3u$/i); + await clickDialogSubtypeOption( + dialog, + /add\s+via\s+file\s+upload/i, + 'mat-button-toggle[value="file"]' + ); + await dialog + .locator('input[type="file"][name="playlist"]') + .setInputFiles(filePath); + await expect( + dialog.getByRole('button', { name: /add playlist/i }) + ).toBeEnabled({ timeout: 10000 }); + await dialog.getByRole('button', { name: /add playlist/i }).click(); + await dialog.waitFor({ state: 'detached' }); } export async function addXtreamPortal( @@ -242,18 +256,24 @@ export async function addXtreamPortal( } = options; await openAddPlaylistDialog(page); - const dialog = page.locator('mat-dialog-container'); - await dialog.locator('mat-button-toggle[value="xtream"]').click(); + const dialog = await getActiveDialog(page); + await clickDialogCategoryOption( + dialog, + /^xtream$/i, + 'mat-button-toggle[value="xtream"]' + ); await setInputValue(dialog.locator('#title'), name); await setInputValue(dialog.locator('#serverUrl'), serverUrl); await setInputValue(dialog.locator('#username'), username); await setInputValue(dialog.locator('#password'), password); - const addButton = dialog.getByRole('button', { name: 'Add', exact: true }); + const addButton = dialog + .getByRole('button', { name: /^(add|add playlist)$/i }) + .last(); await expect(addButton).toBeEnabled({ timeout: 10000 }); await addButton.click(); - await page.waitForSelector('mat-dialog-container', { state: 'detached' }); + await dialog.waitFor({ state: 'detached' }); } async function setInputValue(input: Locator, value: string): Promise { @@ -284,6 +304,80 @@ async function setInputValue(input: Locator, value: string): Promise { await expect(input).toHaveValue(value); } +async function clickDialogCategoryOption( + dialog: Locator, + label: RegExp, + legacySelector?: string +): Promise { + await clickDialogSegmentedOption( + dialog, + 'Playlist category', + label, + legacySelector + ); +} + +async function clickDialogSubtypeOption( + dialog: Locator, + label: RegExp, + legacySelector?: string +): Promise { + await clickDialogSegmentedOption( + dialog, + 'M3U source', + label, + legacySelector + ); +} + +async function clickDialogSegmentedOption( + dialog: Locator, + tablistLabel: string, + label: RegExp, + legacySelector?: string +): Promise { + const tablist = dialog + .locator(`[role="tablist"][aria-label="${tablistLabel}"]`) + .first(); + + if ((await tablist.count()) > 0) { + const optionByTabRole = tablist + .getByRole('tab', { name: label }) + .first(); + + if ((await optionByTabRole.count()) > 0) { + await optionByTabRole.click(); + return; + } + } + + const optionByGlobalTabRole = dialog + .getByRole('tab', { name: label }) + .first(); + + if ((await optionByGlobalTabRole.count()) > 0) { + await optionByGlobalTabRole.click(); + return; + } + + const optionByButtonRole = dialog + .getByRole('button', { name: label }) + .first(); + + if ((await optionByButtonRole.count()) > 0) { + await optionByButtonRole.click(); + return; + } + + if (!legacySelector) { + throw new Error( + `Could not find dialog option matching ${label} in "${tablistLabel}".` + ); + } + + await dialog.locator(legacySelector).click(); +} + export async function addStalkerPortal( page: Page, options: { @@ -299,17 +393,23 @@ export async function addStalkerPortal( } = options; await openAddPlaylistDialog(page); - const dialog = page.locator('mat-dialog-container'); - await dialog.locator('mat-button-toggle[value="stalker"]').click(); + const dialog = await getActiveDialog(page); + await clickDialogCategoryOption( + dialog, + /^stalker$/i, + 'mat-button-toggle[value="stalker"]' + ); await setInputValue(dialog.locator('input#title'), name); await setInputValue(dialog.locator('input#portalUrl'), portalUrl); await setInputValue(dialog.locator('input#macAddress'), macAddress); - const addButton = dialog.getByRole('button', { name: 'Add', exact: true }); + const addButton = dialog + .getByRole('button', { name: /^(add|add playlist)$/i }) + .last(); await expect(addButton).toBeEnabled({ timeout: 10000 }); await addButton.click(); - await page.waitForSelector('mat-dialog-container', { state: 'detached' }); + await dialog.waitFor({ state: 'detached' }); } export async function openSettings(page: Page): Promise { @@ -367,15 +467,20 @@ export async function importM3uPlaylistFromUrl( playlistUrl: string ): Promise { await openAddPlaylistDialog(page); - const dialog = page.locator('mat-dialog-container'); - await dialog.locator('mat-button-toggle[value="url"]').click(); + const dialog = await getActiveDialog(page); + await clickDialogCategoryOption(dialog, /^m3u$/i); + await clickDialogSubtypeOption( + dialog, + /add\s+via\s+url/i, + 'mat-button-toggle[value="url"]' + ); await setInputValue( dialog.locator('input[formcontrolname="playlistUrl"]'), playlistUrl ); await dialog.getByRole('button', { name: /Add playlist/i }).click(); - await page.waitForSelector('mat-dialog-container', { state: 'detached' }); + await dialog.waitFor({ state: 'detached' }); } export function buildM3uContent(channels: M3uTestChannel[]): string { @@ -437,8 +542,7 @@ export function parseM3uFixture(filePath: string): M3uTestChannel[] { logo: line.match(/tvg-logo="([^"]*)"/)?.[1]?.trim() ?? '', name: line.split(',').at(-1)?.trim() ?? '', tvgId: line.match(/tvg-id="([^"]*)"/)?.[1]?.trim() ?? '', - tvgName: - line.match(/tvg-name="([^"]*)"/)?.[1]?.trim() ?? '', + tvgName: line.match(/tvg-name="([^"]*)"/)?.[1]?.trim() ?? '', }; continue; } @@ -520,9 +624,7 @@ export async function openPlaylistFavorites(page: Page): Promise { export async function openPlaylistRecent(page: Page): Promise { await openWorkspaceSection(page, 'Recently viewed'); - await expect - .poll(() => new URL(page.url()).pathname) - .toMatch(/\/recent$/); + await expect.poll(() => new URL(page.url()).pathname).toMatch(/\/recent$/); } export async function openGlobalFavorites(page: Page): Promise { @@ -539,9 +641,7 @@ export async function openGlobalRecent(page: Page): Promise { const dialog = await openCommandPalette(page); await dialog.locator('input[type="search"]').fill('recent'); - await dialog - .getByRole('button', { name: /Open recently viewed/i }) - .click(); + await dialog.getByRole('button', { name: /Open recently viewed/i }).click(); await page.waitForURL(/\/workspace\/global-recent(?:\?.*)?$/); } @@ -622,6 +722,19 @@ export async function expectVisibleContentCardTitle( .toBe(true); } +export async function expectPathname( + page: Page, + pattern: RegExp +): Promise { + await expect.poll(() => new URL(page.url()).pathname).toMatch(pattern); +} + +export function playlistSwitcherTitle(page: Page): Locator { + return page + .locator('.playlist-switcher-trigger .playlist-info .name') + .first(); +} + export function gridListCardByTitle(page: Page, title: string): Locator { return page.locator('.category-content-layout mat-card').filter({ has: page.locator('.title', { @@ -635,7 +748,9 @@ export function gridListCardByTitle(page: Page, title: string): Locator { * its display title. Use this instead of picking from fixture order, since the * grid sorts by date-desc and may paginate items off the first page. */ -export async function waitForFirstGridListCardTitle(page: Page): Promise { +export async function waitForFirstGridListCardTitle( + page: Page +): Promise { const card = page.locator('.category-content-layout mat-card').first(); await expect(card).toBeVisible({ timeout: 20000 }); return ((await card.locator('.title').textContent()) ?? '').trim(); @@ -787,7 +902,9 @@ export async function dragSourceBefore( targetTitle: string ): Promise { const source = sourceRowByTitle(page, sourceTitle).locator('.drag-icon'); - const target = sourceRowByTitle(page, targetTitle).locator('.playlist-item'); + const target = sourceRowByTitle(page, targetTitle).locator( + '.playlist-item' + ); await expect(source.first()).toBeVisible(); await expect(target.first()).toBeVisible(); @@ -930,10 +1047,7 @@ export async function expectSourceDialogValues( } } -export async function deleteSource( - page: Page, - title: string -): Promise { +export async function deleteSource(page: Page, title: string): Promise { const row = sourceRowByTitle(page, title).first(); await expect(row).toBeVisible(); @@ -1011,9 +1125,12 @@ export async function waitForXtreamWorkspaceReady(page: Page): Promise { export async function expectPlaylistUpdatedToast(page: Page): Promise { await expect( - page.locator('.mat-mdc-snack-bar-label').filter({ - hasText: 'Success! The playlist was successfully updated.', - }).last() + page + .locator('.mat-mdc-snack-bar-label') + .filter({ + hasText: 'Success! The playlist was successfully updated.', + }) + .last() ).toBeVisible({ timeout: 20000, }); @@ -1364,7 +1481,9 @@ async function confirmDialog(page: Page, buttonLabel = 'Yes'): Promise { const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('button', { name: buttonLabel, exact: true }).click(); + await dialog + .getByRole('button', { name: buttonLabel, exact: true }) + .click(); await page.waitForSelector('mat-dialog-container', { state: 'detached' }); } diff --git a/apps/electron-backend-e2e/src/favorites.e2e.ts b/apps/electron-backend-e2e/src/favorites.e2e.ts index ff46bb0d8..a03457a4f 100644 --- a/apps/electron-backend-e2e/src/favorites.e2e.ts +++ b/apps/electron-backend-e2e/src/favorites.e2e.ts @@ -11,6 +11,7 @@ import { contentCardByTitle, defaultXtreamPassword, defaultXtreamUsername, + expectPathname, expectVisibleContentCardTitle, expect, importM3uPlaylistFromNativeDialog, @@ -24,6 +25,7 @@ import { switchUnifiedCollectionContent, switchUnifiedCollectionScope, test, + playlistSwitcherTitle, waitForM3uCatalog, waitForStalkerCatalog, waitForXtreamWorkspaceReady, @@ -66,20 +68,20 @@ test.describe('Electron Favorites', () => { await toggleFavoriteForChannel(app.mainWindow, 'M3U Favorite Two'); await openPlaylistFavorites(app.mainWindow); - await expect(channelItemByTitle(app.mainWindow, 'M3U Favorite One')).toHaveCount( - 1 - ); - await expect(channelItemByTitle(app.mainWindow, 'M3U Favorite Two')).toHaveCount( - 1 - ); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Favorite One') + ).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Favorite Two') + ).toHaveCount(1); await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); - await expect(channelItemByTitle(app.mainWindow, 'M3U Favorite One')).toHaveCount( - 1 - ); - await expect(channelItemByTitle(app.mainWindow, 'M3U Favorite Two')).toHaveCount( - 1 - ); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Favorite One') + ).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, 'M3U Favorite Two') + ).toHaveCount(1); const restarted = await restartElectronApp(app, dataDir); app.electronApp = restarted.electronApp; @@ -91,10 +93,7 @@ test.describe('Electron Favorites', () => { .click(); await waitForM3uCatalog(app.mainWindow); await openPlaylistFavorites(app.mainWindow); - await switchUnifiedCollectionScope( - app.mainWindow, - 'All playlists' - ); + await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await expect( channelItemByTitle(app.mainWindow, 'M3U Favorite One') ).toHaveCount(1); @@ -111,13 +110,22 @@ test.describe('Electron Favorites', () => { request, }) => { await resetMockServers(request, ['xtream']); - const liveFixture = await fetchXtreamLiveFixture(request, xtreamCredentials); - const vodFixture = await fetchXtreamVodFixture(request, xtreamCredentials); + const liveFixture = await fetchXtreamLiveFixture( + request, + xtreamCredentials + ); + const vodFixture = await fetchXtreamVodFixture( + request, + xtreamCredentials + ); const seriesFixture = await fetchXtreamSeriesFixture( request, xtreamCredentials ); - const [liveTitle] = pickDistinctTitles(liveFixture.items, getXtreamTitle); + const [liveTitle] = pickDistinctTitles( + liveFixture.items, + getXtreamTitle + ); const portalTitle = 'Xtream Favorites Source'; const app = await launchElectronApp(dataDir); @@ -134,7 +142,9 @@ test.describe('Electron Favorites', () => { ); await toggleFavoriteForChannel(app.mainWindow, liveTitle); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, vodFixture.categoryName @@ -147,7 +157,9 @@ test.describe('Electron Favorites', () => { await addCurrentDetailToFavorites(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, seriesFixture.categoryName @@ -157,7 +169,9 @@ test.describe('Electron Favorites', () => { await goBackFromDetail(app.mainWindow); await openPlaylistFavorites(app.mainWindow); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); @@ -167,7 +181,9 @@ test.describe('Electron Favorites', () => { await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); @@ -181,10 +197,7 @@ test.describe('Electron Favorites', () => { await sourceRowByTitle(app.mainWindow, portalTitle).first().click(); await waitForXtreamWorkspaceReady(app.mainWindow); await openPlaylistFavorites(app.mainWindow); - await switchUnifiedCollectionScope( - app.mainWindow, - 'All playlists' - ); + await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); await expect( channelItemByTitle(app.mainWindow, liveTitle) @@ -198,6 +211,108 @@ test.describe('Electron Favorites', () => { } }); + test('opens Xtream favorite movies and series inline from favorites without switching the playlist or showing the sidebar', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const vodFixture = await fetchXtreamVodFixture( + request, + xtreamCredentials + ); + const seriesFixture = await fetchXtreamSeriesFixture( + request, + xtreamCredentials + ); + const hostPortalTitle = 'Xtream Favorite Detail Host'; + const sourcePortalTitle = 'Xtream Favorite Detail Source'; + const app = await launchElectronApp(dataDir); + + try { + await addXtreamPortal(app.mainWindow, { + name: hostPortalTitle, + }); + await waitForXtreamWorkspaceReady(app.mainWindow); + + await addXtreamPortal(app.mainWindow, { + name: sourcePortalTitle, + }); + await waitForXtreamWorkspaceReady(app.mainWindow); + + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); + await clickCategoryByNameExact( + app.mainWindow, + vodFixture.categoryName + ); + const movieTitle = await clickFirstGridListCard(app.mainWindow); + await addCurrentDetailToFavorites(app.mainWindow); + await goBackFromDetail(app.mainWindow); + + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); + await clickCategoryByNameExact( + app.mainWindow, + seriesFixture.categoryName + ); + const seriesTitle = await clickFirstGridListCard(app.mainWindow); + await addCurrentDetailToFavorites(app.mainWindow); + await goBackFromDetail(app.mainWindow); + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, hostPortalTitle) + .first() + .click(); + await waitForXtreamWorkspaceReady(app.mainWindow); + await openPlaylistFavorites(app.mainWindow); + await expect(playlistSwitcherTitle(app.mainWindow)).toContainText( + hostPortalTitle + ); + + await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); + await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); + await expectVisibleContentCardTitle(app.mainWindow, movieTitle); + await contentCardByTitle(app.mainWindow, movieTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/xtreams\/[^/]+\/favorites$/, + title: movieTitle, + playlistTitle: hostPortalTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/favorites$/ + ); + await expectVisibleContentCardTitle(app.mainWindow, movieTitle); + + await switchUnifiedCollectionContent(app.mainWindow, 'Series'); + await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); + await contentCardByTitle(app.mainWindow, seriesTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/xtreams\/[^/]+\/favorites$/, + title: seriesTitle, + playlistTitle: hostPortalTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/favorites$/ + ); + await switchUnifiedCollectionContent(app.mainWindow, 'Series'); + await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); + } finally { + await closeElectronApp(app); + } + }); + test('shows Stalker live, movie, and series favorites in playlist and all-playlists scope, and preserves them after restart', async ({ dataDir, request, @@ -205,10 +320,22 @@ test.describe('Electron Favorites', () => { await resetMockServers(request, ['stalker']); const liveFixture = await fetchStalkerCategoryFixture(request, 'itv'); const vodFixture = await fetchStalkerCategoryFixture(request, 'vod'); - const seriesFixture = await fetchStalkerCategoryFixture(request, 'series'); - const [liveTitle] = pickDistinctTitles(liveFixture.items, getStalkerTitle); - const [movieTitle] = pickDistinctTitles(vodFixture.items, getStalkerTitle); - const [seriesTitle] = pickDistinctTitles(seriesFixture.items, getStalkerTitle); + const seriesFixture = await fetchStalkerCategoryFixture( + request, + 'series' + ); + const [liveTitle] = pickDistinctTitles( + liveFixture.items, + getStalkerTitle + ); + const [movieTitle] = pickDistinctTitles( + vodFixture.items, + getStalkerTitle + ); + const [seriesTitle] = pickDistinctTitles( + seriesFixture.items, + getStalkerTitle + ); const portalTitle = 'Stalker Favorites Source'; const app = await launchElectronApp(dataDir); @@ -218,38 +345,56 @@ test.describe('Electron Favorites', () => { }); await waitForStalkerCatalog(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Live TV', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Live TV', exact: true }) + .click(); await clickCategoryById(app.mainWindow, liveFixture.categoryId); await toggleFavoriteForChannel(app.mainWindow, liveTitle); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryById(app.mainWindow, vodFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, movieTitle); await addCurrentDetailToFavorites(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryById(app.mainWindow, seriesFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, seriesTitle); await addCurrentDetailToFavorites(app.mainWindow); await goBackFromDetail(app.mainWindow); await openPlaylistFavorites(app.mainWindow); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(1); await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(1); const restarted = await restartElectronApp(app, dataDir); app.electronApp = restarted.electronApp; @@ -259,10 +404,7 @@ test.describe('Electron Favorites', () => { await sourceRowByTitle(app.mainWindow, portalTitle).first().click(); await waitForStalkerCatalog(app.mainWindow); await openPlaylistFavorites(app.mainWindow); - await switchUnifiedCollectionScope( - app.mainWindow, - 'All playlists' - ); + await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); await expect( channelItemByTitle(app.mainWindow, liveTitle) @@ -280,63 +422,88 @@ test.describe('Electron Favorites', () => { } }); - test('opens Stalker favorite movies and series in source detail routes and returns back to favorites', async ({ + test('opens Stalker favorite movies and series inline from favorites without showing the sidebar', async ({ dataDir, request, }) => { await resetMockServers(request, ['stalker']); const vodFixture = await fetchStalkerCategoryFixture(request, 'vod'); - const seriesFixture = await fetchStalkerCategoryFixture(request, 'series'); - const [movieTitle] = pickDistinctTitles(vodFixture.items, getStalkerTitle); - const [seriesTitle] = pickDistinctTitles(seriesFixture.items, getStalkerTitle); + const seriesFixture = await fetchStalkerCategoryFixture( + request, + 'series' + ); + const [movieTitle] = pickDistinctTitles( + vodFixture.items, + getStalkerTitle + ); + const [seriesTitle] = pickDistinctTitles( + seriesFixture.items, + getStalkerTitle + ); + const portalTitle = 'Stalker Favorite Detail Source'; const app = await launchElectronApp(dataDir); try { await addStalkerPortal(app.mainWindow, { - name: 'Stalker Favorite Detail Source', + name: portalTitle, }); await waitForStalkerCatalog(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryById(app.mainWindow, vodFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, movieTitle); await addCurrentDetailToFavorites(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryById(app.mainWindow, seriesFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, seriesTitle); await addCurrentDetailToFavorites(app.mainWindow); await goBackFromDetail(app.mainWindow); await openPlaylistFavorites(app.mainWindow); + await expect(playlistSwitcherTitle(app.mainWindow)).toContainText( + portalTitle + ); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); - await contentCardByTitle(app.mainWindow, movieTitle).first().click(); - await app.mainWindow.waitForURL(/\/workspace\/stalker\/[^/]+\/vod\/[^/]+$/); - await expect(app.mainWindow.locator('app-content-hero')).toContainText( - movieTitle - ); + await contentCardByTitle(app.mainWindow, movieTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/stalker\/[^/]+\/favorites$/, + title: movieTitle, + playlistTitle: portalTitle, + }); await goBackFromDetail(app.mainWindow); - await expect - .poll(() => new URL(app.mainWindow.url()).pathname) - .toMatch(/\/favorites$/); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/favorites$/ + ); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); - await contentCardByTitle(app.mainWindow, seriesTitle).first().click(); - await app.mainWindow.waitForURL(/\/workspace\/stalker\/[^/]+\/series\/[^/]+$/); - await expect(app.mainWindow.locator('app-content-hero')).toContainText( - seriesTitle - ); + await contentCardByTitle(app.mainWindow, seriesTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/stalker\/[^/]+\/favorites$/, + title: seriesTitle, + playlistTitle: portalTitle, + }); await goBackFromDetail(app.mainWindow); - await expect - .poll(() => new URL(app.mainWindow.url()).pathname) - .toMatch(/\/favorites$/); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/favorites$/ + ); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); } finally { @@ -355,23 +522,57 @@ async function addCurrentDetailToFavorites(page: Page): Promise { await expect(addButton).toBeVisible({ timeout: 20000 }); await addButton.click(); - await expect(page.locator('button.favorite-btn--active').first()).toBeVisible({ + await expect( + page.locator('button.favorite-btn--active').first() + ).toBeVisible({ timeout: 20000, }); } async function goBackFromDetail(page: Page): Promise { - const backButton = page.locator('app-content-hero .hero__back-button').first(); + const backButton = page + .locator('app-content-hero .hero__back-button') + .first(); await expect(backButton).toBeVisible({ timeout: 20000 }); - await backButton.click(); + try { + await backButton.click({ timeout: 5000 }); + } catch { + await backButton.evaluate((button: HTMLButtonElement) => + button.click() + ); + } } -async function toggleFavoriteForChannel(page: Page, title: string): Promise { +async function expectInlineCollectionDetail( + page: Page, + params: { + pathname: RegExp; + title: string; + playlistTitle: string; + } +): Promise { + await expectPathname(page, params.pathname); + await expect(playlistSwitcherTitle(page)).toContainText( + params.playlistTitle + ); + await expect(page.locator('app-workspace-context-panel')).toHaveCount(0); + await expect(page.locator('app-content-hero')).toContainText(params.title); + await expect( + page.locator('app-content-hero .hero__back-button').first() + ).toBeVisible({ timeout: 20000 }); +} + +async function toggleFavoriteForChannel( + page: Page, + title: string +): Promise { const item = channelItemByTitle(page, title).first(); await expect(item).toBeVisible({ timeout: 20000 }); await item.hover(); await item.locator('.favorite-button').first().click(); - await expect(item.locator('.favorite-button mat-icon').first()).toHaveText(/star/); + await expect(item.locator('.favorite-button mat-icon').first()).toHaveText( + /star/ + ); } diff --git a/apps/electron-backend-e2e/src/recent.e2e.ts b/apps/electron-backend-e2e/src/recent.e2e.ts index 8b40a5fa0..3cc9efcac 100644 --- a/apps/electron-backend-e2e/src/recent.e2e.ts +++ b/apps/electron-backend-e2e/src/recent.e2e.ts @@ -11,6 +11,7 @@ import { contentCardByTitle, defaultXtreamPassword, defaultXtreamUsername, + expectPathname, expectVisibleContentCardTitle, expect, importM3uPlaylistFromNativeDialog, @@ -25,6 +26,7 @@ import { switchUnifiedCollectionContent, switchUnifiedCollectionScope, test, + playlistSwitcherTitle, waitForM3uCatalog, waitForStalkerCatalog, waitForXtreamWorkspaceReady, @@ -44,13 +46,17 @@ test.describe('Electron Recently Viewed', () => { test('keeps unified live detail open when re-clicking the active M3U recent item', async ({ dataDir, }) => { - const filePath = writeTemporaryM3uFile(dataDir, 'm3u-live-reclick.m3u', [ - { - groupTitle: 'Live', - name: 'Stable Recent Channel', - url: 'https://streams.example.test/stable-recent.m3u8', - }, - ]); + const filePath = writeTemporaryM3uFile( + dataDir, + 'm3u-live-reclick.m3u', + [ + { + groupTitle: 'Live', + name: 'Stable Recent Channel', + url: 'https://streams.example.test/stable-recent.m3u8', + }, + ] + ); const app = await launchElectronApp(dataDir); try { @@ -150,13 +156,22 @@ test.describe('Electron Recently Viewed', () => { request, }) => { await resetMockServers(request, ['xtream']); - const liveFixture = await fetchXtreamLiveFixture(request, xtreamCredentials); - const vodFixture = await fetchXtreamVodFixture(request, xtreamCredentials); + const liveFixture = await fetchXtreamLiveFixture( + request, + xtreamCredentials + ); + const vodFixture = await fetchXtreamVodFixture( + request, + xtreamCredentials + ); const seriesFixture = await fetchXtreamSeriesFixture( request, xtreamCredentials ); - const [liveTitle] = pickDistinctTitles(liveFixture.items, getXtreamTitle); + const [liveTitle] = pickDistinctTitles( + liveFixture.items, + getXtreamTitle + ); const portalTitle = 'Xtream Recent Source'; const app = await launchElectronApp(dataDir); @@ -176,7 +191,9 @@ test.describe('Electron Recently Viewed', () => { await channelItemByTitle(app.mainWindow, liveTitle).first().click(); await closeUnifiedLiveDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, vodFixture.categoryName @@ -190,7 +207,9 @@ test.describe('Electron Recently Viewed', () => { await playCurrentDetail(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, seriesFixture.categoryName @@ -199,7 +218,9 @@ test.describe('Electron Recently Viewed', () => { await playFirstSeriesEpisode(app.mainWindow); await openPlaylistRecent(app.mainWindow); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); @@ -207,7 +228,9 @@ test.describe('Electron Recently Viewed', () => { await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); @@ -223,7 +246,9 @@ test.describe('Electron Recently Viewed', () => { await openPlaylistRecent(app.mainWindow); await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); @@ -233,20 +258,29 @@ test.describe('Electron Recently Viewed', () => { await clearRecentItems(app.mainWindow); // After clearing all items, the content toggle disappears. // All content types are cleared simultaneously, so no toggle switch needed. - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(0); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(0); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(0); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(0); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(0); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(0); } finally { await closeElectronApp(app); } }); - test('opens Xtream recent movies and series in source detail routes and returns back to recent', async ({ + test('opens Xtream recent movies and series inline from recent without switching the playlist or showing the sidebar', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); - const vodFixture = await fetchXtreamVodFixture(request, xtreamCredentials); + const vodFixture = await fetchXtreamVodFixture( + request, + xtreamCredentials + ); const seriesFixture = await fetchXtreamSeriesFixture( request, xtreamCredentials @@ -260,7 +294,9 @@ test.describe('Electron Recently Viewed', () => { }); await waitForXtreamWorkspaceReady(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, vodFixture.categoryName @@ -269,7 +305,9 @@ test.describe('Electron Recently Viewed', () => { await playCurrentDetail(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryByNameExact( app.mainWindow, seriesFixture.categoryName @@ -278,33 +316,44 @@ test.describe('Electron Recently Viewed', () => { await playFirstSeriesEpisode(app.mainWindow); await openPlaylistRecent(app.mainWindow); + await expect(playlistSwitcherTitle(app.mainWindow)).toContainText( + portalTitle + ); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); - await contentCardByTitle(app.mainWindow, movieTitle).first().click(); - await app.mainWindow.waitForURL(/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+$/); - await expect(app.mainWindow.locator('app-content-hero')).toContainText( - movieTitle - ); + await contentCardByTitle(app.mainWindow, movieTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/xtreams\/[^/]+\/recent$/, + title: movieTitle, + playlistTitle: portalTitle, + }); await goBackFromDetail(app.mainWindow); - await expect - .poll(() => new URL(app.mainWindow.url()).pathname) - .toMatch(/\/recent$/); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/recent$/ + ); await expectVisibleContentCardTitle(app.mainWindow, movieTitle); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); - await contentCardByTitle(app.mainWindow, seriesTitle).first().click(); - await app.mainWindow.waitForURL(/\/workspace\/xtreams\/[^/]+\/series\/[^/]+\/[^/]+$/); - await expect(app.mainWindow.locator('app-content-hero')).toContainText( - seriesTitle - ); + await contentCardByTitle(app.mainWindow, seriesTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/xtreams\/[^/]+\/recent$/, + title: seriesTitle, + playlistTitle: portalTitle, + }); await goBackFromDetail(app.mainWindow); - await expect - .poll(() => new URL(app.mainWindow.url()).pathname) - .toMatch(/\/recent$/); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/recent$/ + ); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); } finally { @@ -319,10 +368,22 @@ test.describe('Electron Recently Viewed', () => { await resetMockServers(request, ['stalker']); const liveFixture = await fetchStalkerCategoryFixture(request, 'itv'); const vodFixture = await fetchStalkerCategoryFixture(request, 'vod'); - const seriesFixture = await fetchStalkerCategoryFixture(request, 'series'); - const [liveTitle] = pickDistinctTitles(liveFixture.items, getStalkerTitle); - const [movieTitle] = pickDistinctTitles(vodFixture.items, getStalkerTitle); - const [seriesTitle] = pickDistinctTitles(seriesFixture.items, getStalkerTitle); + const seriesFixture = await fetchStalkerCategoryFixture( + request, + 'series' + ); + const [liveTitle] = pickDistinctTitles( + liveFixture.items, + getStalkerTitle + ); + const [movieTitle] = pickDistinctTitles( + vodFixture.items, + getStalkerTitle + ); + const [seriesTitle] = pickDistinctTitles( + seriesFixture.items, + getStalkerTitle + ); const portalTitle = 'Stalker Recent Source'; const app = await launchElectronApp(dataDir); @@ -332,38 +393,56 @@ test.describe('Electron Recently Viewed', () => { }); await waitForStalkerCatalog(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Live TV', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Live TV', exact: true }) + .click(); await clickCategoryById(app.mainWindow, liveFixture.categoryId); await toggleFavoriteForChannel(app.mainWindow, liveTitle); await openPlaylistFavorites(app.mainWindow); await channelItemByTitle(app.mainWindow, liveTitle).first().click(); await closeUnifiedLiveDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Movies', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); await clickCategoryById(app.mainWindow, vodFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, movieTitle); await playCurrentDetail(app.mainWindow); await goBackFromDetail(app.mainWindow); - await app.mainWindow.getByRole('link', { name: 'Series', exact: true }).click(); + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); await clickCategoryById(app.mainWindow, seriesFixture.categoryId); await clickGridListCardByTitle(app.mainWindow, seriesTitle); await playFirstSeriesEpisode(app.mainWindow); await openPlaylistRecent(app.mainWindow); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(1); await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(1); const restarted = await restartElectronApp(app, dataDir); app.electronApp = restarted.electronApp; @@ -375,11 +454,105 @@ test.describe('Electron Recently Viewed', () => { await openPlaylistRecent(app.mainWindow); await switchUnifiedCollectionScope(app.mainWindow, 'All playlists'); await switchUnifiedCollectionContent(app.mainWindow, 'Live TV'); - await expect(channelItemByTitle(app.mainWindow, liveTitle)).toHaveCount(1); + await expect( + channelItemByTitle(app.mainWindow, liveTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); - await expect(contentCardByTitle(app.mainWindow, movieTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, movieTitle) + ).toHaveCount(1); await switchUnifiedCollectionContent(app.mainWindow, 'Series'); - await expect(contentCardByTitle(app.mainWindow, seriesTitle)).toHaveCount(1); + await expect( + contentCardByTitle(app.mainWindow, seriesTitle) + ).toHaveCount(1); + } finally { + await closeElectronApp(app); + } + }); + + test('opens Stalker recent movies and series inline from recent without showing the sidebar', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['stalker']); + const vodFixture = await fetchStalkerCategoryFixture(request, 'vod'); + const seriesFixture = await fetchStalkerCategoryFixture( + request, + 'series' + ); + const [movieTitle] = pickDistinctTitles( + vodFixture.items, + getStalkerTitle + ); + const [seriesTitle] = pickDistinctTitles( + seriesFixture.items, + getStalkerTitle + ); + const portalTitle = 'Stalker Recent Detail Source'; + const app = await launchElectronApp(dataDir); + + try { + await addStalkerPortal(app.mainWindow, { + name: portalTitle, + }); + await waitForStalkerCatalog(app.mainWindow); + + await app.mainWindow + .getByRole('link', { name: 'Movies', exact: true }) + .click(); + await clickCategoryById(app.mainWindow, vodFixture.categoryId); + await clickGridListCardByTitle(app.mainWindow, movieTitle); + await playCurrentDetail(app.mainWindow); + await goBackFromDetail(app.mainWindow); + + await app.mainWindow + .getByRole('link', { name: 'Series', exact: true }) + .click(); + await clickCategoryById(app.mainWindow, seriesFixture.categoryId); + await clickGridListCardByTitle(app.mainWindow, seriesTitle); + await playFirstSeriesEpisode(app.mainWindow); + + await openPlaylistRecent(app.mainWindow); + await expect(playlistSwitcherTitle(app.mainWindow)).toContainText( + portalTitle + ); + + await switchUnifiedCollectionContent(app.mainWindow, 'Movies'); + await expectVisibleContentCardTitle(app.mainWindow, movieTitle); + await contentCardByTitle(app.mainWindow, movieTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/stalker\/[^/]+\/recent$/, + title: movieTitle, + playlistTitle: portalTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/recent$/ + ); + await expectVisibleContentCardTitle(app.mainWindow, movieTitle); + + await switchUnifiedCollectionContent(app.mainWindow, 'Series'); + await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); + await contentCardByTitle(app.mainWindow, seriesTitle) + .first() + .click(); + await expectInlineCollectionDetail(app.mainWindow, { + pathname: /\/workspace\/stalker\/[^/]+\/recent$/, + title: seriesTitle, + playlistTitle: portalTitle, + }); + + await goBackFromDetail(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/recent$/ + ); + await switchUnifiedCollectionContent(app.mainWindow, 'Series'); + await expectVisibleContentCardTitle(app.mainWindow, seriesTitle); } finally { await closeElectronApp(app); } @@ -405,10 +578,37 @@ async function closeUnifiedLiveDetail(page: Page): Promise { } async function goBackFromDetail(page: Page): Promise { - const backButton = page.locator('app-content-hero .hero__back-button').first(); + const backButton = page + .locator('app-content-hero .hero__back-button') + .first(); await expect(backButton).toBeVisible({ timeout: 20000 }); - await backButton.click(); + try { + await backButton.click({ timeout: 5000 }); + } catch { + await backButton.evaluate((button: HTMLButtonElement) => + button.click() + ); + } +} + +async function expectInlineCollectionDetail( + page: Page, + params: { + pathname: RegExp; + title: string; + playlistTitle: string; + } +): Promise { + await expectPathname(page, params.pathname); + await expect(playlistSwitcherTitle(page)).toContainText( + params.playlistTitle + ); + await expect(page.locator('app-workspace-context-panel')).toHaveCount(0); + await expect(page.locator('app-content-hero')).toContainText(params.title); + await expect( + page.locator('app-content-hero .hero__back-button').first() + ).toBeVisible({ timeout: 20000 }); } async function playCurrentDetail(page: Page): Promise { @@ -420,7 +620,9 @@ async function playCurrentDetail(page: Page): Promise { async function playFirstSeriesEpisode(page: Page): Promise { const seasonCard = page.locator('.season-card').first(); - const episodeCard = page.locator('.episode-card, .episode-list-item').first(); + const episodeCard = page + .locator('.episode-card, .episode-list-item') + .first(); await expect .poll( @@ -449,7 +651,10 @@ async function removeLiveRecentItem(page: Page, title: string): Promise { await item.locator('.favorite-button').first().click(); } -async function toggleFavoriteForChannel(page: Page, title: string): Promise { +async function toggleFavoriteForChannel( + page: Page, + title: string +): Promise { const item = channelItemByTitle(page, title).first(); await expect(item).toBeVisible({ timeout: 20000 });