diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts index be763f4b2..db85b9e16 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts @@ -99,6 +99,11 @@ describe('StalkerSeriesViewComponent', () => { macAddress: '00:1A:79:12:34:56', }); + async function stabilize(): Promise { + fixture.detectChanges(); + await fixture.whenStable(); + } + beforeEach(async () => { selectedContentType.set('series'); selectedItem.set({ @@ -285,8 +290,7 @@ describe('StalkerSeriesViewComponent', () => { }); it('renders quick start for regular series and starts the first episode', async () => { - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); const quickStartButton: HTMLButtonElement | null = @@ -322,8 +326,7 @@ describe('StalkerSeriesViewComponent', () => { it('keeps provider episodes playable while hiding download presentation', async () => { fixture.componentRef.setInput('providerOnly', true); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); const seasonContainer = fixture.debugElement.query( By.directive(StubSeasonContainerComponent) @@ -347,21 +350,16 @@ describe('StalkerSeriesViewComponent', () => { id: '50001', is_series: true, category_id: '18', - info: { - name: 'Signal House', - description: 'Parent series plot', - movie_image: - 'https://images.example.test/posters/signal-house.jpg', - }, + info: { name: 'Signal House' }, }); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); const seasonContainer = fixture.debugElement.query( By.directive(StubSeasonContainerComponent) ).componentInstance as StubSeasonContainerComponent; - const candidate = seasonContainer.downloadAdapter()?.createCandidate( + const initialAdapter = seasonContainer.downloadAdapter(); + const candidate = initialAdapter?.createCandidate( { id: '61001', episode_num: 3, @@ -387,24 +385,60 @@ describe('StalkerSeriesViewComponent', () => { expect(request).toEqual( expect.objectContaining({ playlistId: 'stalker-1', - playlistType: 'stalker', seriesXtreamId: 50001, xtreamId: 61001, - seasonNumber: 2, - episodeNumber: 3, title: 'Signal House - S02E03 - The Call', - url: 'https://cdn.example.test/episode.mpg', - playlistName: 'Living Room Portal', + }) + ); + + currentPlaylist.set({ + _id: 'stalker-2', + title: 'Bedroom Portal', + portalUrl: 'https://bedroom.example.test', + macAddress: '00:1A:79:65:43:21', + }); + selectedItem.set({ + id: '40002:season-slice', + category_id: '22', + info: { name: 'Second Signal' }, + }); + await stabilize(); + + const updatedAdapter = seasonContainer.downloadAdapter(); + expect(updatedAdapter).not.toBe(initialAdapter); + const updatedCandidate = updatedAdapter?.createCandidate( + { + id: '62001', + episode_num: 1, + title: 'Pilot', + custom_sid: 'regular-series', + season: 4, + originalCmd: '/media/file_888.mpg', + } as never, + '4' + ); + if (!updatedCandidate) { + throw new Error('expected a refreshed Stalker download adapter'); + } + const updatedRequest = await updatedCandidate.prepare(); + + expect(fetchLinkToPlay).toHaveBeenLastCalledWith( + 'https://bedroom.example.test', + '00:1A:79:65:43:21', + '/media/file_888.mpg', + 1 + ); + expect(updatedRequest).toEqual( + expect.objectContaining({ + playlistId: 'stalker-2', + seriesXtreamId: 40002, + xtreamId: 62001, + title: 'Second Signal - S04E01 - Pilot', + portalUrl: 'https://bedroom.example.test', + macAddress: '00:1A:79:65:43:21', metadataSnapshot: expect.objectContaining({ - language: 'en', - mediaKind: 'series', - title: 'Signal House', - plot: 'Parent series plot', - providerCategoryId: '18', - episode: expect.objectContaining({ - seasonNumber: 2, - episodeNumber: 3, - }), + title: 'Second Signal', + providerCategoryId: '22', }), }) ); @@ -424,8 +458,7 @@ describe('StalkerSeriesViewComponent', () => { serialSeasonsResource.set([]); vodSeriesSeasonsResource.set([]); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { id: 'season-1', @@ -505,8 +538,7 @@ describe('StalkerSeriesViewComponent', () => { }, ]); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); const quickStartButton: HTMLButtonElement | null = @@ -577,8 +609,7 @@ describe('StalkerSeriesViewComponent', () => { }, ]); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { @@ -677,8 +708,7 @@ describe('StalkerSeriesViewComponent', () => { ]); isEmbeddedPlayer.mockReturnValue(true); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { @@ -820,8 +850,7 @@ describe('StalkerSeriesViewComponent', () => { ]); isEmbeddedPlayer.mockReturnValue(true); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // Season 1 loaded, season 2 still empty — the state a user is in // right after opening the series and starting the first episode. @@ -855,8 +884,7 @@ describe('StalkerSeriesViewComponent', () => { const firstEpisode = fixture.componentInstance.mappedSeasons()['1'][0]; fixture.componentInstance.onEpisodeClicked(firstEpisode); await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); expect(fetchVodSeriesEpisodes).toHaveBeenCalledWith( @@ -904,8 +932,7 @@ describe('StalkerSeriesViewComponent', () => { ]); isEmbeddedPlayer.mockReturnValue(true); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { @@ -938,8 +965,7 @@ describe('StalkerSeriesViewComponent', () => { const seasonOne = fixture.componentInstance.mappedSeasons()['1']; fixture.componentInstance.onEpisodeClicked(seasonOne[0]); await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); expect(fetchVodSeriesEpisodes).toHaveBeenCalledTimes(1); @@ -947,8 +973,7 @@ describe('StalkerSeriesViewComponent', () => { // Further playback activity in the same season must not retrigger it. fixture.componentInstance.onEpisodeClicked(seasonOne[1]); await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); expect(fetchVodSeriesEpisodes).toHaveBeenCalledTimes(1); @@ -982,8 +1007,7 @@ describe('StalkerSeriesViewComponent', () => { ]); isEmbeddedPlayer.mockReturnValue(true); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { @@ -1014,8 +1038,7 @@ describe('StalkerSeriesViewComponent', () => { const seasonOne = fixture.componentInstance.mappedSeasons()['1']; fixture.componentInstance.onEpisodeClicked(seasonOne[0]); await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); // The failure must not loop while the same episode keeps playing. @@ -1027,8 +1050,7 @@ describe('StalkerSeriesViewComponent', () => { ]); fixture.componentInstance.onEpisodeClicked(seasonOne[1]); await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); expect(fetchVodSeriesEpisodes).toHaveBeenCalledTimes(2); @@ -1079,8 +1101,7 @@ describe('StalkerSeriesViewComponent', () => { }, ]); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.vodSeriesSeasons.set([ { @@ -1151,8 +1172,7 @@ describe('StalkerSeriesViewComponent', () => { }); it('fetches the TMDB season once the show-level match arrives after auto-select', async () => { - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.detectChanges(); // Season tabs auto-select immediately — usually before the async @@ -1161,8 +1181,7 @@ describe('StalkerSeriesViewComponent', () => { By.directive(StubSeasonContainerComponent) ).componentInstance as StubSeasonContainerComponent; seasonContainer.seasonSelected.emit('1'); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).not.toHaveBeenCalled(); // The TMDB match lands afterwards — the fetch must run now. @@ -1176,8 +1195,7 @@ describe('StalkerSeriesViewComponent', () => { tmdb_id: 777, }, } as never); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).toHaveBeenCalledWith(777, 1); }); @@ -1194,12 +1212,10 @@ describe('StalkerSeriesViewComponent', () => { tmdb_id: 777, }, } as never); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.onSeasonSelected('1'); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // Season resource still loading — fetching now would pass a zero // season count, suppress the title-marker override and cache the @@ -1214,8 +1230,7 @@ describe('StalkerSeriesViewComponent', () => { series: [1, 2], }, ]); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // Single-season slice whose provider season is renumbered to 1: // the title marker names the real TMDB season. @@ -1223,12 +1238,10 @@ describe('StalkerSeriesViewComponent', () => { }); it('gates the fetch on the reloading season resource during detail-to-detail navigation', async () => { - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.onSeasonSelected('1'); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // No show-level TMDB match yet — nothing fetched for the first item expect(tmdbGetSeason).not.toHaveBeenCalled(); @@ -1246,8 +1259,7 @@ describe('StalkerSeriesViewComponent', () => { tmdb_id: 888, }, } as never); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // The new tmdb_id must NOT pair with the previous series' season // context while the resource reloads. @@ -1266,17 +1278,14 @@ describe('StalkerSeriesViewComponent', () => { }, ]); isSerialSeasonsLoading.set(false); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).toHaveBeenCalledWith(888, 2); }); it('enriches after equal-id navigation once the season resource settles', async () => { - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.onSeasonSelected('1'); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); // Distinct items can reuse a provider id; the loading gate (not an // id comparison) keeps the stale map from being used. @@ -1291,13 +1300,11 @@ describe('StalkerSeriesViewComponent', () => { tmdb_id: 999, }, } as never); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).not.toHaveBeenCalled(); isSerialSeasonsLoading.set(false); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).toHaveBeenCalledWith(999, 3); }); @@ -1313,12 +1320,10 @@ describe('StalkerSeriesViewComponent', () => { tmdb_id: 777, }, } as never); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); fixture.componentInstance.onSeasonSelected('1'); - fixture.detectChanges(); - await fixture.whenStable(); + await stabilize(); expect(tmdbGetSeason).toHaveBeenCalledWith(777, 2); });