From 3170b21e744fabbe2d5a5426065ca2cd2955ab5a Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 12 Aug 2026 19:41:59 +0200 Subject: [PATCH] test(playlist): pin the volume re-read across same-id channel entries `createChannel` falls back to the URL for a missing id, so one stream listed in two groups yields two entries sharing an id. The volume re-read must not depend on ids being distinct; the linkedSignal now sources the channel itself and the guarantee is pinned by a test. Co-Authored-By: Claude Fable 5 --- .../video-player-movie-gate.spec.ts | 17 +++++++++++++++++ .../lib/video-player/video-player.component.ts | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts index 791d15553..ec2092a1c 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts @@ -357,5 +357,22 @@ describe('VideoPlayerComponent — M3U movie recognition gate', () => { expect(component.volume()).toBe(0); }); + + it('re-reads even when the next entry shares the previous id', () => { + // `createChannel` falls back to the URL for the id, so one stream + // listed under two groups yields two entries with the same id. + // Pins the user-visible guarantee independently of what the + // linkedSignal source happens to derive. + localStorage.setItem('volume', '0.8'); + syncStoreState(movieChannel); + fixture.detectChanges(); + expect(component.volume()).toBe(0.8); + + localStorage.setItem('volume', '0.2'); + syncStoreState({ ...movieChannel, group: { title: 'New' } }); + fixture.detectChanges(); + + expect(component.volume()).toBe(0.2); + }); }); }); diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts index a6633699a..96b15ba09 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts @@ -486,9 +486,18 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { * with as soon as the user zapped after adjusting it in the player. * Remote-control writes still `set()` this signal directly and store the * same value, so re-reading the bus per channel agrees with them. + * + * The source is the whole channel, not a derived key: `linkedSignal` + * re-runs its computation whenever the source EXPRESSION invalidates — + * the derived value's equality does not gate it (`producerRecomputeValue` + * calls `computation()` right after re-evaluating `source()`). Selecting + * the channel is therefore the trigger, which is exactly the intent, and + * nothing here depends on ids being distinct — `createChannel` falls back + * to the URL, so one stream listed in two groups shares an id. A re-read + * is one idempotent localStorage hit, so an extra one costs nothing. */ readonly volume = linkedSignal({ - source: () => this.activeChannel()?.id ?? null, + source: () => this.activeChannel(), computation: () => readStoredVolume(), });