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 {