From 21316cef3ddc4b951695d94e71ea8de9f70ffaca Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 26 Oct 2021 19:55:55 +0000 Subject: [PATCH] test: increase test coverage --- src/app/app.component.spec.ts | 2 +- src/app/home/home.component.spec.ts | 15 +- .../playlist-info.component.spec.ts | 2 +- .../recent-playlists.component.spec.ts | 5 +- .../channel-list-container.component.spec.ts | 2 +- .../epg-list/epg-list.component.spec.ts | 2 +- .../video-player/video-player.component.ts | 5 +- src/app/services/electron.service.stub.ts | 24 +++ src/app/settings/settings.component.spec.ts | 81 +++++++- src/app/settings/settings.component.ts | 182 ++++++++++-------- src/app/settings/settings.interface.ts | 6 + 11 files changed, 216 insertions(+), 110 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 891a1ea49..846c3243a 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -4,7 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; import { TranslateModule, TranslatePipe } from '@ngx-translate/core'; import { ElectronService } from './services/electron.service'; -import { ElectronServiceStub } from './home/home.component.spec'; +import { ElectronServiceStub } from './services/electron.service.stub'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MockModule, MockPipe } from 'ng-mocks'; diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts index 1f39d3810..fdf0f3904 100644 --- a/src/app/home/home.component.spec.ts +++ b/src/app/home/home.component.spec.ts @@ -19,25 +19,12 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { ElectronService } from '../services/electron.service'; import { Router } from '@angular/router'; import { DialogService } from '../services/dialog.service'; +import { ElectronServiceStub } from '../services/electron.service.stub'; class MatSnackBarStub { open(): void {} } -export class ElectronServiceStub { - ipcRenderer = { - send: jest.fn(), - on: jest.fn(), - removeAllListeners: jest.fn(), - }; - remote = { - process: { - platform: 'linux', - argv: [0, 1], - }, - }; -} - describe('HomeComponent', () => { let component: HomeComponent; let dialogService: DialogService; diff --git a/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts b/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts index ab5b4a4bd..8fcae30ba 100644 --- a/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts +++ b/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts @@ -1,7 +1,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { PLAYLIST_SAVE_DETAILS } from './../../../../../shared/ipc-commands'; /* eslint-disable @typescript-eslint/unbound-method */ -import { ElectronServiceStub } from './../../home.component.spec'; +import { ElectronServiceStub } from '../../../services/electron.service.stub'; import { ElectronService } from './../../../services/electron.service'; import { TranslatePipe } from '@ngx-translate/core'; import { MockModule, MockPipe } from 'ng-mocks'; diff --git a/src/app/home/recent-playlists/recent-playlists.component.spec.ts b/src/app/home/recent-playlists/recent-playlists.component.spec.ts index ae1e69e2f..93f11e354 100644 --- a/src/app/home/recent-playlists/recent-playlists.component.spec.ts +++ b/src/app/home/recent-playlists/recent-playlists.component.spec.ts @@ -8,10 +8,7 @@ import { PlaylistMeta } from '../home.component'; import { MatListModule } from '@angular/material/list'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatIconModule } from '@angular/material/icon'; - -export class ElectronServiceStub { - sendIpcEvent(): void {} -} +import { ElectronServiceStub } from '../../services/electron.service.stub'; describe('RecentPlaylistsComponent', () => { let component: RecentPlaylistsComponent; 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 da68a6e2d..4b131ec0d 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 @@ -19,7 +19,7 @@ import { By } from '@angular/platform-browser'; import { createChannel } from '../../../state'; import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json'; import { ElectronService } from '../../../services/electron.service'; -import { ElectronServiceStub } from '../../../home/home.component.spec'; +import { ElectronServiceStub } from '../../../services/electron.service.stub'; import { ScrollingModule } from '@angular/cdk/scrolling'; class MatSnackBarStub { 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 286ae8a35..8f45dda88 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 @@ -7,7 +7,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { EpgListComponent, EpgData } from './epg-list.component'; import { MatListModule } from '@angular/material/list'; import { ElectronService } from '../../../services/electron.service'; -import { ElectronServiceStub } from '../../../home/home.component.spec'; +import { ElectronServiceStub } from '../../../services/electron.service.stub'; import * as moment from 'moment'; import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands'; import { Channel, ChannelStore } from '../../../state'; diff --git a/src/app/player/components/video-player/video-player.component.ts b/src/app/player/components/video-player/video-player.component.ts index 45dcc249b..28c8ce1ee 100644 --- a/src/app/player/components/video-player/video-player.component.ts +++ b/src/app/player/components/video-player/video-player.component.ts @@ -5,12 +5,9 @@ import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { MatSidenav } from '@angular/material/sidenav'; import { StorageMap } from '@ngx-pwa/local-storage'; -import { Settings, VideoPlayer } from '../../../settings/settings.interface'; +import { Settings, SETTINGS_STORE_KEY, VideoPlayer } from '../../../settings/settings.interface'; import { MatSnackBar } from '@angular/material/snack-bar'; -/** Settings key in storage */ -export const SETTINGS_STORE_KEY = 'settings'; - @Component({ selector: 'app-video-player', templateUrl: './video-player.component.html', diff --git a/src/app/services/electron.service.stub.ts b/src/app/services/electron.service.stub.ts new file mode 100644 index 000000000..c84c3f82f --- /dev/null +++ b/src/app/services/electron.service.stub.ts @@ -0,0 +1,24 @@ +export class ElectronServiceStub { + ipcRenderer = { + send: jest.fn(), + on: jest.fn(), + removeAllListeners: jest.fn(), + }; + remote = { + process: { + platform: 'linux', + argv: [0, 1], + }, + app: { + getVersion: jest.fn(), + } + }; + + getAppVersion() { + this.remote.app.getVersion(); + } + + sendIpcEvent() { + this.ipcRenderer.send(); + } +} \ No newline at end of file diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/settings.component.spec.ts index cd952b394..f3a9a8b53 100644 --- a/src/app/settings/settings.component.spec.ts +++ b/src/app/settings/settings.component.spec.ts @@ -2,7 +2,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { StorageMap } from '@ngx-pwa/local-storage'; /* eslint-disable @typescript-eslint/unbound-method */ import { ElectronService } from './../services/electron.service'; -import { ElectronServiceStub } from './../home/home.component.spec'; +import { ElectronServiceStub } from '../services/electron.service.stub'; import { MatSelectModule } from '@angular/material/select'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatListModule } from '@angular/material/list'; @@ -21,6 +21,10 @@ import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EPG_FETCH } from '../../../shared/ipc-commands'; import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { VideoPlayer } from './settings.interface'; +import { Language } from './language.enum'; +import { Theme } from './theme.enum'; class MatSnackBarStub { open(): void {} @@ -32,11 +36,21 @@ export class MockRouter { } } +const DEFAULT_SETTINGS = { + player: VideoPlayer.VideoJs, + epgUrl: '', + language: Language.ENGLISH, + showCaptions: false, + theme: Theme.LightTheme +} + describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; let electronService: ElectronService; let router: Router; + let storage: StorageMap; + let translate: TranslateService; beforeEach( waitForAsync(() => { @@ -80,7 +94,9 @@ describe('SettingsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SettingsComponent); electronService = TestBed.inject(ElectronService); + storage = TestBed.inject(StorageMap); router = TestBed.inject(Router); + translate = TestBed.inject(TranslateService); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -89,10 +105,63 @@ describe('SettingsComponent', () => { expect(component).toBeTruthy(); }); + describe('Get and set settings on component init', () => { + const settings = {player: 'test', showCaptions: true}; + let spyOnStorageGet; + + beforeEach(() => { + spyOnStorageGet = spyOn(storage, 'get'); + }) + + it('should init default settings if previous config was not saved', () => { + spyOnStorageGet.and.returnValue(of(null)); + spyOn(component.settingsForm, 'setValue'); + component.ngOnInit(); + expect(storage.get).toHaveBeenCalled(); + expect(component.settingsForm.setValue).toHaveBeenCalledTimes(0); + expect(component.settingsForm.value).toEqual(DEFAULT_SETTINGS); + }); + + it('should call set value function if custom config exists', () => { + spyOnStorageGet.and.returnValue(of(settings)); + spyOn(component.settingsForm, 'setValue'); + component.ngOnInit(); + expect(component.settingsForm.setValue).toHaveBeenCalled(); + }); + + it('should get and apply custom settings', () => { + spyOnStorageGet.and.returnValue(of(settings)); + component.ngOnInit(); + expect(storage.get).toHaveBeenCalled(); + expect(component.settingsForm.value).toEqual({...DEFAULT_SETTINGS, ...settings}); + }); + }); + + + describe.only('Version check', () => { + const latestVersion = '1.0.0'; + const currentVersion = '0.1.0'; + + it('should return true if version is outdated', () => { + spyOn(electronService, 'getAppVersion').and.returnValue(currentVersion); + const isOutdated = component.isCurrentVersionOutdated(latestVersion); + expect(isOutdated).toBeTruthy(); + }); + + it('should update notification message if version is outdated', () => { + spyOn(translate, 'instant'); + spyOn(electronService, 'getAppVersion').and.returnValue(currentVersion); + component.showVersionInformation(currentVersion); + fixture.detectChanges(); + expect(translate.instant).toHaveBeenCalled(); + }); + + }); + it('should send epg fetch command', () => { - spyOn(electronService.ipcRenderer, 'send'); + spyOn(electronService, 'sendIpcEvent'); component.fetchEpg(); - expect(electronService.ipcRenderer.send).toHaveBeenCalledWith( + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( EPG_FETCH, { url: '' } ); @@ -103,4 +172,10 @@ describe('SettingsComponent', () => { component.backToHome(); expect(router.navigateByUrl).toHaveBeenCalledTimes(1); }); + + it('should save settings on submit', () => { + spyOn(storage, 'set').and.returnValue(of([])); + component.onSubmit(); + expect(storage.set).toHaveBeenCalled(); + }); }); diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 08cd6be29..89cb4bc6d 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -1,11 +1,11 @@ import { TranslateService } from '@ngx-translate/core'; -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; import { StorageMap } from '@ngx-pwa/local-storage'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable, Subscription } from 'rxjs'; -import { Settings, VideoPlayer } from './settings.interface'; +import { Settings, SETTINGS_STORE_KEY, VideoPlayer } from './settings.interface'; import { HttpClient } from '@angular/common/http'; import * as semver from 'semver'; import { ElectronService } from '../services/electron.service'; @@ -14,6 +14,7 @@ import { EPG_FETCH } from '../../../shared/ipc-commands'; import { Language } from './language.enum'; import { Theme } from './theme.enum'; import { SettingsService } from './../services/settings.service'; +import { catchError } from 'rxjs/operators'; /** Url of the package.json file in the app repository, required to get the version of the released app */ const PACKAGE_JSON_URL = @@ -24,10 +25,7 @@ const PACKAGE_JSON_URL = templateUrl: './settings.component.html', styleUrls: ['./settings.component.scss'], }) -export class SettingsComponent implements OnInit, OnDestroy { - /** Subscription object */ - private subscription: Subscription = new Subscription(); - +export class SettingsComponent implements OnInit { /** List with available languages as enum */ languageEnum = Language; @@ -61,7 +59,8 @@ export class SettingsComponent implements OnInit, OnDestroy { themeEnum = Theme; /** - * Creates an instance of SettingsComponent and injects some dependencies into the component + * Creates an instance of SettingsComponent and injects + * required dependencies into the component * @param channelQuery * @param electronService * @param formBuilder @@ -91,84 +90,112 @@ export class SettingsComponent implements OnInit, OnDestroy { theme: Theme.LightTheme, }); - this.subscription.add( - this.http - .get(PACKAGE_JSON_URL) - .subscribe((response: { version: string }) => { - this.version = this.electronService.getAppVersion(); - const isOutdated = semver.lt( - this.version, - response.version - ); - - if (isOutdated) { - this.updateMessage = `${ - this.translate.instant( - 'SETTINGS.NEW_VERSION_AVAILABLE' - ) as string - }: ${response.version}`; - } else { - this.updateMessage = this.translate.instant( - 'SETTINGS.LATEST_VERSION' - ); - } - }) - ); + this.checkAppVersion(); } /** - * Reads the config object from the browsers storage (indexed db) + * Reads the config object from the browsers + * storage (indexed db) */ ngOnInit(): void { - this.subscription.add( - this.storage.get('settings').subscribe((settings: Settings) => { - if (settings) { - this.settingsForm.setValue({ - player: settings.player - ? settings.player - : VideoPlayer.VideoJs, - epgUrl: settings.epgUrl ? settings.epgUrl : '', - language: settings.language - ? settings.language - : Language.ENGLISH, - showCaptions: settings.showCaptions - ? settings.showCaptions - : false, - theme: settings.theme - ? settings.theme - : Theme.LightTheme, - }); - } - }) + this.storage.get(SETTINGS_STORE_KEY).subscribe((settings: Settings) => { + if (settings) { + this.settingsForm.setValue({ + player: settings.player + ? settings.player + : VideoPlayer.VideoJs, + epgUrl: settings.epgUrl ? settings.epgUrl : '', + language: settings.language + ? settings.language + : Language.ENGLISH, + showCaptions: settings.showCaptions + ? settings.showCaptions + : false, + theme: settings.theme + ? settings.theme + : Theme.LightTheme, + }); + } + }); + } + + /** + * Checks whether the latest version of the application + * is used and updates the version message in the + * settings UI + */ + checkAppVersion(): void { + this.http + .get(PACKAGE_JSON_URL) + .pipe(catchError(err => { + console.error(err); + throw new Error(err); + })) + .subscribe((response: { version: string }) => { + this.showVersionInformation(response.version); + }); + } + + /** + * Updates the message in settings UI about the used + * version of the app + * @param currentVersion current version of the application + */ + showVersionInformation(currentVersion: string): void { + const isOutdated = this.isCurrentVersionOutdated(currentVersion); + + if (isOutdated) { + this.updateMessage = `${ + this.translate.instant( + 'SETTINGS.NEW_VERSION_AVAILABLE' + ) as string + }: ${currentVersion}`; + } else { + this.updateMessage = this.translate.instant( + 'SETTINGS.LATEST_VERSION' + ); + } + } + + /** + * Compares actual with latest version of the + * application + * @param latestVersion latest version + * @returns returns true if an update is available + */ + isCurrentVersionOutdated(latestVersion: string): boolean { + this.version = this.electronService.getAppVersion(); + return semver.lt( + this.version, + latestVersion ); } /** - * Triggers on form submit and saves the config object to the indexed db store + * Triggers on form submit and saves the config object to + * the indexed db store */ onSubmit(): void { - this.subscription.add( - this.storage - .set('settings', this.settingsForm.value) - .subscribe(() => { - this.settingsForm.markAsPristine(); - // check whether the epg url was changed or not - if (this.settingsForm.value.epgUrl) { - this.fetchEpg(); + this.storage + .set(SETTINGS_STORE_KEY, this.settingsForm.value) + .subscribe(() => { + this.settingsForm.markAsPristine(); + // check whether the epg url was changed or not + if (this.settingsForm.value.epgUrl) { + this.fetchEpg(); + } + this.translate.use(this.settingsForm.value.language); + this.settingsService.changeTheme( + this.settingsForm.value.theme + ); + this.snackBar.open( + this.translate.instant('SETTINGS.SETTINGS_SAVED'), + null, + { + duration: 2000, } - this.translate.use(this.settingsForm.value.language); - this.settingsService.changeTheme( - this.settingsForm.value.theme - ); - this.snackBar.open( - this.translate.instant('SETTINGS.SETTINGS_SAVED'), - null, - { - duration: 2000, - } - ); - }) - ); + ); + }); } /** @@ -178,18 +205,11 @@ export class SettingsComponent implements OnInit, OnDestroy { this.router.navigateByUrl('/', { skipLocationChange: true }); } - /** - * Unsubscribe on destroy - */ - ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - /** * Fetches and updates EPG from the given URL */ fetchEpg(): void { - this.electronService.ipcRenderer.send(EPG_FETCH, { + this.electronService.sendIpcEvent(EPG_FETCH, { url: this.settingsForm.value.epgUrl, }); this.snackBar.open(this.translate.instant('EPG.FETCH_EPG'), 'Close', { diff --git a/src/app/settings/settings.interface.ts b/src/app/settings/settings.interface.ts index 43648adc2..e21d8c478 100644 --- a/src/app/settings/settings.interface.ts +++ b/src/app/settings/settings.interface.ts @@ -1,6 +1,12 @@ import { Language } from './language.enum'; import { Theme } from './theme.enum'; +/** Settings key in storage */ +export const SETTINGS_STORE_KEY = 'settings'; + +/** + * Contains all types of supported video players + */ export enum VideoPlayer { VideoJs = 'videojs', Html5Player = 'html5',