diff --git a/.eslintrc.json b/.eslintrc.json index be7ad60..624783e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -54,6 +54,21 @@ "plugin:@angular-eslint/template/recommended" ], "rules": {} + }, + { + "files": [ + "*.spec.ts" + ], + "rules": { + "@typescript-eslint/await-thenable": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off" + } } ] } diff --git a/src/app/modules/guest/guest-show-data.service.spec.ts b/src/app/modules/guest/guest-show-data.service.spec.ts index 6db7021..ac857e5 100644 --- a/src/app/modules/guest/guest-show-data.service.spec.ts +++ b/src/app/modules/guest/guest-show-data.service.spec.ts @@ -12,7 +12,7 @@ describe('GuestShowDataService', () => { let colSpy: jasmine.Spy; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { docUpdateSpy = jasmine.createSpy('update').and.resolveTo(); docDeleteSpy = jasmine.createSpy('delete').and.resolveTo(); docSpy = jasmine.createSpy('doc').and.returnValue({ @@ -27,7 +27,7 @@ describe('GuestShowDataService', () => { dbServiceSpy.doc.and.callFake(docSpy); dbServiceSpy.col.and.callFake(colSpy); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/modules/guest/guest-show.service.spec.ts b/src/app/modules/guest/guest-show.service.spec.ts index 69b2e02..f16f76a 100644 --- a/src/app/modules/guest/guest-show.service.spec.ts +++ b/src/app/modules/guest/guest-show.service.spec.ts @@ -2,20 +2,22 @@ import {TestBed} from '@angular/core/testing'; import {GuestShowDataService} from './guest-show-data.service'; import {GuestShowService} from './guest-show.service'; import {ShowService} from '../shows/services/show.service'; +import {Show} from '../shows/services/show'; +import {Song} from '../songs/services/song'; describe('GuestShowService', () => { let service: GuestShowService; let guestShowDataServiceSpy: jasmine.SpyObj; let showServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { guestShowDataServiceSpy = jasmine.createSpyObj('GuestShowDataService', ['add', 'update$']); showServiceSpy = jasmine.createSpyObj('ShowService', ['update$']); guestShowDataServiceSpy.add.and.resolveTo('share-1'); guestShowDataServiceSpy.update$.and.resolveTo(); showServiceSpy.update$.and.resolveTo(); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: GuestShowDataService, useValue: guestShowDataServiceSpy}, {provide: ShowService, useValue: showServiceSpy}, @@ -30,8 +32,8 @@ describe('GuestShowService', () => { }); it('should create a new guest share, persist the generated shareId on the show and return the share url', async () => { - const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: ''} as any; - const songs = [{id: 'song-1'}] as any; + const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: ''} as unknown as Show; + const songs = [{id: 'song-1'}] as unknown as Song[]; const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-1'; await expectAsync(service.share(show, songs)).toBeResolvedTo(expectedUrl); @@ -45,8 +47,8 @@ describe('GuestShowService', () => { }); it('should update an existing share and reuse its id in the returned url', async () => { - const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: 'share-9'} as any; - const songs = [{id: 'song-1'}] as any; + const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: 'share-9'} as unknown as Show; + const songs = [{id: 'song-1'}] as unknown as Song[]; const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-9'; await expectAsync(service.share(show, songs)).toBeResolvedTo(expectedUrl); diff --git a/src/app/modules/shows/list/list.component.ts b/src/app/modules/shows/list/list.component.ts index dcdcb95..67d2ac5 100644 --- a/src/app/modules/shows/list/list.component.ts +++ b/src/app/modules/shows/list/list.component.ts @@ -3,7 +3,8 @@ import {combineLatest} from 'rxjs'; import {Show} from '../services/show'; import {fade} from '../../../animations'; import {ShowService} from '../services/show.service'; -import {FilterValues} from './filter/filter-values'import {RouterLink} from '@angular/router'; +import {FilterValues} from './filter/filter-values'; +import {RouterLink} from '@angular/router'; import {map, switchMap} from 'rxjs/operators'; import {FilterStoreService} from '../../../services/filter-store.service'; import {RoleDirective} from '../../../services/user/role.directive'; @@ -49,10 +50,6 @@ export class ListComponent { public trackBy = (index: number, show: unknown) => (show as Show).id; - private matchesFilter(show: Show, filter: FilterValues): boolean { - return this.matchesTimeFilter(show, filter.time || 1) && (!filter.owner || show.owner === filter.owner) && (!filter.showType || show.showType === filter.showType); - } - private matchesPrivateFilter(show: Show, filter: FilterValues): boolean { return this.matchesTimeFilter(show, filter.time || 1) && (!filter.showType || show.showType === filter.showType); } diff --git a/src/app/modules/shows/services/docx.service.spec.ts b/src/app/modules/shows/services/docx.service.spec.ts index b918668..5eb09f4 100644 --- a/src/app/modules/shows/services/docx.service.spec.ts +++ b/src/app/modules/shows/services/docx.service.spec.ts @@ -4,9 +4,14 @@ import {DocxService} from './docx.service'; describe('DocxService', () => { let service: DocxService; + type DocxServiceInternals = DocxService & { + prepareData: (showId: string) => Promise; + prepareNewDocument: (data: unknown, options?: unknown) => unknown; + saveAs: (blob: Blob, name: string) => void; + }; - beforeEach(() => { - void TestBed.configureTestingModule({}); + beforeEach(async () => { + await TestBed.configureTestingModule({}); service = TestBed.inject(DocxService); }); @@ -15,8 +20,9 @@ describe('DocxService', () => { }); it('should not try to save a document when the required data cannot be prepared', async () => { - const prepareDataSpy = spyOn(service, 'prepareData').and.resolveTo(null); - const saveAsSpy = spyOn(service, 'saveAs'); + const serviceInternals = service as DocxServiceInternals; + const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo(null); + const saveAsSpy = spyOn(serviceInternals, 'saveAs'); await service.create('show-1'); @@ -26,7 +32,8 @@ describe('DocxService', () => { it('should build and save a docx file when all data is available', async () => { const blob = new Blob(['docx']); - const prepareDataSpy = spyOn(service, 'prepareData').and.resolveTo({ + const serviceInternals = service as DocxServiceInternals; + const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo({ show: { showType: 'service-worship', date: {toDate: () => new Date('2026-03-10T00:00:00Z')}, @@ -35,8 +42,8 @@ describe('DocxService', () => { user: {name: 'Benjamin'}, config: {ccliLicenseId: '12345'}, }); - const prepareNewDocumentSpy = spyOn(service, 'prepareNewDocument').and.returnValue({doc: true}); - const saveAsSpy = spyOn(service, 'saveAs'); + const prepareNewDocumentSpy = spyOn(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true}); + const saveAsSpy = spyOn(serviceInternals, 'saveAs'); spyOn(Packer, 'toBlob').and.resolveTo(blob); await service.create('show-1', {copyright: true}); diff --git a/src/app/modules/shows/services/show-data.service.spec.ts b/src/app/modules/shows/services/show-data.service.spec.ts index dd72a5a..ab17b2d 100644 --- a/src/app/modules/shows/services/show-data.service.spec.ts +++ b/src/app/modules/shows/services/show-data.service.spec.ts @@ -1,6 +1,6 @@ import {TestBed} from '@angular/core/testing'; import {firstValueFrom, of, Subject} from 'rxjs'; -import {skip, take} from 'rxjs/operators'; +import {take} from 'rxjs/operators'; import {DbService} from '../../../services/db.service'; import {ShowDataService} from './show-data.service'; @@ -13,7 +13,7 @@ describe('ShowDataService', () => { let colSpy: jasmine.Spy; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { shows$ = new Subject number}; archived?: boolean}>>(); docUpdateSpy = jasmine.createSpy('update').and.resolveTo(); docSpy = jasmine.createSpy('doc').and.returnValue({update: docUpdateSpy}); @@ -25,7 +25,7 @@ describe('ShowDataService', () => { dbServiceSpy.doc.and.callFake(docSpy); dbServiceSpy.col.and.callFake(colSpy); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); @@ -76,11 +76,7 @@ describe('ShowDataService', () => { }); it('should request only published recent shows and filter archived entries', async () => { - const publicShows$ = of([ - {id: 'show-1', archived: false}, - {id: 'show-2', archived: true}, - {id: 'show-3'}, - ]); + const publicShows$ = of([{id: 'show-1', archived: false}, {id: 'show-2', archived: true}, {id: 'show-3'}]); dbServiceSpy.col$.and.returnValue(publicShows$ as never); const result = await firstValueFrom(service.listPublicSince$(3)); diff --git a/src/app/modules/shows/services/show-song-data.service.spec.ts b/src/app/modules/shows/services/show-song-data.service.spec.ts index d69dfdd..7a03e8d 100644 --- a/src/app/modules/shows/services/show-song-data.service.spec.ts +++ b/src/app/modules/shows/services/show-song-data.service.spec.ts @@ -12,7 +12,7 @@ describe('ShowSongDataService', () => { let colSpy: jasmine.Spy; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { docUpdateSpy = jasmine.createSpy('update').and.resolveTo(); docDeleteSpy = jasmine.createSpy('delete').and.resolveTo(); docSpy = jasmine.createSpy('doc').and.returnValue({ @@ -27,7 +27,7 @@ describe('ShowSongDataService', () => { dbServiceSpy.doc.and.callFake(docSpy); dbServiceSpy.col.and.callFake(colSpy); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/modules/shows/services/show-song.service.spec.ts b/src/app/modules/shows/services/show-song.service.spec.ts index ccdc34e..9218194 100644 --- a/src/app/modules/shows/services/show-song.service.spec.ts +++ b/src/app/modules/shows/services/show-song.service.spec.ts @@ -5,6 +5,10 @@ import {UserService} from '../../../services/user/user.service'; import {ShowService} from './show.service'; import {ShowSongDataService} from './show-song-data.service'; import {ShowSongService} from './show-song.service'; +import {ShowSong} from './show-song'; +import {Song} from '../../songs/services/song'; +import {Show} from './show'; +import {User} from '../../../services/user/user'; describe('ShowSongService', () => { let service: ShowSongService; @@ -12,13 +16,13 @@ describe('ShowSongService', () => { let songDataServiceSpy: jasmine.SpyObj; let userServiceSpy: jasmine.SpyObj; let showServiceSpy: jasmine.SpyObj; - let user$: BehaviorSubject; - const song = {id: 'song-1', key: 'G', title: 'Amazing Grace'} as any; - const showSong = {id: 'show-song-1', songId: 'song-1'} as any; - const show = {id: 'show-1', order: ['show-song-1', 'show-song-2']} as any; + let user$: BehaviorSubject; + const song = {id: 'song-1', key: 'G', title: 'Amazing Grace'} as unknown as Song; + const showSong = {id: 'show-song-1', songId: 'song-1'} as unknown as ShowSong; + const show = {id: 'show-1', order: ['show-song-1', 'show-song-2']} as unknown as Show; - beforeEach(() => { - user$ = new BehaviorSubject({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}}); + beforeEach(async () => { + user$ = new BehaviorSubject({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}}); showSongDataServiceSpy = jasmine.createSpyObj('ShowSongDataService', ['add', 'read$', 'list$', 'delete', 'update$']); songDataServiceSpy = jasmine.createSpyObj('SongDataService', ['read$']); userServiceSpy = jasmine.createSpyObj('UserService', ['incSongCount', 'decSongCount'], { @@ -37,7 +41,7 @@ describe('ShowSongService', () => { showServiceSpy.read$.and.returnValue(of(show)); showServiceSpy.update$.and.resolveTo(); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: ShowSongDataService, useValue: showSongDataServiceSpy}, {provide: SongDataService, useValue: songDataServiceSpy}, diff --git a/src/app/modules/shows/services/show.service.spec.ts b/src/app/modules/shows/services/show.service.spec.ts index 7532e82..bfb2cc0 100644 --- a/src/app/modules/shows/services/show.service.spec.ts +++ b/src/app/modules/shows/services/show.service.spec.ts @@ -14,7 +14,7 @@ describe('ShowService', () => { {id: 'show-3', owner: 'user-1', published: true, archived: true}, ] as never; - beforeEach(() => { + beforeEach(async () => { user$ = new BehaviorSubject({id: 'user-1'}); showDataServiceSpy = jasmine.createSpyObj('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], { list$: of(shows), @@ -24,7 +24,7 @@ describe('ShowService', () => { showDataServiceSpy.update.and.resolveTo(); showDataServiceSpy.add.and.resolveTo('new-show-id'); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: ShowDataService, useValue: showDataServiceSpy}, {provide: UserService, useValue: {user$: user$.asObservable()}}, diff --git a/src/app/modules/songs/services/file-data.service.spec.ts b/src/app/modules/songs/services/file-data.service.spec.ts index f075969..3293d79 100644 --- a/src/app/modules/songs/services/file-data.service.spec.ts +++ b/src/app/modules/songs/services/file-data.service.spec.ts @@ -12,7 +12,7 @@ describe('FileDataService', () => { let fileDeleteSpy: jasmine.Spy; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { filesCollectionValueChangesSpy = jasmine.createSpy('valueChanges').and.returnValue(of([{id: 'file-1', name: 'plan.pdf'}])); filesCollectionAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'file-2'}); songDocCollectionSpy = jasmine.createSpy('collection').and.returnValue({ @@ -30,7 +30,7 @@ describe('FileDataService', () => { dbServiceSpy = jasmine.createSpyObj('DbService', ['doc']); dbServiceSpy.doc.and.callFake(songDocSpy); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/modules/songs/services/file.service.spec.ts b/src/app/modules/songs/services/file.service.spec.ts index a66a8c6..079117b 100644 --- a/src/app/modules/songs/services/file.service.spec.ts +++ b/src/app/modules/songs/services/file.service.spec.ts @@ -6,12 +6,16 @@ import {FileService} from './file.service'; describe('FileService', () => { let service: FileService; let fileDataServiceSpy: jasmine.SpyObj; + type FileServiceInternals = FileService & { + resolveDownloadUrl: (path: string) => Promise; + deleteFromStorage: (path: string) => Promise; + }; - beforeEach(() => { + beforeEach(async () => { fileDataServiceSpy = jasmine.createSpyObj('FileDataService', ['delete']); fileDataServiceSpy.delete.and.resolveTo(); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Storage, useValue: {app: 'test-storage'}}, {provide: FileDataService, useValue: fileDataServiceSpy}, @@ -26,7 +30,7 @@ describe('FileService', () => { }); it('should resolve download urls via AngularFire storage helpers', async () => { - const resolveSpy = spyOn(service, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf'); + const resolveSpy = spyOn(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf'); await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf'); @@ -34,10 +38,9 @@ describe('FileService', () => { }); it('should delete the file from storage and metadata from firestore', async () => { - const deleteFromStorageSpy = spyOn(service, 'deleteFromStorage').and.resolveTo(); + const deleteFromStorageSpy = spyOn(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo(); - service.delete('songs/song-1/file.pdf', 'song-1', 'file-1'); - await Promise.resolve(); + await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1'); expect(deleteFromStorageSpy).toHaveBeenCalledWith('songs/song-1/file.pdf'); expect(fileDataServiceSpy.delete).toHaveBeenCalledWith('song-1', 'file-1'); diff --git a/src/app/modules/songs/services/song-data.service.spec.ts b/src/app/modules/songs/services/song-data.service.spec.ts index 2b543b6..70c0b38 100644 --- a/src/app/modules/songs/services/song-data.service.spec.ts +++ b/src/app/modules/songs/services/song-data.service.spec.ts @@ -1,6 +1,6 @@ import {TestBed} from '@angular/core/testing'; import {firstValueFrom, Subject} from 'rxjs'; -import {skip, take, toArray} from 'rxjs/operators'; +import {take, toArray} from 'rxjs/operators'; import {DbService} from '../../../services/db.service'; import {SongDataService} from './song-data.service'; @@ -14,7 +14,7 @@ describe('SongDataService', () => { let colSpy: jasmine.Spy; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { songs$ = new Subject>(); docUpdateSpy = jasmine.createSpy('update').and.resolveTo(); docDeleteSpy = jasmine.createSpy('delete').and.resolveTo(); @@ -32,7 +32,7 @@ describe('SongDataService', () => { dbServiceSpy.doc.and.callFake(docSpy); dbServiceSpy.col.and.callFake(colSpy); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/modules/songs/services/song-list.resolver.spec.ts b/src/app/modules/songs/services/song-list.resolver.spec.ts index cbd54b3..ecffb6c 100644 --- a/src/app/modules/songs/services/song-list.resolver.spec.ts +++ b/src/app/modules/songs/services/song-list.resolver.spec.ts @@ -7,11 +7,11 @@ describe('SongListResolver', () => { let resolver: SongListResolver; let songServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { songServiceSpy = jasmine.createSpyObj('SongService', ['list$']); songServiceSpy.list$.and.returnValue(of([{id: 'song-1', title: 'Amazing Grace'}] as never)); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: SongService, useValue: songServiceSpy}], }); diff --git a/src/app/modules/songs/services/song.service.spec.ts b/src/app/modules/songs/services/song.service.spec.ts index 8e3828f..27f3352 100644 --- a/src/app/modules/songs/services/song.service.spec.ts +++ b/src/app/modules/songs/services/song.service.spec.ts @@ -15,7 +15,7 @@ describe('SongService', () => { edits: [], } as never; - beforeEach(() => { + beforeEach(async () => { songDataServiceSpy = jasmine.createSpyObj('SongDataService', ['read$', 'update$', 'add', 'delete'], { list$: of([song]), }); @@ -27,7 +27,7 @@ describe('SongService', () => { songDataServiceSpy.delete.and.resolveTo(); userServiceSpy.currentUser.and.resolveTo({name: 'Benjamin'} as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: SongDataService, useValue: songDataServiceSpy}, {provide: UserService, useValue: userServiceSpy}, diff --git a/src/app/modules/songs/services/text-rendering.service.spec.ts b/src/app/modules/songs/services/text-rendering.service.spec.ts index 973e838..98c5183 100644 --- a/src/app/modules/songs/services/text-rendering.service.spec.ts +++ b/src/app/modules/songs/services/text-rendering.service.spec.ts @@ -333,11 +333,18 @@ Text`; void expect(sections[0].lines[0].text).toBe('Cmaj7(add9)'); void expect(sections[0].lines[0].chords).toEqual([ - {chord: 'C', length: 11, position: 0, add: 'maj7(add9)', slashChord: null, addDescriptor: descriptor('maj7(add9)', { - quality: 'major', - extensions: ['7'], - modifiers: ['(add9)'], - })}, + { + chord: 'C', + length: 11, + position: 0, + add: 'maj7(add9)', + slashChord: null, + addDescriptor: descriptor('maj7(add9)', { + quality: 'major', + extensions: ['7'], + modifiers: ['(add9)'], + }), + }, ]); void expect(service.validateChordNotation(text)).toEqual([]); }); @@ -459,9 +466,7 @@ Text`; void expect(sections[0].lines[0].type).toBe(LineType.chord); void expect(sections[0].lines[0].text).toBe('C Es G'); - void expect(service.validateChordNotation(text)).toEqual([ - jasmine.objectContaining({lineNumber: 2, token: 'Es', reason: 'alias', suggestion: 'Eb'}), - ]); + void expect(service.validateChordNotation(text)).toEqual([jasmine.objectContaining({lineNumber: 2, token: 'Es', reason: 'alias', suggestion: 'Eb'})]); }); it('should flag unknown tokens on mostly chord lines', () => { @@ -470,9 +475,7 @@ Text`; C Foo G a Text`; - void expect(service.validateChordNotation(text)).toEqual([ - jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null}), - ]); + void expect(service.validateChordNotation(text)).toEqual([jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null})]); }); it('should reject tabs on chord lines', () => { diff --git a/src/app/modules/songs/services/text-rendering.service.ts b/src/app/modules/songs/services/text-rendering.service.ts index b37bafb..ea1573c 100644 --- a/src/app/modules/songs/services/text-rendering.service.ts +++ b/src/app/modules/songs/services/text-rendering.service.ts @@ -7,7 +7,12 @@ import {LineType} from './line-type'; import {Chord, ChordAddDescriptor, ChordValidationIssue} from './chord'; import {Line} from './line'; -const CHORD_ROOT_DEFINITIONS = [ +type ChordRootDefinition = { + canonical: string; + aliases: string[]; +}; + +const CHORD_ROOT_DEFINITIONS: readonly ChordRootDefinition[] = [ {canonical: 'C#', aliases: ['Cis']}, {canonical: 'Db', aliases: ['Des']}, {canonical: 'D#', aliases: ['Dis']}, @@ -45,9 +50,12 @@ const CHORD_ROOT_DEFINITIONS = [ ] as const; const CANONICAL_CHORD_ROOTS = CHORD_ROOT_DEFINITIONS.map(entry => entry.canonical); -const ALTERNATIVE_CHORD_ROOTS = Object.fromEntries( - CHORD_ROOT_DEFINITIONS.flatMap(entry => entry.aliases.map(alias => [alias, entry.canonical])) -) as Record; +const ALTERNATIVE_CHORD_ROOTS = CHORD_ROOT_DEFINITIONS.reduce>((aliases, entry) => { + entry.aliases.forEach(alias => { + aliases[alias] = entry.canonical; + }); + return aliases; +}, {}); interface ParsedValidationToken { prefix: string; @@ -66,6 +74,11 @@ interface ChordLineValidationResult { isChordLike: boolean; } +interface ParsedTokenCandidate { + token: string; + parsed: ParsedValidationToken | null; +} + @Injectable({ providedIn: 'root', }) @@ -141,9 +154,7 @@ export class TextRenderingService { private getLineOfLineText(text: string, transpose: TransposeMode | null, lineNumber?: number): Line | null { if (!text) return null; - const validationResult = lineNumber - ? this.getChordLineValidationResult(text, lineNumber) - : {chords: [], issues: [], isStrictChordLine: false, isChordLike: false}; + const validationResult = lineNumber ? this.getChordLineValidationResult(text, lineNumber) : {chords: [], issues: [], isStrictChordLine: false, isChordLike: false}; const validationIssues = validationResult.issues; const hasMatches = validationResult.isStrictChordLine; const isChordLikeLine = hasMatches || validationResult.isChordLike; @@ -194,13 +205,12 @@ export class TextRenderingService { } private getChordLineValidationResult(line: string, lineNumber: number): ChordLineValidationResult { - const tokens = line.match(/\S+/g) ?? []; + const tokens: string[] = line.match(/\S+/g) ?? []; const chords = this.getParsedChords(line); - const parsedTokens = tokens - .map(token => ({ - token, - parsed: this.parseValidationToken(token), - })); + const parsedTokens: ParsedTokenCandidate[] = tokens.map(token => ({ + token, + parsed: this.parseValidationToken(token), + })); const recognizedTokens = parsedTokens.filter((entry): entry is {token: string; parsed: ParsedValidationToken} => entry.parsed !== null); @@ -227,12 +237,12 @@ export class TextRenderingService { ...issues, ...parsedTokens .map(entry => { - if (!entry.parsed) { - return this.createUnknownTokenIssue(line, lineNumber, entry.token); - } + if (!entry.parsed) { + return this.createUnknownTokenIssue(line, lineNumber, entry.token); + } - return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed); - }) + return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed); + }) .filter((issue): issue is ChordValidationIssue => issue !== null), ], isStrictChordLine, @@ -482,9 +492,7 @@ export class TextRenderingService { suffix = this.stripLeadingDurMarker(normalizedSuffix); } - const slashChord = parsed.slashChord - ? this.toMajorRoot(parsed.slashChord) - : null; + const slashChord = parsed.slashChord ? this.toMajorRoot(parsed.slashChord) : null; return parsed.prefix + root + suffix + (slashChord ? `/${slashChord}` : '') + parsed.tokenSuffix; } @@ -671,7 +679,7 @@ export class TextRenderingService { }; while (rest.length > 0) { - const additionMatch = rest.match(/^add([#b+\-]?\d+)/); + const additionMatch = rest.match(/^add([#b+-]?\d+)/); if (additionMatch) { descriptor.additions.push(additionMatch[1]); rest = rest.slice(additionMatch[0].length); @@ -692,7 +700,7 @@ export class TextRenderingService { continue; } - const alterationMatch = rest.match(/^[#b+\-]\d+/); + const alterationMatch = rest.match(/^[#b+-]\d+/); if (alterationMatch) { descriptor.alterations.push(alterationMatch[0]); rest = rest.slice(alterationMatch[0].length); diff --git a/src/app/modules/songs/services/transpose.service.spec.ts b/src/app/modules/songs/services/transpose.service.spec.ts index c31c87e..0c702bc 100644 --- a/src/app/modules/songs/services/transpose.service.spec.ts +++ b/src/app/modules/songs/services/transpose.service.spec.ts @@ -13,7 +13,6 @@ describe('TransposeService', () => { }); it('should create map upwards', () => { - const distance = service.getDistance('D', 'G'); const map = service.getMap('D', 'G'); if (map) { @@ -22,7 +21,6 @@ describe('TransposeService', () => { }); it('should create map downwards', () => { - const distance = service.getDistance('G', 'D'); const map = service.getMap('G', 'D'); if (map) { diff --git a/src/app/modules/songs/services/upload.service.spec.ts b/src/app/modules/songs/services/upload.service.spec.ts index f397560..42a17ef 100644 --- a/src/app/modules/songs/services/upload.service.spec.ts +++ b/src/app/modules/songs/services/upload.service.spec.ts @@ -7,12 +7,18 @@ import {UploadService} from './upload.service'; describe('UploadService', () => { let service: UploadService; let fileDataServiceSpy: jasmine.SpyObj; + type UploadTaskLike = { + on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => void; + }; + type UploadServiceInternals = UploadService & { + startUpload: (path: string, file: File) => UploadTaskLike; + }; - beforeEach(() => { + beforeEach(async () => { fileDataServiceSpy = jasmine.createSpyObj('FileDataService', ['set']); fileDataServiceSpy.set.and.resolveTo('file-1'); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Storage, useValue: {app: 'test-storage'}}, {provide: FileDataService, useValue: fileDataServiceSpy}, @@ -27,17 +33,16 @@ describe('UploadService', () => { }); it('should upload the file, update progress and persist file metadata on success', async () => { - const task = { + const task: UploadTaskLike = { on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => { progress({bytesTransferred: 50, totalBytes: 100}); success(); }, }; - const uploadSpy = spyOn(service, 'startUpload').and.returnValue(task as never); + const uploadSpy = spyOn(service as UploadServiceInternals, 'startUpload').and.returnValue(task); const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'})); - service.pushUpload('song-1', upload); - await Promise.resolve(); + await service.pushUpload('song-1', upload); expect(uploadSpy).toHaveBeenCalledWith('/attachments/song-1/test.pdf', upload.file); expect(upload.progress).toBe(50); diff --git a/src/app/services/config.service.spec.ts b/src/app/services/config.service.spec.ts index 6e270aa..5ef8a91 100644 --- a/src/app/services/config.service.spec.ts +++ b/src/app/services/config.service.spec.ts @@ -7,11 +7,11 @@ describe('ConfigService', () => { let service: ConfigService; let dbServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { dbServiceSpy = jasmine.createSpyObj('DbService', ['doc$']); dbServiceSpy.doc$.and.returnValue(of({copyright: 'CCLI'}) as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/services/global-settings.service.spec.ts b/src/app/services/global-settings.service.spec.ts index 9796556..794ed2e 100644 --- a/src/app/services/global-settings.service.spec.ts +++ b/src/app/services/global-settings.service.spec.ts @@ -8,13 +8,13 @@ describe('GlobalSettingsService', () => { let dbServiceSpy: jasmine.SpyObj; let updateSpy: jasmine.Spy; - beforeEach(() => { + beforeEach(async () => { updateSpy = jasmine.createSpy('update').and.resolveTo(); dbServiceSpy = jasmine.createSpyObj('DbService', ['doc$', 'doc']); dbServiceSpy.doc$.and.returnValue(of({churchName: 'ICF'}) as never); dbServiceSpy.doc.and.returnValue({update: updateSpy} as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [{provide: DbService, useValue: dbServiceSpy}], }); diff --git a/src/app/services/user/user-session.service.spec.ts b/src/app/services/user/user-session.service.spec.ts index 66c97a0..ca92e55 100644 --- a/src/app/services/user/user-session.service.spec.ts +++ b/src/app/services/user/user-session.service.spec.ts @@ -10,10 +10,10 @@ describe('UserSessionService', () => { let dbServiceSpy: jasmine.SpyObj; let routerSpy: jasmine.SpyObj; let authStateSubject: BehaviorSubject; - let createAuthStateSpy: jasmine.Spy; + let createAuthStateSpy: jasmine.Spy<() => ReturnType>; let runInFirebaseContextSpy: jasmine.Spy; - beforeEach(() => { + beforeEach(async () => { authStateSubject = new BehaviorSubject(null); dbServiceSpy = jasmine.createSpyObj('DbService', ['col$', 'doc$', 'doc']); routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']); @@ -34,9 +34,9 @@ describe('UserSessionService', () => { } as never); routerSpy.navigateByUrl.and.resolveTo(true); - createAuthStateSpy = spyOn(UserSessionService.prototype, 'createAuthState$').and.returnValue(authStateSubject.asObservable() as never); + createAuthStateSpy = spyOn(UserSessionService.prototype, 'createAuthState$').and.returnValue(authStateSubject.asObservable() as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: DbService, useValue: dbServiceSpy}, {provide: Router, useValue: routerSpy}, @@ -45,7 +45,7 @@ describe('UserSessionService', () => { }); service = TestBed.inject(UserSessionService); - runInFirebaseContextSpy = spyOn(service, 'runInFirebaseContext'); + runInFirebaseContextSpy = spyOn(service as UserSessionService & {runInFirebaseContext: (...args: unknown[]) => Promise}, 'runInFirebaseContext'); }); it('should be created', () => { @@ -63,9 +63,7 @@ describe('UserSessionService', () => { it('should resolve the current user document from auth state', async () => { authStateSubject.next({uid: 'user-1'}); - await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo( - jasmine.objectContaining({id: 'user-1', name: 'Benjamin'}) as never - ); + await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo(jasmine.objectContaining({id: 'user-1', name: 'Benjamin'}) as never); }); it('should cache user lookups by id', async () => { diff --git a/src/app/services/user/user-song-usage.service.spec.ts b/src/app/services/user/user-song-usage.service.spec.ts index 8583ba0..f962c32 100644 --- a/src/app/services/user/user-song-usage.service.spec.ts +++ b/src/app/services/user/user-song-usage.service.spec.ts @@ -13,7 +13,7 @@ describe('UserSongUsageService', () => { let showDataServiceSpy: jasmine.SpyObj; let showSongDataServiceSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { dbServiceSpy = jasmine.createSpyObj('DbService', ['doc']); sessionSpy = jasmine.createSpyObj('UserSessionService', ['update$'], { user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never, @@ -23,13 +23,18 @@ describe('UserSongUsageService', () => { showSongDataServiceSpy = jasmine.createSpyObj('ShowSongDataService', ['list$']); sessionSpy.update$.and.resolveTo(); - showDataServiceSpy.listRaw$.and.returnValue(of([{id: 'show-1', owner: 'user-1'}, {id: 'show-2', owner: 'user-2'}] as never)); + showDataServiceSpy.listRaw$.and.returnValue( + of([ + {id: 'show-1', owner: 'user-1'}, + {id: 'show-2', owner: 'user-2'}, + ] as never) + ); showSongDataServiceSpy.list$.and.callFake((showId: string) => of(showId === 'show-1' ? ([{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] as never) : ([{songId: 'song-3'}] as never)) ); dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: DbService, useValue: dbServiceSpy}, {provide: UserSessionService, useValue: sessionSpy}, diff --git a/src/app/services/user/user.service.spec.ts b/src/app/services/user/user.service.spec.ts index a5629f6..6f33dc4 100644 --- a/src/app/services/user/user.service.spec.ts +++ b/src/app/services/user/user.service.spec.ts @@ -9,7 +9,7 @@ describe('UserService', () => { let sessionSpy: jasmine.SpyObj; let songUsageSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { sessionSpy = jasmine.createSpyObj( 'UserSessionService', ['currentUser', 'getUserbyId', 'getUserbyId$', 'login', 'loggedIn$', 'list$', 'logout', 'update$', 'changePassword', 'createNewUser'], @@ -35,7 +35,7 @@ describe('UserService', () => { songUsageSpy.decSongCount.and.resolveTo(); songUsageSpy.rebuildSongUsage.and.resolveTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3}); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: UserSessionService, useValue: sessionSpy}, {provide: UserSongUsageService, useValue: songUsageSpy}, diff --git a/src/app/widget-modules/guards/role.guard.spec.ts b/src/app/widget-modules/guards/role.guard.spec.ts index 05a1f65..30f00b9 100644 --- a/src/app/widget-modules/guards/role.guard.spec.ts +++ b/src/app/widget-modules/guards/role.guard.spec.ts @@ -8,11 +8,11 @@ describe('RoleGuard', () => { let guard: RoleGuard; let routerSpy: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); routerSpy.createUrlTree.and.callFake(commands => ({commands}) as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Router, useValue: routerSpy}, {provide: UserService, useValue: {user$: of(null)}}, @@ -37,10 +37,10 @@ describe('RoleGuard', () => { }); }); - it('should allow admins regardless of requiredRoles', done => { + it('should allow admins regardless of requiredRoles', async done => { TestBed.resetTestingModule(); routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Router, useValue: routerSpy}, {provide: UserService, useValue: {user$: of({role: 'user;admin'})}}, @@ -54,10 +54,10 @@ describe('RoleGuard', () => { }); }); - it('should allow users with a matching required role', done => { + it('should allow users with a matching required role', async done => { TestBed.resetTestingModule(); routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Router, useValue: routerSpy}, {provide: UserService, useValue: {user$: of({role: 'leader;user'})}}, @@ -71,11 +71,11 @@ describe('RoleGuard', () => { }); }); - it('should redirect users without the required role to their role default route', done => { + it('should redirect users without the required role to their role default route', async done => { TestBed.resetTestingModule(); routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never); - void TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ providers: [ {provide: Router, useValue: routerSpy}, {provide: UserService, useValue: {user$: of({role: 'presenter'})}}, diff --git a/src/test.ts b/src/test.ts index 90f5e75..3d49554 100644 --- a/src/test.ts +++ b/src/test.ts @@ -17,6 +17,18 @@ import {environment} from './environments/environment'; import {DbService} from './app/services/db.service'; type req = {keys: () => {map: (context: req) => void}}; +type TestingModuleDefinition = Parameters[0]; +type TestingProviderList = NonNullable['providers']>; +type CollectionStub = { + valueChanges: () => ReturnType; + add: () => Promise<{id: string}>; +}; +type DocumentStub = { + set: () => Promise; + update: () => Promise; + delete: () => Promise; + collection: () => CollectionStub; +}; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); @@ -24,7 +36,7 @@ getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDyn const routeParams$ = new BehaviorSubject>({}); const queryParams$ = new BehaviorSubject>({}); -const defaultTestingProviders = [ +const defaultTestingProviders: TestingProviderList = [ provideNoopAnimations(), provideNativeDateAdapter(), provideRouter([]), @@ -49,26 +61,29 @@ const defaultTestingProviders = [ useValue: { col$: () => of([]), doc$: () => of(null), - col: () => ({ + col: (): CollectionStub => ({ valueChanges: () => of([]), - add: async () => ({id: 'test-id'}), + add: () => Promise.resolve({id: 'test-id'}), }), - doc: () => ({ - set: async () => void 0, - update: async () => void 0, - delete: async () => void 0, - collection: () => ({ + doc: (): DocumentStub => ({ + set: () => Promise.resolve(), + update: () => Promise.resolve(), + delete: () => Promise.resolve(), + collection: (): CollectionStub => ({ valueChanges: () => of([]), - add: async () => ({id: 'test-id'}), + add: () => Promise.resolve({id: 'test-id'}), }), }), }, }, ]; -const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed); -TestBed.configureTestingModule = ((moduleDef?: Parameters[0]) => - originalConfigureTestingModule({ - ...moduleDef, - providers: [...defaultTestingProviders, ...(moduleDef?.providers ?? [])], - })) as typeof TestBed.configureTestingModule; +const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed) as typeof TestBed.configureTestingModule; +const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => { + const extraProviders: TestingProviderList = moduleDef?.providers ?? []; + const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {}; + mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders); + + return originalConfigureTestingModule(mergedModuleDef); +}; +TestBed.configureTestingModule = configureTestingModule;