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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 16:53:11 +02:00
1 parent aaa86f8c9c
commit bc034cf163
3 files changed
+66 -3

No files matched your search

@@ -56,6 +56,11 @@
}
<div detail-extras>
<app-cast-crew-row [cast]="cast()" [directors]="directorMembers()" />
<!-- M3U has no actor page: the people stay plain entries -->
<app-cast-crew-row
[cast]="cast()"
[directors]="directorMembers()"
[interactive]="false"
/>
</div>
</app-portal-detail-shell>
@@ -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<CastCrewRowComponent>;
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();
});
});
@@ -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 TmdbEnrichedCastMember[]>([]);
readonly directors = input<readonly TmdbEnrichedCastMember[]>([]);
readonly headingId = input<string | null>('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<TmdbEnrichedCastMember>();
@@ -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 {