fix(stalker): keep live playback when switching the ITV/radio category (#1517)

Switching the Live TV or radio category in the shell context panel tore
down the Stalker player: `onStalkerCategoryClicked` cleared the selected
item for every section, and the live layout gates its player on
`selectedItem`. Xtream live (#936) and M3U groups already keep the channel
playing across a category/group switch.

- Context panel: return before `clearSelectedItem()` for `itv`/`radio`;
  VOD/series clicks still drop the open detail before navigating.
- Live layout: the category-change reset effect no longer wipes the
  playing channel's short-EPG fallback or cancels a fallback load in
  flight; only a section change (itv <-> radio) does that now.
- Regression coverage in the context panel spec, the live layout spec and
  a new web E2E scenario; docs and a `.changes/` note added.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-03 21:42:50 +02:00
1 parent 308ed9cb41
commit 2fc1bd5601
9 files changed
+258 -3

No files matched your search

@@ -534,7 +534,7 @@ describe('WorkspaceContextPanelComponent', () => {
);
});
it('labels Stalker radio categories and keeps category selection in the radio layout', () => {
it('labels Stalker radio categories and keeps the playing station on a category click', () => {
fixture.componentRef.setInput('context', {
provider: 'stalker',
playlistId: 'stalker-1',
@@ -558,10 +558,72 @@ describe('WorkspaceContextPanelComponent', () => {
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith('*');
expect(stalkerStore.setPage).toHaveBeenCalledWith(0);
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
// The radio layout renders its audio player only while the store has
// a selected item — clearing it here would silence the station.
expect(stalkerStore.clearSelectedItem).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('preserves the playing Stalker ITV channel when switching live categories', () => {
// Regression: the click handler used to clearSelectedItem() for every
// Stalker section. The live layout gates its player on selectedItem,
// so a category switch in the sidebar stopped a channel the user never
// switched away from — unlike Xtream live (#936) and M3U groups.
fixture.componentRef.setInput('context', {
provider: 'stalker',
playlistId: 'stalker-1',
});
fixture.componentRef.setInput('section', 'itv');
stalkerStore.getCategoryResource.set([
{ category_id: '*', category_name: 'All channels' },
{ category_id: '7', category_name: 'Sports' },
]);
fixture.detectChanges();
const categoryButtons = Array.from(
fixture.nativeElement.querySelectorAll('.category-item')
) as HTMLButtonElement[];
categoryButtons[1]?.click();
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith('7');
expect(stalkerStore.setPage).toHaveBeenCalledWith(0);
expect(stalkerStore.clearSelectedItem).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('still closes the open Stalker detail when switching VOD categories', () => {
// VOD/series category clicks navigate to a list route, so the open
// detail must be dropped there — the live exemption is deliberate and
// narrow.
fixture.componentRef.setInput('context', {
provider: 'stalker',
playlistId: 'stalker-1',
});
fixture.componentRef.setInput('section', 'vod');
stalkerStore.getCategoryResource.set([
{ category_id: '*', category_name: 'All movies' },
{ category_id: '7', category_name: 'Action' },
]);
fixture.detectChanges();
const categoryButtons = Array.from(
fixture.nativeElement.querySelectorAll('.category-item')
) as HTMLButtonElement[];
categoryButtons[1]?.click();
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith('7');
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith([
'/workspace',
'stalker',
'stalker-1',
'vod',
'7',
]);
});
describe('Stalker category error description', () => {
afterEach(() => {
stalkerStore.isCategoryResourceFailed.set(false as never);
@@ -458,12 +458,18 @@ export class WorkspaceContextPanelComponent {
this.contextDrawer?.close();
this.stalkerStore.setSelectedCategory(categoryId);
this.stalkerStore.setPage(0);
this.stalkerStore.clearSelectedItem();
if (section === 'itv' || section === 'radio') {
// Live TV / radio: the category only re-filters the channel
// sidebar, the selected channel keeps playing (Xtream parity,
// #936). The live layout gates its player on `selectedItem`, so
// clearing it here would tear the player down for a channel the
// user never switched away from.
return;
}
this.stalkerStore.clearSelectedItem();
if (categoryId === '*') {
this.router.navigate([
'/workspace',