diff --git a/libs/playlist/shared/util/src/lib/playlist-context.facade.spec.ts b/libs/playlist/shared/util/src/lib/playlist-context.facade.spec.ts index be6144df7..36529ca18 100644 --- a/libs/playlist/shared/util/src/lib/playlist-context.facade.spec.ts +++ b/libs/playlist/shared/util/src/lib/playlist-context.facade.spec.ts @@ -10,6 +10,7 @@ import { } from '@iptvnator/m3u-state'; import { Subject } from 'rxjs'; import { PlaylistMeta } from '@iptvnator/shared/interfaces'; +import { RuntimeCapabilitiesService } from '@iptvnator/services'; import { PlaylistContextFacade, PlaylistRouteContext, @@ -43,7 +44,7 @@ describe('PlaylistContextFacade', () => { let playlistsSignal: ReturnType>; let loadedSignal: ReturnType>; let activePlaylistIdSignal: ReturnType>; - let originalElectron: unknown; + let runtimeCapabilities: { supportsXtreamSectionNavigation: boolean }; const xtreamA = createPlaylist({ _id: 'xtream-a', @@ -82,13 +83,6 @@ describe('PlaylistContextFacade', () => { url: 'http://example.test/b.m3u', }); - function setElectronAvailability(enabled: boolean): void { - Object.defineProperty(window, 'electron', { - configurable: true, - value: enabled ? {} : undefined, - }); - } - function instantiateFacade(): PlaylistContextFacade { facade = TestBed.inject(PlaylistContextFacade); dispatch.mockClear(); @@ -98,7 +92,6 @@ describe('PlaylistContextFacade', () => { beforeEach(() => { localStorage.clear(); - originalElectron = (window as Window & { electron?: unknown }).electron; routerEvents = new Subject(); router = { @@ -117,7 +110,7 @@ describe('PlaylistContextFacade', () => { ]); loadedSignal = signal(true); activePlaylistIdSignal = signal(xtreamA._id); - setElectronAvailability(true); + runtimeCapabilities = { supportsXtreamSectionNavigation: true }; TestBed.configureTestingModule({ providers: [ @@ -147,16 +140,16 @@ describe('PlaylistContextFacade', () => { }), }, }, + { + provide: RuntimeCapabilitiesService, + useValue: runtimeCapabilities, + }, ], }); }); afterEach(() => { localStorage.clear(); - Object.defineProperty(window, 'electron', { - configurable: true, - value: originalElectron, - }); routerEvents.complete(); jest.restoreAllMocks(); }); @@ -353,8 +346,8 @@ describe('PlaylistContextFacade', () => { ).toEqual(['workspace', 'stalker', stalkerB._id, 'search']); }); - it('omits Xtream sections outside Electron where section navigation is unsupported', () => { - setElectronAvailability(false); + it('omits Xtream sections when runtime section navigation is unsupported', () => { + runtimeCapabilities.supportsXtreamSectionNavigation = false; const service = instantiateFacade(); expect( diff --git a/libs/playlist/shared/util/src/lib/playlist-context.facade.ts b/libs/playlist/shared/util/src/lib/playlist-context.facade.ts index ead6e3b87..696fdd871 100644 --- a/libs/playlist/shared/util/src/lib/playlist-context.facade.ts +++ b/libs/playlist/shared/util/src/lib/playlist-context.facade.ts @@ -21,6 +21,7 @@ import { PortalProvider, PortalRailSection, } from '@iptvnator/portal/shared/util'; +import { RuntimeCapabilitiesService } from '@iptvnator/services'; export interface PlaylistRouteContext { inWorkspace: boolean; @@ -69,6 +70,7 @@ const M3U_SECTIONS = ['all', 'groups', 'favorites', 'recent'] as const; @Injectable({ providedIn: 'root' }) export class PlaylistContextFacade { private readonly destroyRef = inject(DestroyRef); + private readonly runtime = inject(RuntimeCapabilitiesService); private readonly router = inject(Router); private readonly store = inject(Store); @@ -346,7 +348,7 @@ export class PlaylistContextFacade { private supportsSectionNavigation(provider: PortalProvider): boolean { if (provider === 'xtreams') { - return Boolean(window.electron); + return this.runtime.supportsXtreamSectionNavigation; } return true; diff --git a/libs/services/src/lib/runtime-capabilities.service.spec.ts b/libs/services/src/lib/runtime-capabilities.service.spec.ts index 1f19d63e9..a9d8ec726 100644 --- a/libs/services/src/lib/runtime-capabilities.service.spec.ts +++ b/libs/services/src/lib/runtime-capabilities.service.spec.ts @@ -28,6 +28,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEmbeddedMpv).toBe(false); expect(service.supportsDesktopFileSave).toBe(false); expect(service.supportsRemoteControl).toBe(false); + expect(service.supportsXtreamSectionNavigation).toBe(false); }); it('reports Electron capabilities from the available preload bridge methods', () => { @@ -66,6 +67,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEmbeddedMpv).toBe(true); expect(service.supportsDesktopFileSave).toBe(true); expect(service.supportsRemoteControl).toBe(true); + expect(service.supportsXtreamSectionNavigation).toBe(true); }); it('keeps feature-specific capabilities false when an Electron bridge is partial', () => { @@ -85,6 +87,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEmbeddedMpv).toBe(false); expect(service.supportsDesktopFileSave).toBe(false); expect(service.supportsRemoteControl).toBe(false); + expect(service.supportsXtreamSectionNavigation).toBe(true); }); it('reads the bridge dynamically so tests and late preload setup stay accurate', () => { diff --git a/libs/services/src/lib/runtime-capabilities.service.ts b/libs/services/src/lib/runtime-capabilities.service.ts index 6bfd7e921..ec6d9bcca 100644 --- a/libs/services/src/lib/runtime-capabilities.service.ts +++ b/libs/services/src/lib/runtime-capabilities.service.ts @@ -81,6 +81,10 @@ export class RuntimeCapabilitiesService { ); } + get supportsXtreamSectionNavigation(): boolean { + return this.isElectron; + } + private hasElectronMethod(methodName: string): boolean { return typeof this.electronBridge?.[methodName] === 'function'; }