From aee5f61c1b1c5fa3fae75f71a6983eb7f8cec44c Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 28 Jun 2026 11:55:56 +0200 Subject: [PATCH] fix(epg): avoid duplicate track keys for archive rows --- .../src/lib/epg-list/epg-list.component.html | 2 +- .../lib/epg-list/epg-list.component.spec.ts | 55 +++++++++++++++++++ .../src/lib/epg-list/epg-list.component.ts | 13 +++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.html b/libs/ui/epg/src/lib/epg-list/epg-list.component.html index 1e11b461c..6f56ef7eb 100644 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.html +++ b/libs/ui/epg/src/lib/epg-list/epg-list.component.html @@ -35,7 +35,7 @@
@if (items.length > 0) { - @for (program of items; track program.start) { + @for (program of items; track trackProgram($index, program)) { @if (!canActivateProgram(program)) {
{ expect(emitted).toEqual([{ program: programs[0], type: 'timeshift' }]); }); + it('renders duplicate-start programs without duplicate track-key errors and keeps clicks bound to each row', () => { + const programs = [ + buildProgram( + 'duplicate-a', + 'Duplicate Start A', + '2026-04-05T09:00:00.000Z', + '2026-04-05T09:30:00.000Z' + ), + buildProgram( + 'duplicate-b', + 'Duplicate Start B', + '2026-04-05T09:00:00.000Z', + '2026-04-05T10:00:00.000Z' + ), + buildProgram( + 'current', + 'Current Show', + '2026-04-05T11:30:00.000Z', + '2026-04-05T12:30:00.000Z' + ), + ]; + const emitted: unknown[] = []; + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + component.programActivated.subscribe((event) => emitted.push(event)); + fixture.componentRef.setInput('controlledPrograms', programs); + fixture.componentRef.setInput('archivePlaybackAvailable', true); + + try { + expect(component.trackProgram(0, programs[0])).not.toBe( + component.trackProgram(1, programs[1]) + ); + + fixture.detectChanges(); + + const duplicateTrackKeyError = consoleErrorSpy.mock.calls.some( + (args) => args.some((arg) => String(arg).includes('NG0955')) + ); + const rows = + fixture.nativeElement.querySelectorAll( + '.program-item.clickable' + ); + rows[1].click(); + + expect(duplicateTrackKeyError).toBe(false); + expect(rows).toHaveLength(3); + expect(emitted).toEqual([ + { program: programs[1], type: 'timeshift' }, + ]); + } finally { + consoleErrorSpy.mockRestore(); + } + }); + it('updates the selected-day header when the app language changes', () => { fixture.componentRef.setInput('controlledPrograms', buildPrograms()); fixture.detectChanges(); diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.ts b/libs/ui/epg/src/lib/epg-list/epg-list.component.ts index 6a886a8ec..c220a9c49 100644 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.ts +++ b/libs/ui/epg/src/lib/epg-list/epg-list.component.ts @@ -241,6 +241,19 @@ export class EpgListComponent { this.activateProgram(program); } + trackProgram(index: number, program: EpgProgram): string { + const start = getProgramTimeMs(program.start, program.startTimestamp); + const stop = getProgramTimeMs(program.stop, program.stopTimestamp); + + return [ + program.channel ?? '', + Number.isFinite(start) ? start : program.start, + Number.isFinite(stop) ? stop : program.stop, + program.title ?? '', + index, + ].join('|'); + } + canActivateProgram(program: EpgProgram): boolean { return ( this.isProgramPlaying(program) ||