Files
iptvnator/libs/ui/playback/src/lib/web-player-view/web-player-view.component.player-fallback.spec.ts
T
4grayandClaude Fable 5 d0d29567be fix(playback): resolve the player from the store while settings load
The engine is part of the application token, so picking one before the
persisted settings arrive mounts a second application and swaps the player
mid-playback. The settings read is an async IndexedDB subscription with no
initial value, so anyone who had not selected Video.js briefly got it —
visible on the new M3U movie detail path, which (unlike the live layout)
mounts the shared portal inline player and passes no explicit override.

SettingsStore holds the same value synchronously; an unhydrated store still
yields the Video.js default this used to hard-code, and host overrides keep
winning. Fixes the transient wrong engine for the portals' inline players too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:37:21 +02:00

126 lines
4.5 KiB
TypeScript

import { Component, input, output, signal } from '@angular/core';
import {
ComponentFixture,
DeferBlockBehavior,
TestBed,
} from '@angular/core/testing';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslateModule } from '@ngx-translate/core';
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { VideoPlayer } from '@iptvnator/shared/interfaces';
import { EMPTY } from 'rxjs';
import type { WebPlayerViewComponent as WebPlayerViewComponentInstance } from './web-player-view.component';
jest.unstable_mockModule('video.js', () => ({ default: jest.fn() }));
jest.unstable_mockModule('@yangkghjh/videojs-aspect-ratio-panel', () => ({}));
jest.unstable_mockModule('videojs-contrib-quality-levels', () => ({}));
jest.unstable_mockModule('videojs-quality-selector-hls', () => ({}));
@Component({
selector: 'app-vjs-player',
template: '<div data-test-id="stub-vjs"></div>',
})
class StubVjsPlayerComponent {
readonly options = input.required<unknown>();
readonly volume = input(1);
readonly timeUpdate = output<unknown>();
}
@Component({
selector: 'app-html-video-player',
template: '<div data-test-id="stub-html5"></div>',
})
class StubHtmlVideoPlayerComponent {
readonly channel = input.required<unknown>();
readonly volume = input(1);
readonly timeUpdate = output<unknown>();
}
/**
* The engine is part of the application token, so choosing the wrong one
* before the settings arrive mounts a second application and swaps the player
* under the user mid-playback. The persisted settings read is asynchronous;
* `SettingsStore` holds the same value synchronously.
*/
describe('WebPlayerViewComponent player fallback', () => {
let WebPlayerViewComponent: typeof WebPlayerViewComponentInstance;
let fixture: ComponentFixture<WebPlayerViewComponentInstance>;
const storedPlayer = signal<VideoPlayer>(VideoPlayer.Html5Player);
beforeAll(async () => {
({ WebPlayerViewComponent } =
await import('./web-player-view.component'));
});
const create = async (playerOverride: VideoPlayer | null = null) => {
await TestBed.configureTestingModule({
deferBlockBehavior: DeferBlockBehavior.Playthrough,
imports: [WebPlayerViewComponent, TranslateModule.forRoot()],
providers: [
// Never emits: stands in for the IndexedDB read still pending.
{ provide: StorageMap, useValue: { get: () => EMPTY } },
{
provide: SettingsStore,
useValue: {
player: storedPlayer,
showCaptions: signal(false),
},
},
{
provide: RuntimeCapabilitiesService,
useValue: { supportsManagedExternalPlayers: false },
},
],
})
.overrideComponent(WebPlayerViewComponent, {
set: {
imports: [
StubVjsPlayerComponent,
StubHtmlVideoPlayerComponent,
],
},
})
.compileComponents();
fixture = TestBed.createComponent(WebPlayerViewComponent);
fixture.componentRef.setInput('playbackSessionKey', 'live:p1:c1');
fixture.componentRef.setInput('streamUrl', 'http://host/movie.mkv');
if (playerOverride !== null) {
fixture.componentRef.setInput('playerOverride', playerOverride);
}
fixture.detectChanges();
};
afterEach(() => {
fixture?.destroy();
TestBed.resetTestingModule();
});
it('uses the store while the persisted settings read is pending', async () => {
storedPlayer.set(VideoPlayer.Html5Player);
await create();
expect(fixture.componentInstance.selectedPlayer()).toBe(
VideoPlayer.Html5Player
);
});
it('still lets a host override win', async () => {
storedPlayer.set(VideoPlayer.Html5Player);
await create(VideoPlayer.ArtPlayer);
expect(fixture.componentInstance.selectedPlayer()).toBe(
VideoPlayer.ArtPlayer
);
});
it('falls back to Video.js when the store has no player either', async () => {
storedPlayer.set(undefined as unknown as VideoPlayer);
await create();
expect(fixture.componentInstance.selectedPlayer()).toBe(
VideoPlayer.VideoJs
);
});
});