From 54d29f22394d7a431c1c963bb7b60a789a480048 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 22 May 2026 03:37:47 +0300 Subject: [PATCH] refactor(epg): gate multi epg queries by runtime capability --- .../multi-epg-container.component.spec.ts | 96 ++++++++++++++++++- .../multi-epg-container.component.ts | 12 ++- 2 files changed, 105 insertions(+), 3 deletions(-) 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 addd6cb0c..4232a97e1 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 @@ -1,4 +1,14 @@ -import { isSelectedEpgDayToday } from './multi-epg-container.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatDialog } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { RuntimeCapabilitiesService } from '@iptvnator/services'; +import { + MultiEpgContainerComponent, + isSelectedEpgDayToday, +} from './multi-epg-container.component'; +import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; describe('isSelectedEpgDayToday', () => { it('returns true only when the selected EPG day is the actual current day', () => { @@ -9,3 +19,87 @@ describe('isSelectedEpgDayToday', () => { expect(isSelectedEpgDayToday('20260522', now)).toBe(false); }); }); + +describe('MultiEpgContainerComponent runtime gates', () => { + let fixture: ComponentFixture; + let component: MultiEpgContainerComponent; + let runtimeCapabilities: { supportsEpg: boolean }; + const originalElectron = window.electron; + + beforeEach(async () => { + runtimeCapabilities = { supportsEpg: false }; + + await TestBed.configureTestingModule({ + imports: [MultiEpgContainerComponent], + providers: [ + { provide: MatDialog, useValue: { open: jest.fn() } }, + { + provide: COMPONENT_OVERLAY_REF, + useValue: { detach: jest.fn() }, + }, + { + provide: OverlayRef, + useValue: { detach: jest.fn() }, + }, + { + provide: RuntimeCapabilitiesService, + useValue: runtimeCapabilities, + }, + { + provide: TranslateService, + useValue: { + currentLang: 'en', + defaultLang: 'en', + onLangChange: of(null), + }, + }, + ], + }) + .overrideComponent(MultiEpgContainerComponent, { + set: { template: '' }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(MultiEpgContainerComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + window.electron = originalElectron; + fixture.destroy(); + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('does not request EPG channel ranges when runtime EPG support is disabled', async () => { + const getEpgChannelsByRange = jest.fn().mockResolvedValue([]); + window.electron = { + ...window.electron, + getEpgChannelsByRange, + } as unknown as typeof window.electron; + jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + await component.requestPrograms(); + + expect(getEpgChannelsByRange).not.toHaveBeenCalled(); + expect(component.isLoading()).toBe(false); + }); + + it('does not search EPG programs when runtime EPG support is disabled', () => { + jest.useFakeTimers(); + const searchEpgPrograms = jest.fn().mockResolvedValue([]); + window.electron = { + ...window.electron, + searchEpgPrograms, + } as unknown as typeof window.electron; + + component.onProgramSearchInput({ + target: { value: 'news' }, + } as unknown as Event); + jest.advanceTimersByTime(600); + + expect(searchEpgPrograms).not.toHaveBeenCalled(); + expect(component.isSearchingPrograms()).toBe(false); + expect(component.programSearchResults()).toEqual([]); + }); +}); diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts index 3923f4822..1f0e2f3eb 100644 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts @@ -29,6 +29,7 @@ import { EpgChannelWithPrograms, EpgProgram, } from '@iptvnator/shared/interfaces'; +import { RuntimeCapabilitiesService } from '@iptvnator/services'; import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component'; import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; @@ -181,6 +182,7 @@ export class MultiEpgContainerComponent private readonly dialog = inject(MatDialog); private readonly overlayRef = inject(COMPONENT_OVERLAY_REF); + private readonly runtime = inject(RuntimeCapabilitiesService); ngOnInit() { // Update current time line every minute @@ -267,8 +269,8 @@ export class MultiEpgContainerComponent } async requestPrograms(): Promise { - if (!window.electron) { - console.warn('Multi-EPG not available: Electron not detected'); + if (!this.runtime.supportsEpg) { + console.warn('Multi-EPG not available in this runtime'); return; } @@ -460,6 +462,12 @@ export class MultiEpgContainerComponent return; } + if (!this.runtime.supportsEpg) { + this.programSearchResults.set([]); + this.isSearchingPrograms.set(false); + return; + } + // Debounce search by 500ms - show spinner only when search actually starts this.searchDebounceTimer = setTimeout(async () => { this.isSearchingPrograms.set(true);