From 10139955e7e741a6ae3c7dacf16ea595d6ed0939 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Apr 2026 00:25:56 +0200 Subject: [PATCH] fix(stalker): lazy load live tv channels Entire-Checkpoint: c6e522b4276c --- .../with-stalker-content.feature.spec.ts | 73 +++++++++++++++++++ ...alker-live-stream-layout.component.spec.ts | 32 +++++++- .../stalker-live-stream-layout.component.ts | 15 ++-- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.spec.ts index 1525032bb..035e92b97 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.spec.ts @@ -224,6 +224,79 @@ describe('withStalkerContent failure states', () => { ); }); + it('loads live TV channels page by page and appends later pages', async () => { + dataService.sendIpcEvent.mockImplementation( + (_event: unknown, payload: { params?: { p?: number } }) => { + const page = Number(payload.params?.p ?? 1); + + return Promise.resolve({ + js: { + data: [ + { + id: `channel-${page}`, + name: `Channel page ${page}`, + category_id: '5', + }, + ], + total_items: 2, + }, + }); + } + ); + + store.setSelectedContentType('itv'); + store.setCategories('itv', [ + { + category_id: '5', + category_name: 'News', + }, + ]); + store.setSelectedCategory('5'); + store.setCurrentPlaylist(PLAYLIST); + void store.isPaginatedContentLoading(); + + await waitForCondition(() => store.itvChannels().length === 1); + + expect(dataService.sendIpcEvent).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + params: expect.objectContaining({ + action: StalkerPortalActions.GetOrderedList, + type: 'itv', + category: '5', + genre: '5', + p: 1, + }), + }) + ); + expect(store.itvChannels().map((channel) => channel.name)).toEqual([ + 'Channel page 1', + ]); + expect(store.hasMoreChannels()).toBe(true); + + store.setPage(1); + + await waitForCondition(() => store.itvChannels().length === 2); + + expect(dataService.sendIpcEvent).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + params: expect.objectContaining({ + action: StalkerPortalActions.GetOrderedList, + type: 'itv', + category: '5', + genre: '5', + p: 2, + }), + }) + ); + expect(store.itvChannels().map((channel) => channel.name)).toEqual([ + 'Channel page 1', + 'Channel page 2', + ]); + expect(store.hasMoreChannels()).toBe(false); + }); + it('ignores stale content responses after the selected page changes', async () => { const pendingByPage = new Map< number, diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts index 5c112bfd8..643411369 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts @@ -116,12 +116,14 @@ describe('StalkerLiveStreamLayoutComponent', () => { const bulkItvEpgPlaylistId = signal(null); const bulkItvEpgPeriodHours = signal(null); const isLoadingBulkItvEpg = signal(false); + const hasMoreChannels = signal(false); + const page = signal(0); const stalkerStore = { getSelectedCategoryName: signal('News'), itvChannels, searchPhrase, - hasMoreChannels: signal(false), + hasMoreChannels, selectedItvId, currentPlaylist: playlist, selectedItvEpgPrograms, @@ -133,7 +135,7 @@ describe('StalkerLiveStreamLayoutComponent', () => { selectedCategoryId, selectedItem, selectedContentType: signal<'itv' | 'vod' | 'series'>('itv'), - page: signal(0), + page, setItvChannels: jest.fn(), setPage: jest.fn(), setSelectedItem: jest.fn((item) => { @@ -180,6 +182,8 @@ describe('StalkerLiveStreamLayoutComponent', () => { bulkItvEpgPlaylistId.set(null); bulkItvEpgPeriodHours.set(null); isLoadingBulkItvEpg.set(false); + hasMoreChannels.set(false); + page.set(0); resolveItvPlayback.mockReset(); resolveItvPlayback.mockResolvedValue({ @@ -202,7 +206,14 @@ describe('StalkerLiveStreamLayoutComponent', () => { ); }); stalkerStore.setItvChannels.mockClear(); - stalkerStore.setPage.mockClear(); + stalkerStore.setPage.mockReset(); + stalkerStore.setPage.mockImplementation((nextPage: number) => { + if (page() === nextPage) { + return; + } + + page.set(nextPage); + }); stalkerStore.setSelectedItem.mockClear(); stalkerStore.clearBulkItvEpgCache.mockClear(); @@ -272,6 +283,21 @@ describe('StalkerLiveStreamLayoutComponent', () => { ).toBeNull(); }); + it('does not reset live channels when loading the next lazy page', async () => { + hasMoreChannels.set(true); + fixture.detectChanges(); + await fixture.whenStable(); + stalkerStore.setItvChannels.mockClear(); + stalkerStore.setPage.mockClear(); + + component.loadMore(); + await fixture.whenStable(); + + expect(page()).toBe(1); + expect(stalkerStore.setPage).toHaveBeenCalledWith(1); + expect(stalkerStore.setItvChannels).not.toHaveBeenCalled(); + }); + it('keeps row previews empty before bulk epg is loaded', async () => { fixture.detectChanges(); await fixture.whenStable(); diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts index 832fe349a..715122ed3 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts @@ -8,6 +8,7 @@ import { inject, OnDestroy, signal, + untracked, viewChild, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -183,12 +184,14 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { // Reset channels/page on category change effect(() => { this.stalkerStore.selectedCategoryId(); - this.stalkerStore.setItvChannels([]); - this.stalkerStore.setPage(0); - this.clearEpgPreviewMaps(); - this.epgLoadRequestId += 1; - this.fallbackEpgPrograms.set([]); - this.isLoadingFallbackEpg.set(false); + untracked(() => { + this.stalkerStore.setItvChannels([]); + this.stalkerStore.setPage(0); + this.clearEpgPreviewMaps(); + this.epgLoadRequestId += 1; + this.fallbackEpgPrograms.set([]); + this.isLoadingFallbackEpg.set(false); + }); }); // Reset loading state when channels load and keep preview data in sync with bulk EPG.