From 47ff7aa2dca524b3ea80403a78f6f6a7a5a5cd3b Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 1 Dec 2024 16:29:24 +0100 Subject: [PATCH] test: update component tests --- src/app/app.component.spec.ts | 2 +- .../channel-list-container.component.spec.ts | 47 +++--- .../epg-item-description.component.spec.ts | 49 +++--- .../epg-list/epg-list.component.spec.ts | 101 ++++++++----- .../components/epg-list/epg-list.component.ts | 1 - .../html-video-player.component.spec.ts | 12 +- src/app/services/electron.service.stub.ts | 32 ++++ src/app/services/epg.service.spec.ts | 126 +++++++++++---- src/app/settings/settings.component.spec.ts | 143 ++++++++++++------ 9 files changed, 357 insertions(+), 156 deletions(-) create mode 100644 src/app/services/electron.service.stub.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 58d1515e9..30796f896 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -220,7 +220,7 @@ describe('AppComponent', () => { expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings); expect(settingsService.changeTheme).toHaveBeenCalledWith(theme); - expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(2); + expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1); expect(translateService.use).toHaveBeenCalledWith(language); }); diff --git a/src/app/player/components/channel-list-container/channel-list-container.component.spec.ts b/src/app/player/components/channel-list-container/channel-list-container.component.spec.ts index 45a564cd6..3284db823 100644 --- a/src/app/player/components/channel-list-container/channel-list-container.component.spec.ts +++ b/src/app/player/components/channel-list-container/channel-list-container.component.spec.ts @@ -9,6 +9,7 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { Actions } from '@ngrx/effects'; import { provideMockActions } from '@ngrx/effects/testing'; @@ -45,29 +46,28 @@ describe('ChannelListContainerComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ + imports: [ ChannelListContainerComponent, - MockPipes(FilterPipe), + FormsModule, + MatTabsModule, + MockModule(MatExpansionModule), + MockModule(MatIconModule), + MockModule(MatInputModule), + MockModule(MatListModule), + MockModule(MatSnackBarModule), + MockModule(MatTooltipModule), + MockModule(ScrollingModule), + MockModule(TranslateModule), + NoopAnimationsModule, + RouterTestingModule, ], providers: [ - { provide: MatSnackBar, useClass: MatSnackBarStub }, { provide: DataService, useClass: ElectronServiceStub }, - provideMockStore(), - provideMockActions(actions$), + { provide: MatSnackBar, useClass: MatSnackBarStub }, + MockPipes(FilterPipe), MockProviders(NgxIndexedDBService, TranslateService), - ], - imports: [ - MockModule(MatSnackBarModule), - MockModule(MatInputModule), - MockModule(MatIconModule), - MockModule(MatListModule), - MockModule(ScrollingModule), - MockModule(MatTabsModule), - MockModule(MatTooltipModule), - MockModule(MatExpansionModule), - MockModule(TranslateModule), - FormsModule, - RouterTestingModule, + provideMockActions(actions$), + provideMockStore(), ], }).compileComponents(); }); @@ -96,8 +96,15 @@ describe('ChannelListContainerComponent', () => { expect(component).toBeTruthy(); }); - it('should render three tabs', () => { - const tabs = fixture.debugElement.queryAll(By.css('mat-tab')); + it('should render three tabs', async () => { + const tabGroup = fixture.debugElement.query(By.css('mat-tab-group')); + expect(tabGroup).toBeTruthy(); + + // Force another change detection cycle + await fixture.whenStable(); + fixture.detectChanges(); + + const tabs = tabGroup.queryAll(By.css('.mat-mdc-tab')); expect(tabs.length).toEqual(3); }); diff --git a/src/app/player/components/epg-list/epg-item-description/epg-item-description.component.spec.ts b/src/app/player/components/epg-list/epg-item-description/epg-item-description.component.spec.ts index ea941c870..98f5ae7d7 100644 --- a/src/app/player/components/epg-list/epg-item-description/epg-item-description.component.spec.ts +++ b/src/app/player/components/epg-list/epg-item-description/epg-item-description.component.spec.ts @@ -12,16 +12,28 @@ describe('EpgItemDescriptionComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [EpgItemDescriptionComponent], - imports: [MockModule(MatDialogModule), MockModule(TranslateModule)], - providers: [{ provide: MAT_DIALOG_DATA, useValue: {} }], + imports: [ + EpgItemDescriptionComponent, + MockModule(MatDialogModule), + MockModule(TranslateModule), + ], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + title: 'TV Show 1', + desc: 'Highly interesting show about pets', + category: 'Fun', + } as unknown as EpgProgram, + }, + ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(EpgItemDescriptionComponent); component = fixture.componentInstance; - component.epgProgram = {} as EpgProgram; + component.epgProgram = TestBed.inject(MAT_DIALOG_DATA); fixture.detectChanges(); }); @@ -30,25 +42,26 @@ describe('EpgItemDescriptionComponent', () => { }); it('should render epg details in the dialog', () => { - component.epgProgram = { - title: [{ value: 'TV Show 1', lang: 'ru' }], - desc: [{ value: 'Highly interesting show about pets' }], - category: [{ value: 'Fun' }], - } as EpgProgram; fixture.detectChanges(); - const title = fixture.debugElement.query(By.css('[data-test="title"]')); - expect(title.nativeNode.innerHTML).toContain( - component.epgProgram.title[0].value + const titleElement = fixture.debugElement.query( + By.css('[data-test="title"]') ); - const category = fixture.debugElement.query( + expect(titleElement.nativeElement.textContent.trim()).toContain( + 'TV Show 1' + ); + + const categoryElement = fixture.debugElement.query( By.css('[data-test="category"]') ); - expect(category.nativeNode.innerHTML).toContain( - component.epgProgram.category[0].value + expect(categoryElement.nativeElement.textContent.trim()).toContain( + 'Fun' ); - const desc = fixture.debugElement.query(By.css('[data-test="desc"]')); - expect(desc.nativeNode.innerHTML).toContain( - component.epgProgram.desc[0].value + + const descElement = fixture.debugElement.query( + By.css('[data-test="desc"]') + ); + expect(descElement.nativeElement.textContent.trim()).toContain( + 'Highly interesting show about pets' ); }); }); diff --git a/src/app/player/components/epg-list/epg-list.component.spec.ts b/src/app/player/components/epg-list/epg-list.component.spec.ts index b1fb9594a..67cc07b36 100644 --- a/src/app/player/components/epg-list/epg-list.component.spec.ts +++ b/src/app/player/components/epg-list/epg-list.component.spec.ts @@ -8,22 +8,41 @@ import { Actions } from '@ngrx/effects'; import { provideMockActions } from '@ngrx/effects/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { TranslatePipe } from '@ngx-translate/core'; -import * as moment from 'moment'; import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { Channel } from '../../../../../shared/channel.interface'; import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands'; import { DataService } from '../../../services/data.service'; import { ElectronServiceStub } from '../../../services/electron.service.stub'; +import { EpgService } from '../../../services/epg.service'; import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe'; +import { EpgProgram } from '../../models/epg-program.model'; import { EpgListItemComponent } from './epg-list-item/epg-list-item.component'; -import { EpgData, EpgListComponent } from './epg-list.component'; +import { EpgListComponent } from './epg-list.component'; + +// Update moment mock to handle namespace import +jest.mock('moment', () => { + const momentFunc = () => ({ + format: () => '2023-01-01', + subtract: () => ({ + toISOString: () => '2023-01-01T00:00:00Z', + format: () => '2023-01-01', + }), + diff: () => 0, + add: () => ({ + format: () => '2023-01-01', + }), + }); + momentFunc.default = momentFunc; + return momentFunc; +}); describe('EpgListComponent', () => { let component: EpgListComponent; let fixture: ComponentFixture; let electronService: DataService; let mockStore: MockStore; + let epgService: EpgService; const actions$ = new Observable(); const MOCKED_PROGRAMS = { @@ -44,8 +63,8 @@ describe('EpgListComponent', () => { }, items: [ { - start: moment(Date.now()).format('YYYYMMDD'), - stop: moment(Date.now()).format('YYYYMMDD'), + start: '2023-01-01', + stop: '2023-01-01', channel: '12345', title: [{ lang: 'en', value: 'NOW on PBS' }], desc: [ @@ -82,42 +101,46 @@ describe('EpgListComponent', () => { ], audio: [], _attributes: { - start: moment(Date.now()).format('YYYYMMDD'), - stop: moment(Date.now()).format('YYYYMMDD'), + start: '2023-01-01', + stop: '2023-01-01', }, }, ], }; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ - EpgListComponent, - MockPipe(MomentDatePipe), - MockPipe(TranslatePipe), - MockComponent(EpgListItemComponent), - ], - imports: [ - MockModule(MatIconModule), - MockModule(MatTooltipModule), - MockModule(MatListModule), - MockModule(MatDialogModule), - ], - providers: [ - { provide: DataService, useClass: ElectronServiceStub }, - MockProvider(MatDialog), - provideMockStore(), - provideMockActions(actions$), - ], - }).compileComponents(); - }) - ); + beforeEach(waitForAsync(() => { + const mockEpgService = { + currentEpgPrograms$: new BehaviorSubject([]), + }; + + TestBed.configureTestingModule({ + declarations: [ + EpgListComponent, + MockPipe(MomentDatePipe), + MockPipe(TranslatePipe), + MockComponent(EpgListItemComponent), + ], + imports: [ + MockModule(MatIconModule), + MockModule(MatTooltipModule), + MockModule(MatListModule), + MockModule(MatDialogModule), + ], + providers: [ + { provide: DataService, useClass: ElectronServiceStub }, + { provide: EpgService, useValue: mockEpgService }, + MockProvider(MatDialog), + provideMockStore(), + provideMockActions(actions$), + ], + }).compileComponents(); + })); beforeEach(() => { fixture = TestBed.createComponent(EpgListComponent); component = fixture.componentInstance; electronService = TestBed.inject(DataService); + epgService = TestBed.inject(EpgService); mockStore = TestBed.inject(MockStore); mockStore.setState({ @@ -150,13 +173,19 @@ describe('EpgListComponent', () => { }); */ it('should handle an empty epg programs object', () => { - const payload = {} as unknown as EpgData; - component.handleEpgData({ payload }); + const emptyPrograms: EpgProgram[] = []; + (epgService.currentEpgPrograms$ as BehaviorSubject).next( + emptyPrograms + ); + component.handleEpgData(emptyPrograms); fixture.detectChanges(); - expect(component.timeNow).toBeFalsy(); - expect(component.dateToday).toBeFalsy(); + expect(component.timeNow).toBeTruthy(); + expect(component.dateToday).toBeTruthy(); expect(component.channel).toBeNull(); - expect(component.items).toHaveLength(0); + // Use async pipe or subscribe to test the Observable + component.items$.subscribe((items) => { + expect(items).toHaveLength(0); + }); }); it('should remove ipc listeners on destroy', () => { diff --git a/src/app/player/components/epg-list/epg-list.component.ts b/src/app/player/components/epg-list/epg-list.component.ts index 9dbe62359..3c717483a 100644 --- a/src/app/player/components/epg-list/epg-list.component.ts +++ b/src/app/player/components/epg-list/epg-list.component.ts @@ -105,7 +105,6 @@ export class EpgListComponent { ngOnInit(): void { this.timeshiftUntil$ = this.store.select(selectActive).pipe( map((active) => { - console.log(active); this.channel = { id: active?.tvg?.id, name: active?.name, diff --git a/src/app/player/components/html-video-player/html-video-player.component.spec.ts b/src/app/player/components/html-video-player/html-video-player.component.spec.ts index 21a16d12b..1a29cf749 100644 --- a/src/app/player/components/html-video-player/html-video-player.component.spec.ts +++ b/src/app/player/components/html-video-player/html-video-player.component.spec.ts @@ -3,12 +3,13 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslatePipe } from '@ngx-translate/core'; import { MockPipe } from 'ng-mocks'; import { DataService } from '../../../services/data.service'; -import { ElectronServiceStub } from '../../../services/electron.service.stub'; import { HtmlVideoPlayerComponent } from './html-video-player.component'; describe('HtmlVideoPlayerComponent', () => { let component: HtmlVideoPlayerComponent; let fixture: ComponentFixture; + let dataService: DataService; + const TEST_CHANNEL = { id: '1234', url: 'http://test.ts', @@ -22,17 +23,20 @@ describe('HtmlVideoPlayerComponent', () => { }; beforeEach(waitForAsync(() => { + const dataServiceMock = { + sendIpcEvent: jest.fn().mockResolvedValue(undefined), + }; + TestBed.configureTestingModule({ declarations: [HtmlVideoPlayerComponent, MockPipe(TranslatePipe)], - providers: [ - { provide: DataService, useClass: ElectronServiceStub }, - ], + providers: [{ provide: DataService, useValue: dataServiceMock }], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(HtmlVideoPlayerComponent); component = fixture.componentInstance; + dataService = TestBed.inject(DataService); fixture.detectChanges(); }); diff --git a/src/app/services/electron.service.stub.ts b/src/app/services/electron.service.stub.ts new file mode 100644 index 000000000..eee603144 --- /dev/null +++ b/src/app/services/electron.service.stub.ts @@ -0,0 +1,32 @@ +export class ElectronServiceStub { + ipcRenderer = { + send: jest.fn(), + on: jest.fn(), + removeAllListeners: jest.fn(), + }; + remote = { + process: { + platform: 'linux', + argv: [0, 1], + }, + app: { + getVersion: jest.fn(() => '1.0.0'), + }, + }; + + isElectron = true; + + removeAllListeners(type: string) {} + + getAppVersion() { + return this.remote.app.getVersion(); + } + + sendIpcEvent() {} + + listenOn() {} + + getAppEnvironment() { + return 'electron'; + } +} diff --git a/src/app/services/epg.service.spec.ts b/src/app/services/epg.service.spec.ts index 2bd8984c0..d59b3972f 100644 --- a/src/app/services/epg.service.spec.ts +++ b/src/app/services/epg.service.spec.ts @@ -1,12 +1,24 @@ -import { inject, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { TranslateService } from '@ngx-translate/core'; +import { invoke } from '@tauri-apps/api/core'; import { MockModule, MockProviders } from 'ng-mocks'; import { DataService } from './data.service'; import { EpgService } from './epg.service'; +jest.mock('@tauri-apps/api/core', () => ({ + invoke: jest.fn(), + isTauri: () => true, +})); + describe('EpgService', () => { + let service: EpgService; + let snackBar: MatSnackBar; + let store: MockStore; + let translateService: TranslateService; + let dispatchSpy: jest.SpyInstance; + beforeEach(() => { TestBed.configureTestingModule({ providers: [ @@ -16,39 +28,91 @@ describe('EpgService', () => { ], imports: [MockModule(MatSnackBarModule)], }); + + service = TestBed.inject(EpgService); + snackBar = TestBed.inject(MatSnackBar); + store = TestBed.inject(MockStore); + translateService = TestBed.inject(TranslateService); + + jest.spyOn(translateService, 'instant').mockImplementation( + (key) => key + ); + jest.spyOn(snackBar, 'open'); + dispatchSpy = jest.spyOn(store, 'dispatch'); + + // Suppress console.error during tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'log').mockImplementation(() => {}); }); - it('should create a service instance', inject( - [EpgService], - (service: EpgService) => { - expect(service).toBeTruthy(); - } - )); + afterEach(() => { + jest.clearAllMocks(); + }); - it('should show a notification on epg error', inject( - [MatSnackBar, EpgService], - (snackbar: MatSnackBar, service: EpgService) => { - jest.spyOn(snackbar, 'open'); - service.onEpgError(); - expect(snackbar.open).toHaveBeenCalledTimes(1); - } - )); + it('should create a service instance', () => { + expect(service).toBeTruthy(); + }); - it('should handle epg download success', inject( - [MatSnackBar, MockStore, EpgService], - ( - snackbar: MatSnackBar, - channelStore: MockStore, - service: EpgService - ) => { - jest.spyOn(snackbar, 'open'); - jest.spyOn(channelStore, 'dispatch'); - service.onEpgFetchDone(); - expect(snackbar.open).toHaveBeenCalledTimes(1); - expect(channelStore.dispatch).toHaveBeenCalledWith({ - value: true, - type: expect.stringContaining('active epg'), + describe('fetchEpg', () => { + it('should show success notification on successful fetch', async () => { + (invoke as jest.Mock).mockResolvedValueOnce({}); + service.fetchEpg(['http://example.com/epg.xml']); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(snackBar.open).toHaveBeenCalledWith( + 'EPG.FETCH_SUCCESS', + null, + expect.any(Object) + ); + }); + + it('should show error notification on fetch failure', async () => { + (invoke as jest.Mock).mockRejectedValueOnce(new Error('Failed')); + service.fetchEpg(['http://example.com/epg.xml']); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(snackBar.open).toHaveBeenCalledWith( + 'EPG.ERROR', + 'CLOSE', + expect.any(Object) + ); + }); + }); + + describe('getChannelPrograms', () => { + it('should update programs and set EPG flag to true when programs exist', (done) => { + const mockPrograms = [ + { + start: new Date().toISOString(), + stop: new Date().toISOString(), + title: 'Test', + }, + ]; + (invoke as jest.Mock).mockResolvedValueOnce(mockPrograms); + + service.getChannelPrograms('test-channel'); + + service.currentEpgPrograms$.subscribe((programs) => { + expect(programs).toHaveLength(1); + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ value: true }) + ); + done(); }); - } - )); + }); + + it('should set EPG flag to false when no programs found', (done) => { + (invoke as jest.Mock).mockResolvedValueOnce([]); + + service.getChannelPrograms('test-channel'); + + service.currentEpgPrograms$.subscribe((programs) => { + expect(programs).toHaveLength(0); + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ value: false }) + ); + done(); + }); + }); + }); }); diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/settings.component.spec.ts index 4f3356762..93d725405 100644 --- a/src/app/settings/settings.component.spec.ts +++ b/src/app/settings/settings.component.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable */ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { @@ -17,7 +18,6 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; -import { StorageMap } from '@ngx-pwa/local-storage'; import { TranslateModule, TranslatePipe, @@ -30,18 +30,23 @@ import { MockProvider, MockProviders, } from 'ng-mocks'; -import { of } from 'rxjs'; -import { EPG_FORCE_FETCH } from '../../../shared/ipc-commands'; import { DataService } from '../services/data.service'; -import { ElectronServiceStub } from '../services/electron.service.stub'; import { HeaderComponent } from '../shared/components'; import { Language } from './language.enum'; import { SettingsComponent } from './settings.component'; import { VideoPlayer } from './settings.interface'; import { Theme } from './theme.enum'; +import { signal } from '@angular/core'; import { NgxIndexedDBService } from 'ngx-indexed-db'; +import { of } from 'rxjs'; +import { SETTINGS_UPDATE } from '../../../shared/ipc-commands'; +import { DialogService } from '../services/dialog.service'; +import { ElectronServiceStub } from '../services/electron.service.stub'; +import { EpgService } from '../services/epg.service'; import { PlaylistsService } from '../services/playlists.service'; +import { SettingsStore } from '../services/settings-store.service'; +import { SettingsService } from '../services/settings.service'; class MatSnackBarStub { open(): void {} @@ -55,7 +60,6 @@ export class MockRouter { const DEFAULT_SETTINGS = { player: VideoPlayer.VideoJs, - epgUrl: [], language: Language.ENGLISH, showCaptions: false, theme: Theme.LightTheme, @@ -65,13 +69,34 @@ const DEFAULT_SETTINGS = { remoteControlPort: 3000, }; +class MockSettingsStore { + private _settings = signal(DEFAULT_SETTINGS); + + getSettings = () => this._settings; + + loadSettings = jest.fn().mockResolvedValue(undefined); + + updateSettings = jest.fn().mockResolvedValue(undefined); + + // Helper method for tests to modify settings + _setSettings(newSettings: any) { + this._settings.set(newSettings); + } +} + +class MockSettingsService { + getAppVersion = jest.fn().mockReturnValue(of('1.0.0')); + changeTheme = jest.fn(); +} + describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; let electronService: DataService; let router: Router; - let storage: StorageMap; + let settingsStore: SettingsStore; let translate: TranslateService; + let epgService: EpgService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -83,13 +108,16 @@ describe('SettingsComponent', () => { providers: [ UntypedFormBuilder, MockProvider(TranslateService), + { provide: SettingsStore, useClass: MockSettingsStore }, + MockProvider(EpgService), + MockProvider(DialogService), + { provide: SettingsService, useClass: MockSettingsService }, { provide: MatSnackBar, useClass: MatSnackBarStub }, { provide: DataService, useClass: ElectronServiceStub }, { provide: Router, useClass: MockRouter, }, - StorageMap, provideMockStore(), MockProviders(NgxIndexedDBService, PlaylistsService), ], @@ -114,9 +142,10 @@ describe('SettingsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SettingsComponent); electronService = TestBed.inject(DataService); - storage = TestBed.inject(StorageMap); + settingsStore = TestBed.inject(SettingsStore); router = TestBed.inject(Router); translate = TestBed.inject(TranslateService); + epgService = TestBed.inject(EpgService); component = fixture.componentInstance; fixture.detectChanges(); @@ -128,38 +157,32 @@ describe('SettingsComponent', () => { describe('Get and set settings on component init', () => { const settings = { - player: 'test', + player: VideoPlayer.VideoJs, showCaptions: true, - epgUrl: [], - mpvPlayerPath: '', - vlcPlayerPath: '', + mpvPlayerPath: '/test/mpv', + vlcPlayerPath: '/test/vlc', }; - let spyOnStorageGet; - beforeEach(() => { - spyOnStorageGet = jest.spyOn(storage, 'get'); - }); - - it('should init default settings if previous config was not saved', () => { - spyOnStorageGet.mockReturnValue(of(null)); - jest.spyOn(component.settingsForm, 'setValue'); - component.ngOnInit(); - expect(storage.get).toHaveBeenCalled(); - expect(component.settingsForm.setValue).toHaveBeenCalledTimes(0); + it('should init default settings if previous config was not saved', async () => { + await component.ngOnInit(); + expect(settingsStore.loadSettings).toHaveBeenCalled(); expect(component.settingsForm.value).toEqual(DEFAULT_SETTINGS); }); - it('should call set value function if custom config exists', () => { - spyOnStorageGet.mockReturnValue(of(settings)); - jest.spyOn(component.settingsForm, 'setValue'); - component.ngOnInit(); - expect(component.settingsForm.setValue).toHaveBeenCalled(); - }); + it('should get and apply custom settings', async () => { + const mockStore = settingsStore as unknown as MockSettingsStore; + mockStore._setSettings({ + ...DEFAULT_SETTINGS, + ...settings, + }); - it('should get and apply custom settings', () => { - spyOnStorageGet.mockReturnValue(of(settings)); component.ngOnInit(); - expect(storage.get).toHaveBeenCalled(); + + // Force change detection + fixture.detectChanges(); + await fixture.whenStable(); + + expect(settingsStore.loadSettings).toHaveBeenCalled(); expect(component.settingsForm.value).toEqual({ ...DEFAULT_SETTINGS, ...settings, @@ -171,6 +194,24 @@ describe('SettingsComponent', () => { const latestVersion = '1.0.0'; const currentVersion = '0.1.0'; + beforeEach(() => { + const settingsService = TestBed.inject(SettingsService); + (settingsService.getAppVersion as jest.Mock).mockReturnValue( + of(latestVersion) + ); + + // Add translation mock + jest.spyOn(translate, 'instant').mockImplementation((key) => { + if (key === 'SETTINGS.NEW_VERSION_AVAILABLE') { + return 'New version available'; + } + if (key === 'SETTINGS.LATEST_VERSION') { + return 'Latest version installed'; + } + return key; + }); + }); + it('should return true if version is outdated', () => { jest.spyOn(electronService, 'getAppVersion').mockReturnValue( currentVersion @@ -185,31 +226,43 @@ describe('SettingsComponent', () => { jest.spyOn(electronService, 'getAppVersion').mockReturnValue( currentVersion ); - component.showVersionInformation(currentVersion); + component.showVersionInformation(latestVersion); fixture.detectChanges(); - expect(translate.instant).toHaveBeenCalled(); + expect(translate.instant).toHaveBeenCalledWith( + 'SETTINGS.NEW_VERSION_AVAILABLE' + ); + expect(component.updateMessage).toBe( + 'New version available: 1.0.0' + ); }); }); - it('should send epg fetch command', () => { - jest.spyOn(electronService, 'sendIpcEvent'); + it('should send epg refresh command', () => { + jest.spyOn(epgService, 'fetchEpg'); const url = 'http://epg-url-here/data.xml'; component.refreshEpg(url); - expect(electronService.sendIpcEvent).toHaveBeenCalledWith( - EPG_FORCE_FETCH, - url - ); + expect(epgService.fetchEpg).toHaveBeenCalledWith([url]); }); it('should navigate back to home page', () => { jest.spyOn(router, 'navigateByUrl'); component.backToHome(); - expect(router.navigateByUrl).toHaveBeenCalledTimes(1); + expect(router.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('should save settings on submit', () => { - jest.spyOn(storage, 'set').mockReturnValue(of([] as any)); - component.onSubmit(); - expect(storage.set).toHaveBeenCalled(); + it('should save settings on submit', async () => { + const mockStore = settingsStore as unknown as MockSettingsStore; + mockStore.updateSettings.mockResolvedValue(undefined); + + jest.spyOn(electronService, 'sendIpcEvent'); + await component.onSubmit(); + + expect(mockStore.updateSettings).toHaveBeenCalledWith( + component.settingsForm.value + ); + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( + SETTINGS_UPDATE, + component.settingsForm.value + ); }); });