From bc034cf16345cc38ddd31cd324bbd75637d21870 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 16:53:11 +0200 Subject: [PATCH] fix(m3u): the cast row renders plain entries where no actor page exists With TMDB enrichment the M3U movie details showed every person as a focusable button whose activation did nothing, since the M3U library has no actor page. The row takes an `interactive` input and the M3U details turn it off. Co-Authored-By: Claude Fable 5.1 --- .../m3u-vod-detail.component.html | 7 ++- .../detail-ui/cast-crew-row.component.spec.ts | 56 +++++++++++++++++++ .../lib/detail-ui/cast-crew-row.component.ts | 6 +- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 libs/ui/components/src/lib/detail-ui/cast-crew-row.component.spec.ts diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html index 8a2505391..c1d407481 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html @@ -56,6 +56,11 @@ }
- + +
diff --git a/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.spec.ts b/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.spec.ts new file mode 100644 index 000000000..1006ed35d --- /dev/null +++ b/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import type { TmdbEnrichedCastMember } from '@iptvnator/shared/interfaces'; +import { CastCrewRowComponent } from './cast-crew-row.component'; + +const ACTOR: TmdbEnrichedCastMember = { + name: 'Ada Vance', + character: 'Captain', + tmdbPersonId: 501, +} as TmdbEnrichedCastMember; + +describe('CastCrewRowComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CastCrewRowComponent, TranslateModule.forRoot()], + }).compileComponents(); + fixture = TestBed.createComponent(CastCrewRowComponent); + fixture.componentRef.setInput('cast', [ACTOR]); + }); + + function person(): HTMLElement { + const element = (fixture.nativeElement as HTMLElement).querySelector( + '.person' + ); + expect(element).not.toBeNull(); + return element as HTMLElement; + } + + it('offers a person with a TMDB id as a button that opens the actor page', () => { + const personSelected = jest.fn(); + fixture.componentInstance.personSelected.subscribe(personSelected); + fixture.detectChanges(); + + expect(person().getAttribute('role')).toBe('button'); + expect(person().getAttribute('tabindex')).toBe('0'); + person().click(); + expect(personSelected).toHaveBeenCalledWith(ACTOR); + }); + + it('renders plain entries for a host without an actor page', () => { + const personSelected = jest.fn(); + fixture.componentInstance.personSelected.subscribe(personSelected); + fixture.componentRef.setInput('interactive', false); + fixture.detectChanges(); + + // A focusable button that does nothing would mislead mouse and + // keyboard users alike. + expect(person().getAttribute('role')).toBeNull(); + expect(person().getAttribute('tabindex')).toBeNull(); + expect(person().classList.contains('person--clickable')).toBe(false); + person().click(); + expect(personSelected).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.ts b/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.ts index 3fe9df2d0..6adba7851 100644 --- a/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.ts +++ b/libs/ui/components/src/lib/detail-ui/cast-crew-row.component.ts @@ -22,7 +22,7 @@ export function scrollToCastCrewRow(): void { * "Cast & crew" row under the hero: 72px round avatars with the full name * and the character (or "Director"), the director first, initials when a * person has no photo. A click on a person with a TMDB id opens the actor - * page through the host. + * page through the host; a host without one renders plain entries. */ @Component({ selector: 'app-cast-crew-row', @@ -35,6 +35,8 @@ export class CastCrewRowComponent { readonly cast = input([]); readonly directors = input([]); readonly headingId = input('detail-cast-crew'); + /** False when the host has no actor page: nobody becomes a button that does nothing. */ + readonly interactive = input(true); readonly personSelected = output(); @@ -49,7 +51,7 @@ export class CastCrewRowComponent { ); isClickable(entry: CastCrewEntry): boolean { - return !!entry.member.tmdbPersonId; + return this.interactive() && !!entry.member.tmdbPersonId; } select(entry: CastCrewEntry): void {