mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(runtime): address consolidated review feedback
This commit is contained in:
1 parent
582422c512
commit
ed8680116c
17 files changed
+293
-35
No files matched your search
@@ -48,7 +48,7 @@
|
||||
[players]="players()"
|
||||
[streamFormatEnum]="streamFormatEnum"
|
||||
[isDesktop]="isDesktop"
|
||||
[supportsManagedExternalPlayers]="supportsManagedExternalPlayers"
|
||||
[supportsManagedExternalPlayers]="supportsExternalPlayerPathSettings"
|
||||
(selectRecordingFolder)="selectRecordingFolder()"
|
||||
/>
|
||||
|
||||
|
||||
@@ -934,10 +934,11 @@ describe('SettingsComponent', () => {
|
||||
const clickSpy = jest
|
||||
.spyOn(HTMLAnchorElement.prototype, 'click')
|
||||
.mockImplementation();
|
||||
let partialFileSaveFixture: ComponentFixture<SettingsComponent> | 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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -729,7 +729,7 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
private async getSavedOrder(): Promise<string[]> {
|
||||
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<void> {
|
||||
if (!this.runtime.hasElectronMethod('dbSetAppState')) return;
|
||||
if (!this.runtime.supportsAppStateStorage) return;
|
||||
try {
|
||||
await window.electron?.dbSetAppState(
|
||||
GLOBAL_FAVORITES_ORDER_KEY,
|
||||
|
||||
+3
-5
@@ -32,7 +32,7 @@ describe('withStalkerPortal', () => {
|
||||
let dbCreatePlaylist: jest.Mock;
|
||||
let dbGetPlaylist: jest.Mock;
|
||||
let runtime: {
|
||||
hasElectronMethod: jest.Mock<boolean, [string]>;
|
||||
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);
|
||||
|
||||
|
||||
+4
-11
@@ -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<unknown>;
|
||||
dbGetPlaylist?: (playlistId: string) => Promise<unknown>;
|
||||
dbGetPlaylist: (playlistId: string) => Promise<unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, jest.Mock> {
|
||||
@@ -338,3 +393,41 @@ function createPlaylistStorageBridge(): Record<string, jest.Mock> {
|
||||
dbUpsertAppPlaylists: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function createXtreamSqliteBridge(): Record<string, jest.Mock> {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
+15
-1
@@ -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',
|
||||
|
||||
@@ -80,7 +80,7 @@ export class EpgSourceStatusComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.runtime.supportsEpg || !window.electron?.checkEpgFreshness) {
|
||||
if (!this.runtime.supportsEpg) {
|
||||
return;
|
||||
}
|
||||
const url = this.url();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
+1
-2
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user