Files
iptvnator/apps/web/src/app/services/external-playback.service.spec.ts
T
4grayandClaude Opus 5.5 e41bcb9f65 perf(playback): keep the history gate off the initial bundle
The `@iptvnator/services` barrel ships in the initial bundle, so adding
PlaybackHistoryGate there (and subscribing to it from the app-wide
ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes.

- Move the gate to a new lazy-only `playback-data-access` project
  (`@iptvnator/playback/data-access`; scope:shared, domain:playback,
  type:data-access) and register it in the coverage policy.
- The gate subscribes to MPV/VLC session updates itself; it is created by
  the first deferred write, which precedes the launch it waits for.
  ExternalPlaybackService is back to master.
- The Xtream "playlist switched before confirmation" check moves to the
  lazy helper; the initial-path store only takes a `skipListRefresh` flag.

Net effect on this branch: +27 bytes over master (master itself is
108 bytes over the ratchet baseline already).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:41:31 +02:00

181 lines
5.5 KiB
TypeScript

import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
import { ExternalPlaybackService } from './external-playback.service';
describe('ExternalPlaybackService', () => {
let listener: ((session: ExternalPlayerSession) => void) | undefined;
let closeExternalPlayerSession: jest.Mock;
let service: ExternalPlaybackService;
const createSession = (
overrides: Partial<ExternalPlayerSession> = {}
): ExternalPlayerSession => ({
id: 'session-1',
player: 'mpv',
status: 'launching',
title: 'Example',
streamUrl: 'https://example.com/video.m3u8',
contentInfo: {
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'vod',
},
startedAt: '2026-03-07T10:00:00.000Z',
updatedAt: '2026-03-07T10:00:00.000Z',
canClose: true,
...overrides,
});
beforeEach(() => {
listener = undefined;
closeExternalPlayerSession = jest.fn().mockResolvedValue(null);
Object.defineProperty(window, 'electron', {
configurable: true,
value: {
onExternalPlayerSessionUpdate: jest.fn((callback) => {
listener = callback;
return () => undefined;
}),
closeExternalPlayerSession,
},
});
service = new ExternalPlaybackService();
});
it('tracks the latest launch and hides dismissed sessions until the next launch', () => {
const first = createSession();
listener?.(first);
expect(service.activeSession()).toEqual(first);
expect(service.visibleSession()).toEqual(first);
service.dismissActiveSession();
expect(service.visibleSession()).toBeNull();
const next = createSession({
id: 'session-2',
title: 'Another Example',
});
listener?.(next);
expect(service.visibleSession()).toEqual(next);
});
it('matches only active non-error sessions to content info', () => {
const playing = createSession({
status: 'playing',
updatedAt: '2026-03-07T10:00:05.000Z',
});
listener?.(playing);
expect(
service.findMatchingSession({
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'vod',
})
).toEqual(playing);
listener?.(
createSession({
status: 'error',
error: 'Launch failed',
updatedAt: '2026-03-07T10:00:06.000Z',
})
);
expect(
service.findMatchingSession({
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'vod',
})
).toBeNull();
expect(service.visibleSession()).toEqual(
expect.objectContaining({ status: 'error' })
);
});
it('delegates close requests for closable sessions', async () => {
const session = createSession({ status: 'opened' });
listener?.(session);
closeExternalPlayerSession.mockResolvedValue(
createSession({
...session,
status: 'closed',
canClose: false,
updatedAt: '2026-03-07T10:00:10.000Z',
})
);
await service.closeActiveSession();
expect(closeExternalPlayerSession).toHaveBeenCalledWith(session.id);
expect(service.visibleSession()).toBeNull();
});
it('can close a specific session directly', async () => {
const session = createSession({ id: 'session-2', status: 'playing' });
closeExternalPlayerSession.mockResolvedValue(
createSession({
...session,
status: 'closed',
canClose: false,
updatedAt: '2026-03-07T10:00:12.000Z',
})
);
await service.closeSession(session);
expect(closeExternalPlayerSession).toHaveBeenCalledWith('session-2');
});
it('hides closed sessions but keeps errors visible until dismissed', () => {
listener?.(createSession({ status: 'closed', canClose: false }));
expect(service.visibleSession()).toBeNull();
const failed = createSession({
id: 'session-3',
status: 'error',
error: 'Launch failed',
canClose: false,
});
listener?.(createSession({ id: failed.id }));
listener?.(failed);
expect(service.visibleSession()).toEqual(failed);
service.dismissActiveSession();
expect(service.visibleSession()).toBeNull();
});
it('adopts an exactly correlated restored session after replacement teardown fails', () => {
const previous = createSession({
id: 'session-previous',
status: 'opened',
title: 'Previous stream',
});
const replacement = createSession({
id: 'session-replacement',
title: 'Replacement stream',
});
listener?.(previous);
listener?.(replacement);
listener?.({
...previous,
restoredFromSessionId: replacement.id,
updatedAt: '2026-03-07T10:00:20.000Z',
});
expect(service.activeSession()).toMatchObject({
id: previous.id,
status: 'opened',
});
expect(service.visibleSession()?.id).toBe(previous.id);
});
});