diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index 649fca38c..f7f435542 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -48,7 +48,7 @@ [players]="players()" [streamFormatEnum]="streamFormatEnum" [isDesktop]="isDesktop" - [supportsManagedExternalPlayers]="supportsManagedExternalPlayers" + [supportsManagedExternalPlayers]="supportsExternalPlayerPathSettings" (selectRecordingFolder)="selectRecordingFolder()" /> diff --git a/apps/web/src/app/settings/settings.component.spec.ts b/apps/web/src/app/settings/settings.component.spec.ts index c861510e6..b6b1dff38 100644 --- a/apps/web/src/app/settings/settings.component.spec.ts +++ b/apps/web/src/app/settings/settings.component.spec.ts @@ -934,10 +934,11 @@ describe('SettingsComponent', () => { const clickSpy = jest .spyOn(HTMLAnchorElement.prototype, 'click') .mockImplementation(); + let partialFileSaveFixture: ComponentFixture | null = + null; try { - const partialFileSaveFixture = - TestBed.createComponent(SettingsComponent); + partialFileSaveFixture = TestBed.createComponent(SettingsComponent); const partialFileSaveComponent = partialFileSaveFixture.componentInstance; partialFileSaveComponent.checkAppVersion = jest.fn(); @@ -958,6 +959,7 @@ describe('SettingsComponent', () => { expect(clickSpy).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:backup'); } finally { + partialFileSaveFixture?.destroy(); clickSpy.mockRestore(); Object.defineProperty(window.URL, 'createObjectURL', { configurable: true, diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 13c26f4fa..ba7b9b480 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -612,7 +612,7 @@ export class SettingsComponent implements OnInit, OnDestroy { if (!this.supportsEpg || !url) { return; } - void window.electron!.forceFetchEpg!(url); + void window.electron.forceFetchEpg(url); } /** @@ -625,7 +625,7 @@ export class SettingsComponent implements OnInit, OnDestroy { const urls = (this.epgUrl.value as string[]) .map((url) => url?.trim()) .filter((url): url is string => Boolean(url)); - urls.forEach((url) => void window.electron!.forceFetchEpg!(url)); + urls.forEach((url) => void window.electron.forceFetchEpg(url)); } /** @@ -666,7 +666,7 @@ export class SettingsComponent implements OnInit, OnDestroy { this.isClearingEpgData.set(true); try { - const result = await window.electron!.clearEpgData!(); + const result = await window.electron.clearEpgData(); if (result && result.success === false) { throw new Error('Clear EPG returned success=false'); } diff --git a/libs/epg/data-access/src/lib/epg-progress.service.spec.ts b/libs/epg/data-access/src/lib/epg-progress.service.spec.ts index 77b60de45..746d0a4f4 100644 --- a/libs/epg/data-access/src/lib/epg-progress.service.spec.ts +++ b/libs/epg/data-access/src/lib/epg-progress.service.spec.ts @@ -58,6 +58,22 @@ describe('EpgProgressService', () => { expect(forceFetchEpg).not.toHaveBeenCalled(); }); + it('forces retry through the Electron bridge when runtime EPG support is enabled', () => { + const forceFetchEpg = jest.fn(); + window.electron = { + ...window.electron, + forceFetchEpg, + } as unknown as typeof window.electron; + runtimeCapabilities.supportsEpg = true; + const service = configureService(); + + service.retry('https://example.com/epg.xml'); + + expect(forceFetchEpg).toHaveBeenCalledWith( + 'https://example.com/epg.xml' + ); + }); + it('updates imports from Electron progress events when runtime EPG support is enabled', () => { let listener: ((progress: EpgImportProgress) => void) | undefined; window.electron = { diff --git a/libs/epg/data-access/src/lib/epg-progress.service.ts b/libs/epg/data-access/src/lib/epg-progress.service.ts index a3307c4d9..6c19f88e2 100644 --- a/libs/epg/data-access/src/lib/epg-progress.service.ts +++ b/libs/epg/data-access/src/lib/epg-progress.service.ts @@ -53,7 +53,7 @@ export class EpgProgressService { if (!this.runtime.supportsEpg) { return; } - void window.electron?.forceFetchEpg?.(url); + void window.electron.forceFetchEpg(url); } private initializeListener(): void { diff --git a/libs/epg/data-access/src/lib/epg.service.spec.ts b/libs/epg/data-access/src/lib/epg.service.spec.ts index cbdfeb941..661c0df3c 100644 --- a/libs/epg/data-access/src/lib/epg.service.spec.ts +++ b/libs/epg/data-access/src/lib/epg.service.spec.ts @@ -54,6 +54,26 @@ describe('EpgService', () => { expect(fetchEpg).not.toHaveBeenCalled(); }); + it('fetches EPG through the Electron bridge when runtime EPG support is enabled', () => { + const fetchEpg = jest.fn().mockResolvedValue({ success: true }); + window.electron = { + ...window.electron, + fetchEpg, + } as unknown as typeof window.electron; + runtimeCapabilities.supportsEpg = true; + + service.fetchEpg([ + 'https://example.com/epg.xml', + '', + 'https://example.com/other.xml', + ]); + + expect(fetchEpg).toHaveBeenCalledWith([ + 'https://example.com/epg.xml', + 'https://example.com/other.xml', + ]); + }); + it('returns an empty batch result when the desktop bridge is unavailable', async () => { const result = await firstValueFrom( service.getCurrentProgramsForChannels(['channel-1']) diff --git a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts index 7a92a7c94..02584545e 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts @@ -6,6 +6,7 @@ import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@iptvnator/ui/components'; import { + DataService, DatabaseService, DbOperationEvent, PlaybackPositionService, @@ -13,7 +14,11 @@ import { RuntimeCapabilitiesService, } from '@iptvnator/services'; import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state'; -import { Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces'; +import { + PLAYLIST_UPDATE, + Playlist, + PlaylistMeta, +} from '@iptvnator/shared/interfaces'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; import { PlaylistRefreshActionService } from './playlist-refresh-action.service'; @@ -58,6 +63,9 @@ describe('PlaylistRefreshActionService', () => { deleteXtreamPlaylistContent: jest.Mock; updateXtreamPlaylistDetails: jest.Mock; }; + let dataService: { + sendIpcEvent: jest.Mock; + }; let dialogService: { openConfirmDialog: jest.Mock; }; @@ -113,6 +121,9 @@ describe('PlaylistRefreshActionService', () => { }), updateXtreamPlaylistDetails: jest.fn().mockResolvedValue(true), }; + dataService = { + sendIpcEvent: jest.fn(), + }; dialogService = { openConfirmDialog: jest.fn(), }; @@ -167,6 +178,10 @@ describe('PlaylistRefreshActionService', () => { provide: DatabaseService, useValue: databaseService, }, + { + provide: DataService, + useValue: dataService, + }, { provide: PlaylistRefreshService, useValue: playlistRefreshService, @@ -227,6 +242,45 @@ describe('PlaylistRefreshActionService', () => { ).toBe(false); }); + it('treats URL-backed M3U playlists as refreshable without the refresh bridge', () => { + runtime.supportsPlaylistRefresh = false; + + expect( + service.canRefresh( + createPlaylistMeta({ + serverUrl: undefined, + username: undefined, + password: undefined, + url: 'https://example.com/playlist.m3u', + }) + ) + ).toBe(true); + }); + + it('uses the browser URL refresh path when the refresh bridge is unavailable', () => { + runtime.supportsPlaylistRefresh = false; + const playlist = createPlaylistMeta({ + _id: 'playlist-url', + title: 'URL playlist', + serverUrl: undefined, + username: undefined, + password: undefined, + url: 'https://example.com/playlist.m3u', + }); + + service.refresh(playlist); + + expect(dataService.sendIpcEvent).toHaveBeenCalledWith( + PLAYLIST_UPDATE, + { + id: 'playlist-url', + title: 'URL playlist', + url: 'https://example.com/playlist.m3u', + } + ); + expect(playlistRefreshService.refreshPlaylist).not.toHaveBeenCalled(); + }); + it('treats Xtream playlists as refreshable only when the SQLite data source is available', () => { runtime.supportsXtreamSqliteDataSource = true; diff --git a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts index aa5a5b099..03c800bd9 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts @@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DialogService } from '@iptvnator/ui/components'; import { + DataService, DatabaseService, type DbOperationEvent, isDbAbortError, @@ -14,7 +15,7 @@ import { XtreamPendingRestoreService, } from '@iptvnator/services'; import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state'; -import { PlaylistMeta } from '@iptvnator/shared/interfaces'; +import { PLAYLIST_UPDATE, PlaylistMeta } from '@iptvnator/shared/interfaces'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; export interface XtreamRefreshPreparationState { @@ -33,6 +34,7 @@ export class PlaylistRefreshActionService { private readonly snackBar = inject(MatSnackBar); private readonly dialogService = inject(DialogService); private readonly databaseService = inject(DatabaseService); + private readonly dataService = inject(DataService); private readonly playbackPositionService = inject(PlaybackPositionService); private readonly playlistRefreshService = inject(PlaylistRefreshService); private readonly runtime = inject(RuntimeCapabilitiesService); @@ -56,10 +58,11 @@ export class PlaylistRefreshActionService { return this.runtime.supportsXtreamSqliteDataSource; } - return ( - this.runtime.supportsPlaylistRefresh && - Boolean(playlist.url || playlist.filePath) - ); + if (playlist.url) { + return true; + } + + return this.runtime.supportsPlaylistRefresh && Boolean(playlist.filePath); } refresh(playlist: PlaylistMeta): void { @@ -74,6 +77,12 @@ export class PlaylistRefreshActionService { (playlist.url || playlist.filePath) ) { void this.refreshM3u(playlist); + } else if (playlist.url) { + this.dataService.sendIpcEvent(PLAYLIST_UPDATE, { + id: playlist._id, + title: playlist.title, + url: playlist.url, + }); } } diff --git a/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts b/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts index 1db243c50..2cd8c0b63 100644 --- a/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts +++ b/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts @@ -729,7 +729,7 @@ export class UnifiedFavoritesDataService { } private async getSavedOrder(): Promise { - if (!this.runtime.hasElectronMethod('dbGetAppState')) return []; + if (!this.runtime.supportsAppStateStorage) return []; try { const raw = await window.electron?.dbGetAppState( GLOBAL_FAVORITES_ORDER_KEY @@ -741,7 +741,7 @@ export class UnifiedFavoritesDataService { } private async saveOrder(uidOrder: string[]): Promise { - if (!this.runtime.hasElectronMethod('dbSetAppState')) return; + if (!this.runtime.supportsAppStateStorage) return; try { await window.electron?.dbSetAppState( GLOBAL_FAVORITES_ORDER_KEY, diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts index b35b5fc81..30225b006 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts @@ -32,7 +32,7 @@ describe('withStalkerPortal', () => { let dbCreatePlaylist: jest.Mock; let dbGetPlaylist: jest.Mock; let runtime: { - hasElectronMethod: jest.Mock; + supportsStalkerPlaylistSqliteSync: boolean; }; let stalkerSession: { ensureToken: jest.Mock; @@ -51,7 +51,7 @@ describe('withStalkerPortal', () => { }); runtime = { - hasElectronMethod: jest.fn(() => true), + supportsStalkerPlaylistSqliteSync: true, }; stalkerSession = { ensureToken: jest.fn(), @@ -101,9 +101,7 @@ describe('withStalkerPortal', () => { }); it('does not touch SQLite when the Electron bridge is partial', async () => { - runtime.hasElectronMethod.mockImplementation( - (methodName) => methodName === 'dbGetPlaylist' - ); + runtime.supportsStalkerPlaylistSqliteSync = false; await store.setCurrentPlaylist(PLAYLIST); diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts index e77e61460..16c8cd06c 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts @@ -13,15 +13,15 @@ import { StalkerSessionService } from '../../stalker-session.service'; import { toStalkerSessionPlaylist } from '../utils'; type StalkerPortalWindow = Window & { - electron?: { - dbCreatePlaylist?: (playlist: { + electron: { + dbCreatePlaylist: (playlist: { id: string; name: string; macAddress: string; url: string; type: 'stalker'; }) => Promise; - dbGetPlaylist?: (playlistId: string) => Promise; + dbGetPlaylist: (playlistId: string) => Promise; }; }; @@ -96,8 +96,7 @@ export function withStalkerPortal() { // Only sync if this is actually a Stalker playlist (has macAddress and portalUrl) if ( playlist && - runtime.hasElectronMethod('dbGetPlaylist') && - runtime.hasElectronMethod('dbCreatePlaylist') && + runtime.supportsStalkerPlaylistSqliteSync && playlist._id && playlist.macAddress && playlist.portalUrl @@ -105,12 +104,6 @@ export function withStalkerPortal() { try { const electronApi = (window as StalkerPortalWindow) .electron; - if ( - !electronApi?.dbGetPlaylist || - !electronApi.dbCreatePlaylist - ) { - return; - } const playlistId = String(playlist._id); // Check if playlist exists in SQLite diff --git a/libs/services/src/lib/runtime-capabilities.service.spec.ts b/libs/services/src/lib/runtime-capabilities.service.spec.ts index faf5e07ec..3215e7c93 100644 --- a/libs/services/src/lib/runtime-capabilities.service.spec.ts +++ b/libs/services/src/lib/runtime-capabilities.service.spec.ts @@ -25,6 +25,8 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsXtreamSqliteDataSource).toBe(false); expect(service.supportsDownloads).toBe(false); expect(service.supportsPortalActivityStorage).toBe(false); + expect(service.supportsAppStateStorage).toBe(false); + expect(service.supportsStalkerPlaylistSqliteSync).toBe(false); expect(service.supportsPlaylistRefresh).toBe(false); expect(service.supportsManagedExternalPlayers).toBe(false); expect(service.supportsExternalPlayerPathSettings).toBe(false); @@ -128,6 +130,8 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsXtreamSqliteDataSource).toBe(true); expect(service.supportsDownloads).toBe(true); expect(service.supportsPortalActivityStorage).toBe(true); + expect(service.supportsAppStateStorage).toBe(true); + expect(service.supportsStalkerPlaylistSqliteSync).toBe(true); expect(service.supportsPlaylistRefresh).toBe(true); expect(service.supportsManagedExternalPlayers).toBe(true); expect(service.supportsExternalPlayerPathSettings).toBe(true); @@ -152,6 +156,8 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsXtreamSqliteDataSource).toBe(false); expect(service.supportsDownloads).toBe(false); expect(service.supportsPortalActivityStorage).toBe(false); + expect(service.supportsAppStateStorage).toBe(false); + expect(service.supportsStalkerPlaylistSqliteSync).toBe(false); expect(service.supportsPlaylistRefresh).toBe(false); expect(service.supportsManagedExternalPlayers).toBe(false); expect(service.supportsExternalPlayerPathSettings).toBe(false); @@ -179,6 +185,45 @@ describe('RuntimeCapabilitiesService', () => { const service = new RuntimeCapabilitiesService(); expect(service.supportsSqlite).toBe(true); + expect(service.supportsAppStateStorage).toBe(true); + expect(service.supportsXtreamSqliteDataSource).toBe(false); + }); + + it('checks app state storage without requiring full playlist SQLite support', () => { + testWindow.electron = { + dbGetAppState: jest.fn(), + }; + + const service = new RuntimeCapabilitiesService(); + + expect(service.isElectron).toBe(true); + expect(service.supportsAppStateStorage).toBe(false); + + testWindow.electron = { + dbGetAppState: jest.fn(), + dbSetAppState: jest.fn(), + }; + + expect(service.supportsAppStateStorage).toBe(true); + expect(service.supportsSqlite).toBe(false); + }); + + it('checks Stalker playlist SQLite sync without requiring the full Xtream data source', () => { + testWindow.electron = { + dbGetPlaylist: jest.fn(), + }; + + const service = new RuntimeCapabilitiesService(); + + expect(service.isElectron).toBe(true); + expect(service.supportsStalkerPlaylistSqliteSync).toBe(false); + + testWindow.electron = { + dbGetPlaylist: jest.fn(), + dbCreatePlaylist: jest.fn(), + }; + + expect(service.supportsStalkerPlaylistSqliteSync).toBe(true); expect(service.supportsXtreamSqliteDataSource).toBe(false); }); @@ -324,6 +369,16 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsXtreamSqliteDataSource).toBe(false); expect(service.supportsXtreamSectionNavigation).toBe(true); }); + + it('supports Xtream section navigation in Electron when only the SQLite data source is available', () => { + testWindow.electron = createXtreamSqliteBridge(); + + const service = new RuntimeCapabilitiesService(); + + expect(service.isElectron).toBe(true); + expect(service.supportsXtreamSqliteDataSource).toBe(true); + expect(service.supportsXtreamSectionNavigation).toBe(true); + }); }); function createPlaylistStorageBridge(): Record { @@ -338,3 +393,41 @@ function createPlaylistStorageBridge(): Record { dbUpsertAppPlaylists: jest.fn(), }; } + +function createXtreamSqliteBridge(): Record { + return { + dbAddFavorite: jest.fn(), + dbAddRecentItem: jest.fn(), + dbClearAllPlaybackPositions: jest.fn(), + dbClearPlaybackPosition: jest.fn(), + dbClearPlaylistRecentItems: jest.fn(), + dbCreatePlaylist: jest.fn(), + dbDeletePlaylist: jest.fn(), + dbDeleteXtreamContent: jest.fn(), + dbGetAllCategories: jest.fn(), + dbGetAllPlaybackPositions: jest.fn(), + dbGetAppState: jest.fn(), + dbGetCategories: jest.fn(), + dbGetContent: jest.fn(), + dbGetContentByXtreamId: jest.fn(), + dbGetFavorites: jest.fn(), + dbGetPlaylist: jest.fn(), + dbGetPlaybackPosition: jest.fn(), + dbGetRecentItems: jest.fn(), + dbGetRecentPlaybackPositions: jest.fn(), + dbGetSeriesPlaybackPositions: jest.fn(), + dbHasCategories: jest.fn(), + dbHasContent: jest.fn(), + dbIsFavorite: jest.fn(), + dbRemoveFavorite: jest.fn(), + dbRemoveRecentItem: jest.fn(), + dbRestoreXtreamUserData: jest.fn(), + dbSaveCategories: jest.fn(), + dbSaveContent: jest.fn(), + dbSavePlaybackPosition: jest.fn(), + dbSearchContent: jest.fn(), + dbSetAppState: jest.fn(), + dbUpdateCategoryVisibility: jest.fn(), + dbUpdatePlaylist: jest.fn(), + }; +} diff --git a/libs/services/src/lib/runtime-capabilities.service.ts b/libs/services/src/lib/runtime-capabilities.service.ts index 374a70197..9832ff6d9 100644 --- a/libs/services/src/lib/runtime-capabilities.service.ts +++ b/libs/services/src/lib/runtime-capabilities.service.ts @@ -130,6 +130,18 @@ export class RuntimeCapabilitiesService { ].every((methodName) => this.hasElectronMethod(methodName)); } + get supportsAppStateStorage(): boolean { + return ['dbGetAppState', 'dbSetAppState'].every((methodName) => + this.hasElectronMethod(methodName) + ); + } + + get supportsStalkerPlaylistSqliteSync(): boolean { + return ['dbGetPlaylist', 'dbCreatePlaylist'].every((methodName) => + this.hasElectronMethod(methodName) + ); + } + get supportsPlaylistRefresh(): boolean { return [ 'refreshPlaylist', diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts index d2f1a1d3e..93c7e4ec3 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts @@ -195,7 +195,9 @@ describe('ChannelListContainerComponent', () => { it('does not enable EPG rows when runtime EPG support is unavailable', () => { runtimeCapabilities.supportsEpg = false; - storageGet.mockReturnValue(of({ epgUrl: ['https://example.com/epg.xml'] })); + storageGet.mockReturnValue( + of({ epgUrl: ['https://example.com/epg.xml'] }) + ); fixture.detectChanges(); @@ -203,6 +205,18 @@ describe('ChannelListContainerComponent', () => { expect(storageGet).not.toHaveBeenCalled(); }); + it('enables EPG rows when runtime EPG support and an EPG URL are available', () => { + runtimeCapabilities.supportsEpg = true; + storageGet.mockReturnValue( + of({ epgUrl: ['https://example.com/epg.xml'] }) + ); + + fixture.detectChanges(); + + expect(storageGet).toHaveBeenCalled(); + expect(fixture.componentInstance.shouldShowEpg()).toBe(true); + }); + it('dispatches playlist meta updates when hidden group titles change', () => { fixture.componentInstance.onHiddenGroupTitlesChanged([ 'Movies', diff --git a/libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts b/libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts index a33f019e6..910fff2d4 100644 --- a/libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts +++ b/libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts @@ -80,7 +80,7 @@ export class EpgSourceStatusComponent implements OnInit { } async ngOnInit(): Promise { - if (!this.runtime.supportsEpg || !window.electron?.checkEpgFreshness) { + if (!this.runtime.supportsEpg) { return; } const url = this.url(); diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts index 4232a97e1..7e57fab02 100644 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts +++ b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts @@ -85,6 +85,26 @@ describe('MultiEpgContainerComponent runtime gates', () => { expect(component.isLoading()).toBe(false); }); + it('requests EPG channel ranges when runtime EPG support is enabled', async () => { + const getEpgChannelsByRange = jest.fn().mockResolvedValue([ + { + channel_id: 'channel-1', + display_name: 'Channel One', + programs: [], + }, + ]); + window.electron = { + ...window.electron, + getEpgChannelsByRange, + } as unknown as typeof window.electron; + runtimeCapabilities.supportsEpg = true; + + await component.requestPrograms(); + + expect(getEpgChannelsByRange).toHaveBeenCalledWith(0, 20); + expect(component.isLoading()).toBe(false); + }); + it('does not search EPG programs when runtime EPG support is disabled', () => { jest.useFakeTimers(); const searchEpgPrograms = jest.fn().mockResolvedValue([]); @@ -102,4 +122,32 @@ describe('MultiEpgContainerComponent runtime gates', () => { expect(component.isSearchingPrograms()).toBe(false); expect(component.programSearchResults()).toEqual([]); }); + + it('searches EPG programs when runtime EPG support is enabled', async () => { + jest.useFakeTimers(); + const results = [ + { + channelId: 'channel-1', + start: '2026-05-22T10:00:00.000Z', + stop: '2026-05-22T11:00:00.000Z', + title: 'News', + }, + ]; + const searchEpgPrograms = jest.fn().mockResolvedValue(results); + window.electron = { + ...window.electron, + searchEpgPrograms, + } as unknown as typeof window.electron; + runtimeCapabilities.supportsEpg = true; + + component.onProgramSearchInput({ + target: { value: 'news' }, + } as unknown as Event); + jest.advanceTimersByTime(500); + await Promise.resolve(); + + expect(searchEpgPrograms).toHaveBeenCalledWith('news', 20); + expect(component.programSearchResults()).toEqual(results); + expect(component.isSearchingPrograms()).toBe(false); + }); }); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts index f282beb3d..52ff2a1d5 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts @@ -697,8 +697,7 @@ describe('WorkspaceShellFacade', () => { }); it('hides the downloads command when downloads are unsupported', () => { - (facade as unknown as { supportsDownloads: boolean }).supportsDownloads = - false; + runtime.supportsDownloads = false; activePlaylistSignal.set(null); playlistsSignal.set([]); facade.currentUrl.set('/workspace/dashboard');