From a1b0d9186c32d4d4f3fa155ff7c45c5c28d8d97c Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Tue, 26 May 2026 19:30:21 +0200 Subject: [PATCH] chore(test): clean lint and warning baseline --- apps/web/src/test-setup.ts | 33 ++++--- .../vod-details-route.component.spec.ts | 31 ++++++ .../src/lib/xtream-content-gate.component.ts | 16 +-- libs/portal/xtream/feature/src/test-setup.ts | 3 + libs/shared/testing/project.json | 12 +++ libs/shared/testing/src/index.ts | 1 + .../testing/src/lib/console-warn-filters.ts | 19 ++++ .../embedded-mpv-session-controller.ts | 98 ++++++++++++------- tsconfig.base.json | 1 + 9 files changed, 154 insertions(+), 60 deletions(-) create mode 100644 libs/shared/testing/project.json create mode 100644 libs/shared/testing/src/index.ts create mode 100644 libs/shared/testing/src/lib/console-warn-filters.ts diff --git a/apps/web/src/test-setup.ts b/apps/web/src/test-setup.ts index 5506c1d45..c28563d25 100644 --- a/apps/web/src/test-setup.ts +++ b/apps/web/src/test-setup.ts @@ -1,26 +1,31 @@ import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone/index.mjs'; +import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing'; Object.defineProperty(globalThis, 'jest', { - configurable: true, - value: import.meta.jest, + configurable: true, + value: import.meta.jest, }); -if (typeof (globalThis as { IDBFactory?: unknown }).IDBFactory === 'undefined') { - Object.defineProperty(globalThis, 'IDBFactory', { - configurable: true, - value: class MockIDBFactory {}, - }); +if ( + typeof (globalThis as { IDBFactory?: unknown }).IDBFactory === 'undefined' +) { + Object.defineProperty(globalThis, 'IDBFactory', { + configurable: true, + value: class MockIDBFactory {}, + }); } if (!Element.prototype.animate) { - Element.prototype.animate = () => - ({ - cancel: () => undefined, - finished: Promise.resolve(), - }) as unknown as Animation; + Element.prototype.animate = () => + ({ + cancel: () => undefined, + finished: Promise.resolve(), + }) as unknown as Animation; } +installDuplicateVideoJsQualityLevelsWarnFilter(); + setupZoneTestEnv({ - errorOnUnknownElements: true, - errorOnUnknownProperties: true, + errorOnUnknownElements: true, + errorOnUnknownProperties: true, }); diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.spec.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.spec.ts index 4970c8466..775d1787f 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.spec.ts @@ -23,6 +23,8 @@ import { VodDetailsRouteComponent } from './vod-details-route.component'; describe('VodDetailsRouteComponent', () => { let fixture: ComponentFixture; + let consoleDebugSpy: jest.SpyInstance | undefined; + let consoleWarnSpy: jest.SpyInstance | undefined; const selectedItem = signal(null); const isLoadingDetails = signal(false); const detailsError = signal(null); @@ -47,6 +49,30 @@ describe('VodDetailsRouteComponent', () => { const getPlaybackPosition = jest.fn().mockResolvedValue(null); beforeEach(async () => { + const consoleDebug = console.debug.bind(console); + const consoleWarn = console.warn.bind(console); + consoleDebugSpy = jest + .spyOn(console, 'debug') + .mockImplementation((...args: unknown[]) => { + if (args[0] === '[VodDetailsRoute]') { + return; + } + + consoleDebug(...args); + }); + consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation((...args: unknown[]) => { + if ( + args[0] === '[VodDetailsRoute]' && + args[1] === 'Deferring VOD details init: playlist not ready' + ) { + return; + } + + consoleWarn(...args); + }); + selectedItem.set(null); isLoadingDetails.set(false); detailsError.set(null); @@ -166,6 +192,11 @@ describe('VodDetailsRouteComponent', () => { fixture = TestBed.createComponent(VodDetailsRouteComponent); }); + afterEach(() => { + consoleDebugSpy?.mockRestore(); + consoleWarnSpy?.mockRestore(); + }); + it('renders an informational fallback without playback controls when Xtream returns empty metadata', () => { selectedItem.set({ info: [], diff --git a/libs/portal/xtream/feature/src/lib/xtream-content-gate.component.ts b/libs/portal/xtream/feature/src/lib/xtream-content-gate.component.ts index 420d545aa..e53f35e12 100644 --- a/libs/portal/xtream/feature/src/lib/xtream-content-gate.component.ts +++ b/libs/portal/xtream/feature/src/lib/xtream-content-gate.component.ts @@ -1,15 +1,15 @@ -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { RouterOutlet } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; -import { - PlaylistErrorViewComponent, -} from '@iptvnator/portal/shared/ui'; -import { - XtreamContentInitBlockReason, - XtreamStore, -} from '@iptvnator/portal/xtream/data-access'; +import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui'; +import { XtreamStore } from '@iptvnator/portal/xtream/data-access'; import { XtreamCachedOfflineNoticeComponent } from './xtream-cached-offline-notice.component'; @Component({ diff --git a/libs/portal/xtream/feature/src/test-setup.ts b/libs/portal/xtream/feature/src/test-setup.ts index 0d88648e3..119f7a954 100644 --- a/libs/portal/xtream/feature/src/test-setup.ts +++ b/libs/portal/xtream/feature/src/test-setup.ts @@ -1,4 +1,7 @@ import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; +import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing'; + +installDuplicateVideoJsQualityLevelsWarnFilter(); setupZoneTestEnv({ errorOnUnknownElements: true, diff --git a/libs/shared/testing/project.json b/libs/shared/testing/project.json new file mode 100644 index 000000000..f0eccc583 --- /dev/null +++ b/libs/shared/testing/project.json @@ -0,0 +1,12 @@ +{ + "name": "shared-testing", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/testing/src", + "projectType": "library", + "tags": ["scope:shared", "domain:shared-runtime", "type:util"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/shared/testing/src/index.ts b/libs/shared/testing/src/index.ts new file mode 100644 index 000000000..caf0abf98 --- /dev/null +++ b/libs/shared/testing/src/index.ts @@ -0,0 +1 @@ +export { installDuplicateVideoJsQualityLevelsWarnFilter } from './lib/console-warn-filters'; diff --git a/libs/shared/testing/src/lib/console-warn-filters.ts b/libs/shared/testing/src/lib/console-warn-filters.ts new file mode 100644 index 000000000..f2b8ba45d --- /dev/null +++ b/libs/shared/testing/src/lib/console-warn-filters.ts @@ -0,0 +1,19 @@ +export function installDuplicateVideoJsQualityLevelsWarnFilter(): void { + const originalConsoleWarn = console.warn.bind(console); + + console.warn = (...args: unknown[]) => { + const message = args + .filter((arg): arg is string => typeof arg === 'string') + .join(' '); + + if (isDuplicateVideoJsQualityLevelsWarning(message)) { + return; + } + + originalConsoleWarn(...args); + }; +} + +function isDuplicateVideoJsQualityLevelsWarning(message: string): boolean { + return message.includes('A plugin named "qualityLevels" already exists.'); +} diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts index f7c21358f..d9083af80 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts @@ -21,6 +21,8 @@ export type EmbeddedMpvBoundsProvider = ( const STALLED_TIMEOUT_MS = 30_000; +type ElectronBridge = Window['electron']; + @Injectable() export class EmbeddedMpvSessionController { readonly support = signal(null); @@ -144,7 +146,14 @@ export class EmbeddedMpvSessionController { return; } - const prepared = await window.electron!.prepareEmbeddedMpv?.(); + const electron = this.getElectronBridge(); + if (!electron) { + throw new Error( + 'Embedded MPV requires the Electron desktop build.' + ); + } + + const prepared = await electron.prepareEmbeddedMpv?.(); if (disposed) { return; } @@ -158,24 +167,21 @@ export class EmbeddedMpvSessionController { this.support.set(prepared); } - const created = await window.electron!.createEmbeddedMpvSession( + const created = await electron.createEmbeddedMpvSession( measureBounds(host), playback.title, initialVolume ); if (disposed) { - await window.electron!.disposeEmbeddedMpvSession(created.id); + await electron.disposeEmbeddedMpvSession(created.id); return; } activeSessionId = created.id; this.sessionId.set(created.id); this.session.set(created); - await window.electron!.loadEmbeddedMpvPlayback( - created.id, - playback - ); + await electron.loadEmbeddedMpvPlayback(created.id, playback); scheduleBoundsSync(); }; @@ -213,14 +219,12 @@ export class EmbeddedMpvSessionController { async togglePaused(): Promise { const id = this.sessionId(); const session = this.session(); - if (!id || !session || !window.electron?.setEmbeddedMpvPaused) { + const electron = this.getElectronBridge(); + if (!id || !session || !electron?.setEmbeddedMpvPaused) { return; } const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvPaused( - id, - session.status !== 'paused' - ) + electron.setEmbeddedMpvPaused(id, session.status !== 'paused') ); if (updated) { this.session.set(updated); @@ -230,12 +234,13 @@ export class EmbeddedMpvSessionController { async seekBy(deltaSeconds: number): Promise { const id = this.sessionId(); const session = this.session(); - if (!id || !session || !window.electron?.seekEmbeddedMpv) { + const electron = this.getElectronBridge(); + if (!id || !session || !electron?.seekEmbeddedMpv) { return false; } const next = Math.max(0, session.positionSeconds + deltaSeconds); const updated = await this.guardIpc(() => - window.electron!.seekEmbeddedMpv(id, next) + electron.seekEmbeddedMpv(id, next) ); if (updated) { this.session.set(updated); @@ -245,11 +250,12 @@ export class EmbeddedMpvSessionController { async seekTo(seconds: number): Promise { const id = this.sessionId(); - if (!id || !window.electron?.seekEmbeddedMpv) { + const electron = this.getElectronBridge(); + if (!id || !electron?.seekEmbeddedMpv) { return; } const updated = await this.guardIpc(() => - window.electron!.seekEmbeddedMpv(id, seconds) + electron.seekEmbeddedMpv(id, seconds) ); if (updated) { this.session.set(updated); @@ -258,11 +264,12 @@ export class EmbeddedMpvSessionController { async applyVolume(value: number): Promise { const id = this.sessionId(); - if (!id || !window.electron?.setEmbeddedMpvVolume) { + const electron = this.getElectronBridge(); + if (!id || !electron?.setEmbeddedMpvVolume) { return; } const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvVolume(id, value) + electron.setEmbeddedMpvVolume(id, value) ); if (updated) { this.session.set(updated); @@ -271,11 +278,12 @@ export class EmbeddedMpvSessionController { async setAudioTrack(trackId: number): Promise { const id = this.sessionId(); - if (!id || !window.electron?.setEmbeddedMpvAudioTrack) { + const electron = this.getElectronBridge(); + if (!id || !electron?.setEmbeddedMpvAudioTrack) { return; } const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvAudioTrack(id, trackId) + electron.setEmbeddedMpvAudioTrack(id, trackId) ); if (updated) { this.session.set(updated); @@ -284,11 +292,13 @@ export class EmbeddedMpvSessionController { async setSubtitleTrack(trackId: number): Promise { const id = this.sessionId(); - if (!id || !window.electron?.setEmbeddedMpvSubtitleTrack) { + const electron = this.getElectronBridge(); + if (!id || !electron?.setEmbeddedMpvSubtitleTrack) { return; } + const setSubtitleTrack = electron.setEmbeddedMpvSubtitleTrack; const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvSubtitleTrack!(id, trackId) + setSubtitleTrack(id, trackId) ); if (updated) { this.session.set(updated); @@ -297,12 +307,12 @@ export class EmbeddedMpvSessionController { async setSpeed(speed: number): Promise { const id = this.sessionId(); - if (!id || !window.electron?.setEmbeddedMpvSpeed) { + const electron = this.getElectronBridge(); + if (!id || !electron?.setEmbeddedMpvSpeed) { return; } - const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvSpeed!(id, speed) - ); + const setSpeed = electron.setEmbeddedMpvSpeed; + const updated = await this.guardIpc(() => setSpeed(id, speed)); if (updated) { this.session.set(updated); } @@ -310,12 +320,12 @@ export class EmbeddedMpvSessionController { async setAspect(aspect: string): Promise { const id = this.sessionId(); - if (!id || !window.electron?.setEmbeddedMpvAspect) { + const electron = this.getElectronBridge(); + if (!id || !electron?.setEmbeddedMpvAspect) { return; } - const updated = await this.guardIpc(() => - window.electron!.setEmbeddedMpvAspect!(id, aspect) - ); + const setAspect = electron.setEmbeddedMpvAspect; + const updated = await this.guardIpc(() => setAspect(id, aspect)); if (updated) { this.session.set(updated); } @@ -326,15 +336,17 @@ export class EmbeddedMpvSessionController { title: string ): Promise { const id = this.sessionId(); - if (!id || !window.electron?.startEmbeddedMpvRecording) { + const electron = this.getElectronBridge(); + if (!id || !electron?.startEmbeddedMpvRecording) { return null; } + const startEmbeddedMpvRecording = electron.startEmbeddedMpvRecording; const resolvedDirectory = directory?.trim() || - (await window.electron.getEmbeddedMpvDefaultRecordingFolder?.()); + (await electron.getEmbeddedMpvDefaultRecordingFolder?.()); const updated = await this.guardIpc(() => - window.electron!.startEmbeddedMpvRecording!(id, { + startEmbeddedMpvRecording(id, { directory: resolvedDirectory, title, }) @@ -348,12 +360,12 @@ export class EmbeddedMpvSessionController { async stopRecording(): Promise { const id = this.sessionId(); - if (!id || !window.electron?.stopEmbeddedMpvRecording) { + const electron = this.getElectronBridge(); + if (!id || !electron?.stopEmbeddedMpvRecording) { return null; } - const updated = await this.guardIpc(() => - window.electron!.stopEmbeddedMpvRecording!(id) - ); + const stopEmbeddedMpvRecording = electron.stopEmbeddedMpvRecording; + const updated = await this.guardIpc(() => stopEmbeddedMpvRecording(id)); if (updated) { this.session.set(updated); return updated.recording ?? null; @@ -373,7 +385,13 @@ export class EmbeddedMpvSessionController { private async loadSupport(): Promise { try { - this.support.set(await window.electron!.getEmbeddedMpvSupport()); + const electron = this.getElectronBridge(); + if (!electron?.getEmbeddedMpvSupport) { + throw new Error( + 'Embedded MPV requires the Electron desktop build.' + ); + } + this.support.set(await electron.getEmbeddedMpvSupport()); } catch (error) { this.support.set({ supported: false, @@ -383,6 +401,10 @@ export class EmbeddedMpvSessionController { } } + private getElectronBridge(): ElectronBridge | undefined { + return window.electron; + } + private handleStalledTracking( status: EmbeddedMpvSession['status'] | null ): void { diff --git a/tsconfig.base.json b/tsconfig.base.json index 4a3b0d3d9..8f5882ce8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -79,6 +79,7 @@ "@iptvnator/shared/m3u-utils": [ "libs/shared/m3u-utils/src/index.ts" ], + "@iptvnator/shared/testing": ["libs/shared/testing/src/index.ts"], "@iptvnator/services": ["libs/services/src/index.ts"], "@iptvnator/shared/interfaces": [ "libs/shared/interfaces/src/index.ts"