mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
fix(downloads): harden offline detail interactions
This commit is contained in:
1 parent
a741415a6a
commit
211ef784fc
24 files changed
+1417
-255
No files matched your search
@@ -1,8 +1,13 @@
|
||||
<!-- Hero Section with Backdrop -->
|
||||
<section class="hero">
|
||||
<!-- Back Button -->
|
||||
<button class="hero__back-button" (click)="onBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
<button
|
||||
class="hero__back-button"
|
||||
type="button"
|
||||
[attr.aria-label]="backLabel() || ('BACK' | translate)"
|
||||
(click)="onBack()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
||||
</button>
|
||||
@if (errorMessage()) {
|
||||
<div class="hero__content hero__error">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { ContentHeroComponent } from './content-hero.component';
|
||||
|
||||
describe('ContentHeroComponent', () => {
|
||||
@@ -20,6 +20,9 @@ describe('ContentHeroComponent', () => {
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ContentHeroComponent);
|
||||
const translate = TestBed.inject(TranslateService);
|
||||
translate.setTranslation('en', { BACK: 'Go back' });
|
||||
translate.use('en');
|
||||
});
|
||||
|
||||
it('renders description content when ResizeObserver is unavailable', () => {
|
||||
@@ -38,4 +41,47 @@ describe('ContentHeroComponent', () => {
|
||||
expect(host.textContent).toContain('Fallback Title');
|
||||
expect(host.textContent).toContain('Plain description');
|
||||
});
|
||||
|
||||
it('renders an accessible button with the translated fallback back label', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const back = (fixture.nativeElement as HTMLElement).querySelector(
|
||||
'.hero__back-button'
|
||||
) as HTMLButtonElement;
|
||||
expect(back.type).toBe('button');
|
||||
expect(back.getAttribute('aria-label')).toBe('Go back');
|
||||
});
|
||||
|
||||
it('uses an explicit back label when supplied', () => {
|
||||
fixture.componentRef.setInput('backLabel', 'Back to downloads');
|
||||
fixture.detectChanges();
|
||||
|
||||
const back = (fixture.nativeElement as HTMLElement).querySelector(
|
||||
'.hero__back-button'
|
||||
) as HTMLButtonElement;
|
||||
expect(back.getAttribute('aria-label')).toBe('Back to downloads');
|
||||
});
|
||||
|
||||
it('resets a poster failure when the poster URL changes', () => {
|
||||
fixture.componentRef.setInput('posterUrl', 'broken.jpg');
|
||||
fixture.detectChanges();
|
||||
const first = (fixture.nativeElement as HTMLElement).querySelector(
|
||||
'img[src="broken.jpg"]'
|
||||
) as HTMLImageElement;
|
||||
first.dispatchEvent(new Event('error'));
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).querySelector(
|
||||
'img[src="broken.jpg"]'
|
||||
)
|
||||
).toBeNull();
|
||||
|
||||
fixture.componentRef.setInput('posterUrl', 'replacement.jpg');
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).querySelector(
|
||||
'img[src="replacement.jpg"]'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -37,17 +37,23 @@ export class ContentHeroComponent {
|
||||
readonly backdropUrl = input<string>();
|
||||
readonly isLoading = input(false);
|
||||
readonly errorMessage = input<string>();
|
||||
readonly backLabel = input<string>();
|
||||
|
||||
readonly backClicked = output<void>();
|
||||
readonly posterError = signal(false);
|
||||
|
||||
readonly descriptionEl = viewChild<ElementRef<HTMLElement>>('descriptionEl');
|
||||
readonly descriptionEl =
|
||||
viewChild<ElementRef<HTMLElement>>('descriptionEl');
|
||||
readonly isDescriptionExpanded = signal(false);
|
||||
readonly hasDescriptionOverflow = signal(false);
|
||||
|
||||
private resizeObserver?: ResizeObserver;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.posterUrl();
|
||||
untracked(() => this.posterError.set(false));
|
||||
});
|
||||
effect(() => {
|
||||
// Re-measure whenever description content or the element changes.
|
||||
this.description();
|
||||
@@ -120,7 +126,9 @@ export class ContentHeroComponent {
|
||||
this.measureOverflow(el);
|
||||
return;
|
||||
}
|
||||
this.resizeObserver = new ResizeObserver(() => this.measureOverflow(el));
|
||||
this.resizeObserver = new ResizeObserver(() =>
|
||||
this.measureOverflow(el)
|
||||
);
|
||||
this.resizeObserver.observe(el);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
[backdropUrl]="backdropUrl()"
|
||||
[isLoading]="isLoading()"
|
||||
[errorMessage]="errorMessage()"
|
||||
[backLabel]="backLabel()"
|
||||
(backClicked)="backClicked.emit()"
|
||||
>
|
||||
<ng-container hero-tags>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PortalDetailShellComponent } from './portal-detail-shell.component';
|
||||
[title]="'Show Title'"
|
||||
[description]="'Show description'"
|
||||
[posterUrl]="'poster.jpg'"
|
||||
[backLabel]="'Return to downloads'"
|
||||
[playbackActive]="playbackActive()"
|
||||
(closePlayerRequested)="closeRequests = closeRequests + 1"
|
||||
>
|
||||
@@ -72,6 +73,9 @@ describe('PortalDetailShellComponent', () => {
|
||||
expect(query('.details__meta .details__meta-item')).toBeTruthy();
|
||||
expect(query('.action-buttons .play-btn')).toBeTruthy();
|
||||
expect(query('app-content-about')).toBeNull();
|
||||
expect(query('.hero__back-button')?.getAttribute('aria-label')).toBe(
|
||||
'Return to downloads'
|
||||
);
|
||||
});
|
||||
|
||||
it('collapses hero and shows About with re-stamped templates in watch state', () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ export class PortalDetailShellComponent {
|
||||
readonly backdropUrl = input<string>();
|
||||
readonly isLoading = input(false);
|
||||
readonly errorMessage = input<string>();
|
||||
readonly backLabel = input<string>();
|
||||
/** True while inline playback is active — flips the layout to watch state. */
|
||||
readonly playbackActive = input(false);
|
||||
|
||||
|
||||
Reference in new issue
Block a user