global filter

This commit is contained in:
2026-03-11 18:04:42 +01:00
parent c2bcac58b3
commit 196e8c80d8
25 changed files with 192 additions and 136 deletions

View File

@@ -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"
}
}
]
}

View File

@@ -12,7 +12,7 @@ describe('GuestShowDataService', () => {
let colSpy: jasmine.Spy;
let dbServiceSpy: jasmine.SpyObj<DbService>;
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}],
});

View File

@@ -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<GuestShowDataService>;
let showServiceSpy: jasmine.SpyObj<ShowService>;
beforeEach(() => {
beforeEach(async () => {
guestShowDataServiceSpy = jasmine.createSpyObj<GuestShowDataService>('GuestShowDataService', ['add', 'update$']);
showServiceSpy = jasmine.createSpyObj<ShowService>('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);

View File

@@ -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);
}

View File

@@ -4,9 +4,14 @@ import {DocxService} from './docx.service';
describe('DocxService', () => {
let service: DocxService;
type DocxServiceInternals = DocxService & {
prepareData: (showId: string) => Promise<unknown>;
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<any>(service, 'prepareData').and.resolveTo(null);
const saveAsSpy = spyOn<any>(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<any>(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<any>(service, 'prepareNewDocument').and.returnValue({doc: true});
const saveAsSpy = spyOn<any>(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});

View File

@@ -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<DbService>;
beforeEach(() => {
beforeEach(async () => {
shows$ = new Subject<Array<{id: string; date: {toMillis: () => 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));

View File

@@ -12,7 +12,7 @@ describe('ShowSongDataService', () => {
let colSpy: jasmine.Spy;
let dbServiceSpy: jasmine.SpyObj<DbService>;
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}],
});

View File

@@ -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<SongDataService>;
let userServiceSpy: jasmine.SpyObj<UserService>;
let showServiceSpy: jasmine.SpyObj<ShowService>;
let user$: BehaviorSubject<any>;
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<User | null>;
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<any>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}});
beforeEach(async () => {
user$ = new BehaviorSubject<User | null>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}});
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['add', 'read$', 'list$', 'delete', 'update$']);
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$']);
userServiceSpy = jasmine.createSpyObj<UserService>('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},

View File

@@ -14,7 +14,7 @@ describe('ShowService', () => {
{id: 'show-3', owner: 'user-1', published: true, archived: true},
] as never;
beforeEach(() => {
beforeEach(async () => {
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('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()}},

View File

@@ -12,7 +12,7 @@ describe('FileDataService', () => {
let fileDeleteSpy: jasmine.Spy;
let dbServiceSpy: jasmine.SpyObj<DbService>;
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>('DbService', ['doc']);
dbServiceSpy.doc.and.callFake(songDocSpy);
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [{provide: DbService, useValue: dbServiceSpy}],
});

View File

@@ -6,12 +6,16 @@ import {FileService} from './file.service';
describe('FileService', () => {
let service: FileService;
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
type FileServiceInternals = FileService & {
resolveDownloadUrl: (path: string) => Promise<string>;
deleteFromStorage: (path: string) => Promise<void>;
};
beforeEach(() => {
beforeEach(async () => {
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('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<any>(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<any>(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');

View File

@@ -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<DbService>;
beforeEach(() => {
beforeEach(async () => {
songs$ = new Subject<Array<{id: string; title: string}>>();
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}],
});

View File

@@ -7,11 +7,11 @@ describe('SongListResolver', () => {
let resolver: SongListResolver;
let songServiceSpy: jasmine.SpyObj<SongService>;
beforeEach(() => {
beforeEach(async () => {
songServiceSpy = jasmine.createSpyObj<SongService>('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}],
});

View File

@@ -15,7 +15,7 @@ describe('SongService', () => {
edits: [],
} as never;
beforeEach(() => {
beforeEach(async () => {
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('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},

View File

@@ -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)', {
{
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', () => {

View File

@@ -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<string, string>;
const ALTERNATIVE_CHORD_ROOTS = CHORD_ROOT_DEFINITIONS.reduce<Record<string, string>>((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,10 +205,9 @@ 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 => ({
const parsedTokens: ParsedTokenCandidate[] = tokens.map(token => ({
token,
parsed: this.parseValidationToken(token),
}));
@@ -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);

View File

@@ -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) {

View File

@@ -7,12 +7,18 @@ import {UploadService} from './upload.service';
describe('UploadService', () => {
let service: UploadService;
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
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>('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<any>(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);

View File

@@ -7,11 +7,11 @@ describe('ConfigService', () => {
let service: ConfigService;
let dbServiceSpy: jasmine.SpyObj<DbService>;
beforeEach(() => {
beforeEach(async () => {
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$']);
dbServiceSpy.doc$.and.returnValue(of({copyright: 'CCLI'}) as never);
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [{provide: DbService, useValue: dbServiceSpy}],
});

View File

@@ -8,13 +8,13 @@ describe('GlobalSettingsService', () => {
let dbServiceSpy: jasmine.SpyObj<DbService>;
let updateSpy: jasmine.Spy;
beforeEach(() => {
beforeEach(async () => {
updateSpy = jasmine.createSpy('update').and.resolveTo();
dbServiceSpy = jasmine.createSpyObj<DbService>('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}],
});

View File

@@ -10,10 +10,10 @@ describe('UserSessionService', () => {
let dbServiceSpy: jasmine.SpyObj<DbService>;
let routerSpy: jasmine.SpyObj<Router>;
let authStateSubject: BehaviorSubject<unknown>;
let createAuthStateSpy: jasmine.Spy;
let createAuthStateSpy: jasmine.Spy<() => ReturnType<UserSessionService['createAuthState$']>>;
let runInFirebaseContextSpy: jasmine.Spy;
beforeEach(() => {
beforeEach(async () => {
authStateSubject = new BehaviorSubject<unknown>(null);
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc']);
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
@@ -34,9 +34,9 @@ describe('UserSessionService', () => {
} as never);
routerSpy.navigateByUrl.and.resolveTo(true);
createAuthStateSpy = spyOn<any>(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<any>(service, 'runInFirebaseContext');
runInFirebaseContextSpy = spyOn(service as UserSessionService & {runInFirebaseContext: (...args: unknown[]) => Promise<unknown>}, '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 () => {

View File

@@ -13,7 +13,7 @@ describe('UserSongUsageService', () => {
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
beforeEach(() => {
beforeEach(async () => {
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never,
@@ -23,13 +23,18 @@ describe('UserSongUsageService', () => {
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('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},

View File

@@ -9,7 +9,7 @@ describe('UserService', () => {
let sessionSpy: jasmine.SpyObj<UserSessionService>;
let songUsageSpy: jasmine.SpyObj<UserSongUsageService>;
beforeEach(() => {
beforeEach(async () => {
sessionSpy = jasmine.createSpyObj<UserSessionService>(
'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},

View File

@@ -8,11 +8,11 @@ describe('RoleGuard', () => {
let guard: RoleGuard;
let routerSpy: jasmine.SpyObj<Router>;
beforeEach(() => {
beforeEach(async () => {
routerSpy = jasmine.createSpyObj<Router>('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>('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>('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>('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'})}},

View File

@@ -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<typeof TestBed.configureTestingModule>[0];
type TestingProviderList = NonNullable<NonNullable<TestingModuleDefinition>['providers']>;
type CollectionStub = {
valueChanges: () => ReturnType<typeof of>;
add: () => Promise<{id: string}>;
};
type DocumentStub = {
set: () => Promise<void>;
update: () => Promise<void>;
delete: () => Promise<void>;
collection: () => CollectionStub;
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
@@ -24,7 +36,7 @@ getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDyn
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
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<typeof TestBed.configureTestingModule>[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;