Merge pull request #977 from 4gray/agent/playlist-context-runtime-capabilities

refactor(playlist): use runtime capability for xtream sections
This commit is contained in:
4gray authored and GitHub committed 2026-05-22 12:41:17 +03:00
commit 2db1a56007
4 files changed
+19 -17

No files matched your search

@@ -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<typeof signal<PlaylistMeta[]>>;
let loadedSignal: ReturnType<typeof signal<boolean>>;
let activePlaylistIdSignal: ReturnType<typeof signal<string | null>>;
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<NavigationEnd>();
router = {
@@ -117,7 +110,7 @@ describe('PlaylistContextFacade', () => {
]);
loadedSignal = signal(true);
activePlaylistIdSignal = signal<string | null>(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(
@@ -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;
@@ -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', () => {
@@ -81,6 +81,10 @@ export class RuntimeCapabilitiesService {
);
}
get supportsXtreamSectionNavigation(): boolean {
return this.isElectron;
}
private hasElectronMethod(methodName: string): boolean {
return typeof this.electronBridge?.[methodName] === 'function';
}