From f9e95466e8d5d7a619cf7a86581f22f6454370be Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 22 May 2026 03:20:57 +0300 Subject: [PATCH 1/2] refactor(downloads): use runtime availability capability --- .../src/lib/downloads.service.spec.ts | 21 +++++++++++++++++++ libs/services/src/lib/downloads.service.ts | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libs/services/src/lib/downloads.service.spec.ts b/libs/services/src/lib/downloads.service.spec.ts index 43f6d6689..886850fad 100644 --- a/libs/services/src/lib/downloads.service.spec.ts +++ b/libs/services/src/lib/downloads.service.spec.ts @@ -1,8 +1,11 @@ import { signal, Signal, WritableSignal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { DownloadItem, DownloadsService, } from './downloads.service'; +import { RuntimeCapabilitiesService } from './runtime-capabilities.service'; +import { SettingsStore } from './settings-store.service'; type TestDownloadsService = { downloads: WritableSignal; @@ -27,6 +30,7 @@ describe('DownloadsService', () => { afterEach(() => { testWindow.electron = originalElectron; + TestBed.resetTestingModule(); jest.restoreAllMocks(); }); @@ -74,6 +78,23 @@ describe('DownloadsService', () => { return service; } + it('reports availability through the runtime capability', () => { + TestBed.configureTestingModule({ + providers: [ + DownloadsService, + { provide: SettingsStore, useValue: {} }, + { + provide: RuntimeCapabilitiesService, + useValue: { supportsDownloads: false }, + }, + ], + }); + + const service = TestBed.inject(DownloadsService); + + expect(service.isAvailable()).toBe(false); + }); + it('tracks loading and loaded state around a successful download list request', async () => { const item = createDownload(1); const pending = createDeferred(); diff --git a/libs/services/src/lib/downloads.service.ts b/libs/services/src/lib/downloads.service.ts index 61372f503..505c7fa53 100644 --- a/libs/services/src/lib/downloads.service.ts +++ b/libs/services/src/lib/downloads.service.ts @@ -1,4 +1,5 @@ import { computed, inject, Injectable, OnDestroy, signal } from '@angular/core'; +import { RuntimeCapabilitiesService } from './runtime-capabilities.service'; import { SettingsStore } from './settings-store.service'; export type DownloadStatus = @@ -31,6 +32,7 @@ export interface DownloadItem { @Injectable({ providedIn: 'root' }) export class DownloadsService implements OnDestroy { + private readonly runtime = inject(RuntimeCapabilitiesService); private readonly settingsStore = inject(SettingsStore); private unsubscribe?: () => void; private loadDownloadsRequestId = 0; @@ -48,7 +50,7 @@ export class DownloadsService implements OnDestroy { readonly hasLoadedDownloads = this._hasLoadedDownloads.asReadonly(); /** Whether the download feature is available (Electron only) */ - readonly isAvailable = computed(() => !!window.electron?.downloadsGetList); + readonly isAvailable = computed(() => this.runtime.supportsDownloads); /** Whether there are any downloads */ readonly hasDownloads = computed(() => this.downloads().length > 0); From 5ae8c5beddf4d6a2d8428d145ad953b13aa658e3 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 22 May 2026 13:01:28 +0300 Subject: [PATCH 2/2] test(services): avoid angular testing entrypoint --- .../src/lib/downloads.service.spec.ts | 30 ++++++++++++----- .../playlist-delete-action.service.spec.ts | 33 ++++++++++++++----- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/libs/services/src/lib/downloads.service.spec.ts b/libs/services/src/lib/downloads.service.spec.ts index 886850fad..ee83d16a5 100644 --- a/libs/services/src/lib/downloads.service.spec.ts +++ b/libs/services/src/lib/downloads.service.spec.ts @@ -1,5 +1,12 @@ -import { signal, Signal, WritableSignal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { + EnvironmentInjector, + Injector, + Signal, + WritableSignal, + createEnvironmentInjector, + runInInjectionContext, + signal, +} from '@angular/core'; import { DownloadItem, DownloadsService, @@ -30,7 +37,6 @@ describe('DownloadsService', () => { afterEach(() => { testWindow.electron = originalElectron; - TestBed.resetTestingModule(); jest.restoreAllMocks(); }); @@ -79,8 +85,8 @@ describe('DownloadsService', () => { } it('reports availability through the runtime capability', () => { - TestBed.configureTestingModule({ - providers: [ + const injector = createEnvironmentInjector( + [ DownloadsService, { provide: SettingsStore, useValue: {} }, { @@ -88,11 +94,19 @@ describe('DownloadsService', () => { useValue: { supportsDownloads: false }, }, ], - }); + Injector.NULL as unknown as EnvironmentInjector + ); - const service = TestBed.inject(DownloadsService); + try { + const service = runInInjectionContext( + injector, + () => new DownloadsService() + ); - expect(service.isAvailable()).toBe(false); + expect(service.isAvailable()).toBe(false); + } finally { + injector.destroy(); + } }); it('tracks loading and loaded state around a successful download list request', async () => { diff --git a/libs/services/src/lib/playlist-delete-action.service.spec.ts b/libs/services/src/lib/playlist-delete-action.service.spec.ts index aa6258c5a..e9bfc0f38 100644 --- a/libs/services/src/lib/playlist-delete-action.service.spec.ts +++ b/libs/services/src/lib/playlist-delete-action.service.spec.ts @@ -1,4 +1,9 @@ -import { TestBed } from '@angular/core/testing'; +import { + EnvironmentInjector, + Injector, + createEnvironmentInjector, + runInInjectionContext, +} from '@angular/core'; import { of } from 'rxjs'; import { PlaylistMeta } from '@iptvnator/shared/interfaces'; import { DatabaseService } from './database-electron.service'; @@ -23,6 +28,7 @@ describe('PlaylistDeleteActionService', () => { let runtime: { isElectron: boolean; }; + let injector: EnvironmentInjector; beforeEach(() => { databaseService = { @@ -38,18 +44,29 @@ describe('PlaylistDeleteActionService', () => { isElectron: false, }; - TestBed.configureTestingModule({ - providers: [ - PlaylistDeleteActionService, + injector = createEnvironmentInjector( + [ { provide: DatabaseService, useValue: databaseService }, { provide: PlaylistsService, useValue: playlistsService }, { provide: RuntimeCapabilitiesService, useValue: runtime }, ], - }); + Injector.NULL as unknown as EnvironmentInjector + ); }); + afterEach(() => { + injector.destroy(); + }); + + function createService(): PlaylistDeleteActionService { + return runInInjectionContext( + injector, + () => new PlaylistDeleteActionService() + ); + } + it('deletes browser playlists through PlaylistsService', async () => { - const service = TestBed.inject(PlaylistDeleteActionService); + const service = createService(); await expect(service.deletePlaylist(playlist)).resolves.toBe(true); @@ -62,7 +79,7 @@ describe('PlaylistDeleteActionService', () => { it('deletes Electron Xtream playlists through DatabaseService with progress options', async () => { runtime.isElectron = true; const onEvent = jest.fn(); - const service = TestBed.inject(PlaylistDeleteActionService); + const service = createService(); await expect( service.deletePlaylist(playlist, { onEvent }) @@ -83,7 +100,7 @@ describe('PlaylistDeleteActionService', () => { it('deletes Electron non-Xtream playlists without progress options', async () => { runtime.isElectron = true; - const service = TestBed.inject(PlaylistDeleteActionService); + const service = createService(); await expect( service.deletePlaylist({