diff --git a/apps/electron-backend-e2e/src/search.e2e.ts b/apps/electron-backend-e2e/src/search.e2e.ts index 291dd7175..653acae96 100644 --- a/apps/electron-backend-e2e/src/search.e2e.ts +++ b/apps/electron-backend-e2e/src/search.e2e.ts @@ -206,7 +206,10 @@ test.describe('Electron Workspace Search', () => { await fillWorkspaceSearch(app.mainWindow, sample.groupTitle); - await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/); + await expectPathname( + app.mainWindow, + /\/workspace\/global-favorites$/ + ); await expectQueryParam(app.mainWindow, 'q', sample.groupTitle); await expectWorkspaceSearchScope( app.mainWindow, @@ -411,6 +414,134 @@ test.describe('Electron Workspace Search', () => { } }); + test('@search @xtream filters VOD and series root views across categories', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const vodSample = await fetchXtreamVodFixture(request); + const seriesSample = await fetchXtreamSeriesFixture(request); + + const app = await launchElectronApp(dataDir); + + try { + await addXtreamPortal(app.mainWindow, { + password: xtreamSearchPassword, + username: xtreamSearchUsername, + }); + await waitForXtreamWorkspaceReady(app.mainWindow); + + await expectXtreamRootCatalogSearch(app.mainWindow, { + pathPattern: /\/workspace\/xtreams\/[^/]+\/vod$/, + sample: vodSample, + sectionLabel: 'Movies', + }); + await expect( + app.mainWindow.locator(headerSearchSelector) + ).toHaveValue(vodSample.targetTitle); + + await fillWorkspaceSearch(app.mainWindow, ''); + await openWorkspaceSection(app.mainWindow, 'Series'); + + await expectXtreamRootCatalogSearch(app.mainWindow, { + pathPattern: /\/workspace\/xtreams\/[^/]+\/series$/, + sample: seriesSample, + sectionLabel: 'Series', + }); + await expect( + app.mainWindow.locator(headerSearchSelector) + ).toHaveValue(seriesSample.targetTitle); + } finally { + await closeElectronApp(app); + } + }); + + test('@search @xtream filters live root and category scopes while preserving playback sidebar', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const sample = await fetchXtreamLiveFixture(request); + + const app = await launchElectronApp(dataDir); + + try { + await addXtreamPortal(app.mainWindow, { + password: xtreamSearchPassword, + username: xtreamSearchUsername, + }); + await waitForXtreamWorkspaceReady(app.mainWindow); + await openWorkspaceSection(app.mainWindow, 'Live TV'); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live$/ + ); + + await fillWorkspaceSearch(app.mainWindow, sample.targetTitle); + + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live$/ + ); + await expectQueryParam(app.mainWindow, 'q', sample.targetTitle); + await expectWorkspaceSearchScope( + app.mainWindow, + 'Live TV / All Items' + ); + await expect( + liveChannelSidebar(app.mainWindow) + .locator('[data-test-id="channel-item"]') + .first() + ).toBeVisible({ timeout: 20000 }); + await expect( + channelItemByTitle(app.mainWindow, sample.targetTitle).first() + ).toBeVisible({ timeout: 20000 }); + await expect( + channelItemByTitle(app.mainWindow, sample.controlTitle) + ).toHaveCount(0); + + await channelItemByTitle(app.mainWindow, sample.targetTitle) + .first() + .click(); + + await expect( + app.mainWindow.locator( + 'app-live-stream-layout .content-container .video-player' + ) + ).toBeVisible({ timeout: 20000 }); + await expect( + liveChannelSidebar(app.mainWindow) + .locator('[data-test-id="channel-item"]') + .first() + ).toBeVisible(); + await expect( + channelItemByTitle(app.mainWindow, sample.targetTitle).first() + ).toHaveClass(/(^|\s)active(\s|$)/); + + await fillWorkspaceSearch(app.mainWindow, ''); + await clickCategoryByNameExact(app.mainWindow, sample.categoryName); + await fillWorkspaceSearch(app.mainWindow, sample.targetTitle); + + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live(?:\/[^/]+)?$/ + ); + await expectQueryParam(app.mainWindow, 'q', sample.targetTitle); + await expectWorkspaceSearchScope( + app.mainWindow, + `Live TV / ${sample.categoryName}` + ); + await expect( + channelItemByTitle(app.mainWindow, sample.targetTitle).first() + ).toBeVisible({ timeout: 20000 }); + await expect( + channelItemByTitle(app.mainWindow, sample.controlTitle) + ).toHaveCount(0); + } finally { + await closeElectronApp(app); + } + }); + test('@search @xtream promotes dashboard header search into advanced playlist search', async ({ dataDir, request, @@ -561,7 +692,10 @@ test.describe('Electron Workspace Search', () => { ); await openWorkspaceSection(app.mainWindow, 'Favorites'); - await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/); + await expectPathname( + app.mainWindow, + /\/workspace\/global-favorites$/ + ); await expect( contentCardByTitle( app.mainWindow, @@ -801,7 +935,10 @@ test.describe('Electron Workspace Search', () => { await toggleFavoriteForChannel(app.mainWindow, sample.controlTitle); await openWorkspaceSection(app.mainWindow, 'Favorites'); - await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/); + await expectPathname( + app.mainWindow, + /\/workspace\/global-favorites$/ + ); await expect( channelItemByTitle(app.mainWindow, sample.targetTitle).first() ).toBeVisible({ timeout: 20000 }); @@ -911,6 +1048,15 @@ async function fetchXtreamVodFixture( }); } +async function fetchXtreamLiveFixture( + request: APIRequestContext +): Promise { + return fetchXtreamFixture(request, { + categoriesAction: 'get_live_categories', + itemsAction: 'get_live_streams', + }); +} + async function fetchXtreamSeriesFixture( request: APIRequestContext ): Promise { @@ -1262,6 +1408,10 @@ function xtreamSearchResultCards(page: Page) { return page.locator('app-search-results .results-grid .content-card'); } +function liveChannelSidebar(page: Page) { + return page.locator('app-live-stream-layout .sidebar'); +} + function gridListCardByTitle(page: Page, title: string) { return page.locator('.category-content-layout mat-card').filter({ has: page.locator('.title', { @@ -1329,6 +1479,34 @@ async function resolveVisibleXtreamTitles( }; } +async function expectXtreamRootCatalogSearch( + page: Page, + options: { + pathPattern: RegExp; + sample: XtreamVodFixture; + sectionLabel: string; + } +): Promise { + await openWorkspaceSection(page, options.sectionLabel); + await expectPathname(page, options.pathPattern); + await fillWorkspaceSearch(page, options.sample.targetTitle); + + await expectPathname(page, options.pathPattern); + await expectQueryParam(page, 'q', options.sample.targetTitle); + await expectWorkspaceSearchScope( + page, + `${options.sectionLabel} / All Items` + ); + const contentLayout = page.locator('.category-content-layout'); + await expect(contentLayout).toContainText( + flexibleTextPattern(options.sample.targetTitle), + { timeout: 20000 } + ); + await expect(contentLayout).not.toContainText( + flexibleTextPattern(options.sample.controlTitle) + ); +} + async function addCurrentDetailToFavorites(page: Page): Promise { const addButton = page .locator('button.favorite-btn') diff --git a/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.spec.ts b/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.spec.ts index d7676347c..ff5268eba 100644 --- a/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.spec.ts +++ b/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.spec.ts @@ -9,8 +9,37 @@ const TestSelectionStore = signalStore( vod: 'ready', series: 'ready', }, - liveCategories: [], - liveStreams: [], + liveCategories: [ + { + id: 50, + category_id: '50', + category_name: 'News', + type: 'live', + }, + { + id: 60, + category_id: '60', + category_name: 'Sports', + type: 'live', + }, + ], + liveStreams: [ + { + xtream_id: 201, + category_id: '50', + title: 'World News', + }, + { + xtream_id: 202, + category_id: '60', + title: 'World Sports', + }, + { + xtream_id: 203, + category_id: '60', + title: 'Match Day', + }, + ], vodCategories: [ { id: 10, @@ -175,7 +204,7 @@ describe('withSelection', () => { ).toEqual(['Stargate SG-1', 'Stargate Atlantis']); }); - it('keeps VOD search scoped to the selected category', () => { + it('keeps VOD category route search scoped to the selected category', () => { store.setSelectedContentType('vod'); store.setSelectedCategory(10); @@ -185,4 +214,37 @@ describe('withSelection', () => { store.selectItemsFromSelectedCategory().map((item) => item.title) ).toEqual(['First']); }); + + it('keeps series category route search scoped to the selected category', () => { + store.setSelectedContentType('series'); + store.setSelectedCategory(30); + + store.setCategorySearchTerm('stargate'); + + expect( + store.selectItemsFromSelectedCategory().map((item) => item.title) + ).toEqual(['Stargate SG-1']); + }); + + it('keeps live category route search scoped to the selected category', () => { + store.setSelectedContentType('live'); + store.setSelectedCategory(50); + + store.setCategorySearchTerm('world'); + + expect( + store.selectItemsFromSelectedCategory().map((item) => item.title) + ).toEqual(['World News']); + }); + + it('filters all live items across categories when no category is selected', () => { + store.setSelectedContentType('live'); + store.setSelectedCategory(null); + + store.setCategorySearchTerm('world'); + + expect( + store.selectItemsFromSelectedCategory().map((item) => item.title) + ).toEqual(['World News', 'World Sports']); + }); }); diff --git a/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.ts b/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.ts index fe2fcfb33..7157306ee 100644 --- a/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.ts +++ b/libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.ts @@ -276,19 +276,19 @@ export function withSelection() { : sortedContent(); filtered = filterBySearchTerm(filtered, searchTerm); - return categoryId + return categoryId || searchTerm ? sortByMode(filtered, sortMode, categoryType) : filtered; } if (!categoryId) { - return sortedContent(); + return filterBySearchTerm(sortedContent(), searchTerm); } const filtered = content.filter( (item) => Number(item.category_id) === categoryId ); - return filtered; + return filterBySearchTerm(filtered, searchTerm); }); return { diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html index 023f0623f..1d1d99120 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html @@ -1,7 +1,7 @@ @let isEmbeddedPlayer = usesEmbeddedPlayer(); @let streamUrl = activeStreamUrl(); -@if (selectedCategoryId()) { +@if (showLiveChannelSidebar()) {