fix(portal): drop a poster that fails to load from the About block (#1673)

The watch-state About block rendered its poster whenever the URL was
non-empty, so a URL that failed to load showed the browser's broken-image
glyph with clipped alt text. The hero above already falls back on error.

A failed URL now counts as missing, matching the block's own degradation
rule. The failure is keyed by URL so a different poster gets a fresh attempt.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-09-27 23:14:10 +02:00
1 parent 6b855eb73b
commit a0562d9f7c
3 files changed
+58 -3

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
type: fix
area: portal
---
The About section under the player no longer shows a broken image when a movie or series poster cannot be loaded; the poster is simply left out, as it is when the provider sends none.
@@ -0,0 +1,39 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { ContentAboutComponent } from './content-about.component';
describe('ContentAboutComponent', () => {
let fixture: ComponentFixture<ContentAboutComponent>;
const poster = (): HTMLImageElement | null =>
(fixture.nativeElement as HTMLElement).querySelector('.about__poster');
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContentAboutComponent, TranslateModule.forRoot()],
}).compileComponents();
fixture = TestBed.createComponent(ContentAboutComponent);
fixture.componentRef.setInput('title', 'Movie Title');
});
it('omits the poster when there is no URL', () => {
fixture.detectChanges();
expect(poster()).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Movie Title');
});
it('drops a poster that fails to load and retries a new URL', () => {
fixture.componentRef.setInput('posterUrl', 'https://img.test/a.jpg');
fixture.detectChanges();
expect(poster()?.getAttribute('src')).toBe('https://img.test/a.jpg');
poster()?.dispatchEvent(new Event('error'));
fixture.detectChanges();
expect(poster()).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Movie Title');
fixture.componentRef.setInput('posterUrl', 'https://img.test/b.jpg');
fixture.detectChanges();
expect(poster()?.getAttribute('src')).toBe('https://img.test/b.jpg');
});
});
@@ -2,7 +2,9 @@ import { NgTemplateOutlet } from '@angular/common';
import {
Component,
TemplateRef,
computed,
input,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@@ -13,7 +15,7 @@ import { TranslateModule } from '@ngx-translate/core';
* stays reachable while the player occupies the hero position.
*
* Degradation rule: anything missing simply is not rendered — no "N/A"
* placeholders. Chips and credits are stamped from the host-provided
* placeholders. A poster URL that fails to load counts as missing. Chips and credits are stamped from the host-provided
* *appDetailTags / *appDetailMeta templates, so the host's own @if guards apply.
*/
@Component({
@@ -24,12 +26,13 @@ import { TranslateModule } from '@ngx-translate/core';
<section class="about">
<h3 class="about__heading">{{ 'PORTALS.ABOUT' | translate }}</h3>
<div class="about__body">
@if (posterUrl()) {
@if (visiblePosterUrl(); as poster) {
<img
class="about__poster"
[src]="posterUrl()"
[src]="poster"
[alt]="title() ?? ''"
loading="lazy"
(error)="failedPosterUrl.set(poster)"
/>
}
<div class="about__info">
@@ -63,4 +66,11 @@ export class ContentAboutComponent {
readonly description = input<string>();
readonly tagsTemplate = input<TemplateRef<unknown> | null>(null);
readonly metaTemplate = input<TemplateRef<unknown> | null>(null);
// Keyed by URL so a different poster gets its own load attempt.
protected readonly failedPosterUrl = signal<string | undefined>(undefined);
protected readonly visiblePosterUrl = computed(() => {
const url = this.posterUrl();
return url && url !== this.failedPosterUrl() ? url : undefined;
});
}