import { RuntimeCapabilitiesService } from './runtime-capabilities.service'; describe('RuntimeCapabilitiesService', () => { const testWindow = window as unknown as { electron?: Record; }; const originalElectron = testWindow.electron; afterEach(() => { testWindow.electron = originalElectron; }); it('reports browser PWA capabilities when the Electron bridge is absent', () => { testWindow.electron = undefined; const service = new RuntimeCapabilitiesService(); expect(service.environment).toBe('pwa'); expect(service.isPwa).toBe(true); expect(service.isElectron).toBe(false); expect(service.platform).toBeUndefined(); expect(service.isMacOS).toBe(false); expect(service.supportsEpg).toBe(false); expect(service.supportsSqlite).toBe(false); expect(service.supportsXtreamSqliteDataSource).toBe(false); expect(service.supportsDownloads).toBe(false); expect(service.supportsPortalActivityStorage).toBe(false); expect(service.supportsPlaybackPositionStorage).toBe(false); expect(service.supportsPlaybackPositionUpdates).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); expect(service.supportsEmbeddedMpv).toBe(false); expect(service.supportsDesktopFileSave).toBe(false); expect(service.supportsRemoteControl).toBe(false); expect(service.supportsXtreamSectionNavigation).toBe(true); expect(service.supportsEpgImport).toBe(false); expect(service.supportsEpgProgress).toBe(false); expect(service.supportsEpgProgramLookup).toBe(false); expect(service.supportsEpgCurrentProgramBatch).toBe(false); expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); expect(service.supportsEpgChannelBrowser).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); it('reports Electron capabilities from the available preload bridge methods', () => { testWindow.electron = { platform: 'darwin', dbDeleteAllPlaylists: jest.fn(), dbDeletePlaylist: jest.fn(), dbGetAppPlaylist: jest.fn(), dbGetAppPlaylists: jest.fn(), dbUpsertAppPlaylist: jest.fn(), dbUpsertAppPlaylists: jest.fn(), dbGetAppState: jest.fn(), dbSetAppState: jest.fn(), dbGetRecentlyViewed: jest.fn(), dbClearRecentlyViewed: jest.fn(), dbGetAllGlobalFavorites: jest.fn(), dbGetGlobalRecentlyAdded: jest.fn(), dbAddFavorite: jest.fn(), dbRemoveRecentItem: jest.fn(), dbRemoveFavorite: jest.fn(), dbGetFavorites: jest.fn(), dbReorderGlobalFavorites: jest.fn(), dbGetRecentItems: jest.fn(), dbAddRecentItem: jest.fn(), dbClearPlaylistRecentItems: jest.fn(), dbRemoveRecentItemsBatch: jest.fn(), dbGetContentByXtreamId: jest.fn(), dbGetPlaylist: jest.fn(), dbCreatePlaylist: jest.fn(), dbUpdatePlaylist: jest.fn(), dbHasCategories: jest.fn(), dbGetCategories: jest.fn(), dbSaveCategories: jest.fn(), dbGetAllCategories: jest.fn(), dbUpdateCategoryVisibility: jest.fn(), dbHasContent: jest.fn(), dbGetContent: jest.fn(), dbSaveContent: jest.fn(), dbSearchContent: jest.fn(), dbIsFavorite: jest.fn(), dbSavePlaybackPosition: jest.fn(), dbGetPlaybackPosition: jest.fn(), dbGetSeriesPlaybackPositions: jest.fn(), dbGetRecentPlaybackPositions: jest.fn(), dbGetAllPlaybackPositions: jest.fn(), dbClearAllPlaybackPositions: jest.fn(), dbClearPlaybackPosition: jest.fn(), onPlaybackPositionUpdate: jest.fn(), dbDeleteXtreamContent: jest.fn(), dbRestoreXtreamUserData: jest.fn(), downloadsStart: jest.fn(), downloadsCancel: jest.fn(), downloadsPause: jest.fn(), downloadsResume: jest.fn(), downloadsRetry: jest.fn(), downloadsRemove: jest.fn(), downloadsGetList: jest.fn(), downloadsGet: jest.fn(), downloadsGetDefaultFolder: jest.fn(), downloadsSelectFolder: jest.fn(), downloadsRevealFile: jest.fn(), downloadsPlayFile: jest.fn(), downloadsClearCompleted: jest.fn(), onDownloadsUpdate: jest.fn(), refreshPlaylist: jest.fn(), cancelPlaylistRefresh: jest.fn(), onPlaylistRefreshEvent: jest.fn(), openInMpv: jest.fn(), openInVlc: jest.fn(), setMpvPlayerPath: jest.fn(), setVlcPlayerPath: jest.fn(), prepareEmbeddedMpv: jest.fn(), saveFileDialog: jest.fn(), writeFile: jest.fn(), updateRemoteControlStatus: jest.fn(), onChannelChange: jest.fn(), onRemoteControlCommand: jest.fn(), xtreamRequest: jest.fn(), fetchEpg: jest.fn(), getChannelPrograms: jest.fn(), getCurrentProgramsBatch: jest.fn(), getEpgChannelMetadata: jest.fn(), checkEpgFreshness: jest.fn(), onEpgProgress: jest.fn(), forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), getEpgChannelsByRange: jest.fn(), searchEpgPrograms: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.environment).toBe('electron'); expect(service.isElectron).toBe(true); expect(service.isPwa).toBe(false); expect(service.platform).toBe('darwin'); expect(service.isMacOS).toBe(true); expect(service.supportsEpg).toBe(true); expect(service.supportsSqlite).toBe(true); expect(service.supportsXtreamSqliteDataSource).toBe(true); expect(service.supportsDownloads).toBe(true); expect(service.supportsPortalActivityStorage).toBe(true); expect(service.supportsPlaybackPositionStorage).toBe(true); expect(service.supportsPlaybackPositionUpdates).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); expect(service.supportsEmbeddedMpv).toBe(true); expect(service.supportsDesktopFileSave).toBe(true); expect(service.supportsRemoteControl).toBe(true); expect(service.supportsXtreamSectionNavigation).toBe(true); expect(service.supportsEpgImport).toBe(true); expect(service.supportsEpgProgress).toBe(true); expect(service.supportsEpgProgramLookup).toBe(true); expect(service.supportsEpgCurrentProgramBatch).toBe(true); expect(service.supportsEpgChannelMetadata).toBe(true); expect(service.supportsEpgSourceFreshness).toBe(true); expect(service.supportsEpgDataManagement).toBe(true); expect(service.supportsEpgChannelBrowser).toBe(true); expect(service.supportsEpgProgramSearch).toBe(true); }); it('keeps feature-specific capabilities false when an Electron bridge is partial', () => { testWindow.electron = { updateRemoteControlStatus: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.environment).toBe('electron'); expect(service.platform).toBeUndefined(); expect(service.isMacOS).toBe(false); expect(service.supportsEpg).toBe(false); expect(service.supportsSqlite).toBe(false); expect(service.supportsXtreamSqliteDataSource).toBe(false); expect(service.supportsDownloads).toBe(false); expect(service.supportsPortalActivityStorage).toBe(false); expect(service.supportsPlaybackPositionStorage).toBe(false); expect(service.supportsPlaybackPositionUpdates).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); expect(service.supportsEmbeddedMpv).toBe(false); expect(service.supportsDesktopFileSave).toBe(false); expect(service.supportsRemoteControl).toBe(false); expect(service.supportsXtreamSectionNavigation).toBe(false); expect(service.supportsEpgImport).toBe(false); expect(service.supportsEpgProgress).toBe(false); expect(service.supportsEpgProgramLookup).toBe(false); expect(service.supportsEpgCurrentProgramBatch).toBe(false); expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); expect(service.supportsEpgChannelBrowser).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); it('reads the bridge dynamically so tests and late preload setup stay accurate', () => { testWindow.electron = undefined; const service = new RuntimeCapabilitiesService(); expect(service.isPwa).toBe(true); testWindow.electron = {}; expect(service.isElectron).toBe(true); expect(service.environment).toBe('electron'); }); it('keeps the Xtream SQLite data source disabled when only generic SQLite methods exist', () => { testWindow.electron = createPlaylistStorageBridge(); 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); }); it('decouples external player launch support from path-setting support', () => { testWindow.electron = { openInMpv: jest.fn(), openInVlc: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsManagedExternalPlayers).toBe(true); expect(service.supportsExternalPlayerPathSettings).toBe(false); testWindow.electron = { openInMpv: jest.fn(), openInVlc: jest.fn(), setMpvPlayerPath: jest.fn(), setVlcPlayerPath: jest.fn(), }; expect(service.supportsManagedExternalPlayers).toBe(true); expect(service.supportsExternalPlayerPathSettings).toBe(true); }); it('requires the complete EPG preload surface', () => { testWindow.electron = { fetchEpg: jest.fn(), getChannelPrograms: jest.fn(), checkEpgFreshness: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsEpg).toBe(false); testWindow.electron = { fetchEpg: jest.fn(), getChannelPrograms: jest.fn(), checkEpgFreshness: jest.fn(), forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), getEpgChannelsByRange: jest.fn(), searchEpgPrograms: jest.fn(), }; expect(service.supportsEpg).toBe(true); }); it('exposes EPG capabilities by the specific preload surface they need', () => { testWindow.electron = { fetchEpg: jest.fn(), onEpgProgress: jest.fn(), getChannelPrograms: jest.fn(), checkEpgFreshness: jest.fn(), forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), getEpgChannelsByRange: jest.fn(), searchEpgPrograms: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.supportsEpg).toBe(true); expect(service.supportsEpgImport).toBe(true); expect(service.supportsEpgProgress).toBe(true); expect(service.supportsEpgProgramLookup).toBe(true); expect(service.supportsEpgCurrentProgramBatch).toBe(false); expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(true); expect(service.supportsEpgDataManagement).toBe(true); expect(service.supportsEpgChannelBrowser).toBe(true); expect(service.supportsEpgProgramSearch).toBe(true); testWindow.electron = { checkEpgFreshness: jest.fn(), }; expect(service.supportsEpg).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(true); expect(service.supportsEpgImport).toBe(false); expect(service.supportsEpgProgramLookup).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); expect(service.supportsEpgChannelBrowser).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); it('requires the complete downloads preload surface', () => { testWindow.electron = { downloadsGetList: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsDownloads).toBe(false); testWindow.electron = { downloadsStart: jest.fn(), downloadsCancel: jest.fn(), downloadsPause: jest.fn(), downloadsResume: jest.fn(), downloadsRetry: jest.fn(), downloadsRemove: jest.fn(), downloadsGetList: jest.fn(), downloadsGet: jest.fn(), downloadsGetDefaultFolder: jest.fn(), downloadsSelectFolder: jest.fn(), downloadsRevealFile: jest.fn(), downloadsPlayFile: jest.fn(), downloadsClearCompleted: jest.fn(), onDownloadsUpdate: jest.fn(), }; expect(service.supportsDownloads).toBe(true); }); it('requires both desktop file-save preload methods', () => { testWindow.electron = { saveFileDialog: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsDesktopFileSave).toBe(false); testWindow.electron = { saveFileDialog: jest.fn(), writeFile: jest.fn(), }; expect(service.supportsDesktopFileSave).toBe(true); }); it('requires the complete playlist refresh preload surface', () => { testWindow.electron = { refreshPlaylist: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsPlaylistRefresh).toBe(false); testWindow.electron = { refreshPlaylist: jest.fn(), cancelPlaylistRefresh: jest.fn(), onPlaylistRefreshEvent: jest.fn(), }; expect(service.supportsPlaylistRefresh).toBe(true); }); it('requires the complete playlist storage SQLite preload surface', () => { testWindow.electron = { dbGetAppPlaylists: jest.fn(), dbUpsertAppPlaylist: jest.fn(), dbGetAppState: jest.fn(), dbSetAppState: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); expect(service.supportsSqlite).toBe(false); testWindow.electron = createPlaylistStorageBridge(); expect(service.supportsSqlite).toBe(true); }); it('checks playback-position storage and update bridge capabilities independently', () => { testWindow.electron = { dbSavePlaybackPosition: jest.fn(), dbGetPlaybackPosition: jest.fn(), dbGetSeriesPlaybackPositions: jest.fn(), dbGetRecentPlaybackPositions: jest.fn(), dbGetAllPlaybackPositions: jest.fn(), dbClearAllPlaybackPositions: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.supportsPlaybackPositionStorage).toBe(false); expect(service.supportsPlaybackPositionUpdates).toBe(false); testWindow.electron = { ...testWindow.electron, dbClearPlaybackPosition: jest.fn(), }; expect(service.supportsPlaybackPositionStorage).toBe(true); expect(service.supportsPlaybackPositionUpdates).toBe(false); testWindow.electron = { ...testWindow.electron, onPlaybackPositionUpdate: jest.fn(), }; expect(service.supportsPlaybackPositionStorage).toBe(true); expect(service.supportsPlaybackPositionUpdates).toBe(true); expect(service.supportsXtreamSqliteDataSource).toBe(false); }); it('supports Xtream section navigation in Electron when Xtream API transport is available', () => { testWindow.electron = { xtreamRequest: jest.fn(), }; const service = new RuntimeCapabilitiesService(); expect(service.isElectron).toBe(true); 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 { return { dbDeleteAllPlaylists: jest.fn(), dbDeletePlaylist: jest.fn(), dbGetAppPlaylist: jest.fn(), dbGetAppPlaylists: jest.fn(), dbGetAppState: jest.fn(), dbSetAppState: jest.fn(), dbUpsertAppPlaylist: jest.fn(), 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(), }; }