migrate angular 21 tests
This commit is contained in:
@@ -1,12 +1,68 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {of} from 'rxjs';
|
||||
import {DbService} from '../../../services/db.service';
|
||||
import {FileDataService} from './file-data.service';
|
||||
|
||||
describe('FileDataService', () => {
|
||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
||||
let service: FileDataService;
|
||||
let filesCollectionValueChangesSpy: jasmine.Spy;
|
||||
let filesCollectionAddSpy: jasmine.Spy;
|
||||
let songDocCollectionSpy: jasmine.Spy;
|
||||
let songDocSpy: jasmine.Spy;
|
||||
let fileDeleteSpy: jasmine.Spy;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
|
||||
beforeEach(() => {
|
||||
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({
|
||||
add: filesCollectionAddSpy,
|
||||
valueChanges: filesCollectionValueChangesSpy,
|
||||
});
|
||||
songDocSpy = jasmine.createSpy('songDoc').and.callFake((path: string) => {
|
||||
if (path.includes('/files/')) {
|
||||
return {delete: fileDeleteSpy};
|
||||
}
|
||||
|
||||
return {collection: songDocCollectionSpy};
|
||||
});
|
||||
fileDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
||||
dbServiceSpy.doc.and.callFake(songDocSpy);
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
});
|
||||
|
||||
service = TestBed.inject(FileDataService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
const service: FileDataService = TestBed.inject(FileDataService);
|
||||
void expect(service).toBeTruthy();
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add files to the song files subcollection and return the generated id', async () => {
|
||||
const file = {name: 'setlist.pdf', path: 'songs/song-1/files', createdAt: new Date()};
|
||||
|
||||
await expectAsync(service.set('song-1', file)).toBeResolvedTo('file-2');
|
||||
|
||||
expect(songDocSpy).toHaveBeenCalledWith('songs/song-1');
|
||||
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||
expect(filesCollectionAddSpy).toHaveBeenCalledWith(file);
|
||||
});
|
||||
|
||||
it('should delete files from the nested file document path', async () => {
|
||||
await service.delete('song-4', 'file-9');
|
||||
|
||||
expect(songDocSpy).toHaveBeenCalledWith('songs/song-4/files/file-9');
|
||||
expect(fileDeleteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should read files from the files subcollection with the id field attached', () => {
|
||||
service.read$('song-3').subscribe();
|
||||
|
||||
expect(songDocSpy).toHaveBeenCalledWith('songs/song-3');
|
||||
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||
expect(filesCollectionValueChangesSpy).toHaveBeenCalledWith({idField: 'id'});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||
import {deleteObject, getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
||||
import {from, Observable} from 'rxjs';
|
||||
import {FileDataService} from './file-data.service';
|
||||
@@ -9,13 +9,14 @@ import {FileDataService} from './file-data.service';
|
||||
export class FileService {
|
||||
private storage = inject(Storage);
|
||||
private fileDataService = inject(FileDataService);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
public getDownloadUrl(path: string): Observable<string> {
|
||||
return from(getDownloadURL(ref(this.storage, path)));
|
||||
return from(runInInjectionContext(this.environmentInjector, () => getDownloadURL(ref(this.storage, path))));
|
||||
}
|
||||
|
||||
public delete(path: string, songId: string, fileId: string): void {
|
||||
void deleteObject(ref(this.storage, path));
|
||||
void runInInjectionContext(this.environmentInjector, () => deleteObject(ref(this.storage, path)));
|
||||
void this.fileDataService.delete(songId, fileId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,104 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {SongDataService} from './song-data.service';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {firstValueFrom, Subject} from 'rxjs';
|
||||
import {skip, take, toArray} from 'rxjs/operators';
|
||||
import {DbService} from '../../../services/db.service';
|
||||
import {SongDataService} from './song-data.service';
|
||||
|
||||
describe('SongDataService', () => {
|
||||
const songs = [{title: 'title1'}];
|
||||
let service: SongDataService;
|
||||
let songs$: Subject<Array<{id: string; title: string}>>;
|
||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
||||
let docSpy: jasmine.Spy;
|
||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||
let colSpy: jasmine.Spy;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
|
||||
const mockDbService = {
|
||||
col$: () => of(songs),
|
||||
};
|
||||
beforeEach(() => {
|
||||
songs$ = new Subject<Array<{id: string; title: string}>>();
|
||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||
docSpy = jasmine.createSpy('doc').and.callFake(() => ({
|
||||
update: docUpdateSpy,
|
||||
delete: docDeleteSpy,
|
||||
}));
|
||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'song-3'});
|
||||
colSpy = jasmine.createSpy('col').and.returnValue({
|
||||
add: colAddSpy,
|
||||
});
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||
dbServiceSpy.col$.and.returnValue(songs$.asObservable());
|
||||
dbServiceSpy.doc$.and.returnValue(songs$.asObservable() as never);
|
||||
dbServiceSpy.doc.and.callFake(docSpy);
|
||||
dbServiceSpy.col.and.callFake(colSpy);
|
||||
|
||||
beforeEach(
|
||||
() =>
|
||||
void TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: mockDbService}],
|
||||
})
|
||||
);
|
||||
void TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
});
|
||||
|
||||
service = TestBed.inject(SongDataService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
const service: SongDataService = TestBed.inject(SongDataService);
|
||||
void expect(service).toBeTruthy();
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should list songs', async () => {
|
||||
const service: SongDataService = TestBed.inject(SongDataService);
|
||||
const list = await firstValueFrom(service.list$);
|
||||
void expect(list[0].title).toEqual('title1');
|
||||
it('should load songs from the songs collection once on creation', () => {
|
||||
expect(dbServiceSpy.col$).toHaveBeenCalledTimes(1);
|
||||
expect(dbServiceSpy.col$).toHaveBeenCalledWith('songs');
|
||||
});
|
||||
|
||||
it('should emit an empty list first and then the loaded songs', async () => {
|
||||
const emissionsPromise = firstValueFrom(service.list$.pipe(take(2), toArray()));
|
||||
|
||||
songs$.next([{id: 'song-1', title: 'Amazing Grace'}]);
|
||||
|
||||
const emissions = await emissionsPromise;
|
||||
|
||||
expect(emissions[0]).toEqual([]);
|
||||
expect(emissions[1].map(song => song.id)).toEqual(['song-1']);
|
||||
expect(emissions[1].map(song => song.title)).toEqual(['Amazing Grace']);
|
||||
});
|
||||
|
||||
it('should replay the latest songs to late subscribers', async () => {
|
||||
const initialSubscription = service.list$.subscribe();
|
||||
|
||||
songs$.next([{id: 'song-2', title: 'How Great'}]);
|
||||
|
||||
const latestSongs = await firstValueFrom(service.list$.pipe(take(1)));
|
||||
|
||||
expect(latestSongs.map(song => song.id)).toEqual(['song-2']);
|
||||
expect(latestSongs.map(song => song.title)).toEqual(['How Great']);
|
||||
|
||||
initialSubscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should read a single song by id', () => {
|
||||
service.read$('song-7');
|
||||
|
||||
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('songs/song-7');
|
||||
});
|
||||
|
||||
it('should update a song at the expected document path', async () => {
|
||||
await service.update$('song-8', {title: 'Updated'});
|
||||
|
||||
expect(docSpy).toHaveBeenCalledWith('songs/song-8');
|
||||
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
expect(updatePayload).toEqual({title: 'Updated'});
|
||||
});
|
||||
|
||||
it('should add a song to the songs collection and return the new id', async () => {
|
||||
await expectAsync(service.add({title: 'New Song'})).toBeResolvedTo('song-3');
|
||||
|
||||
expect(colSpy).toHaveBeenCalledWith('songs');
|
||||
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual({title: 'New Song'});
|
||||
});
|
||||
|
||||
it('should delete a song at the expected document path', async () => {
|
||||
await service.delete('song-9');
|
||||
|
||||
expect(docSpy).toHaveBeenCalledWith('songs/song-9');
|
||||
expect(docDeleteSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,100 @@
|
||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import {SongService} from './song.service';
|
||||
import {SongDataService} from './song-data.service';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {SongDataService} from './song-data.service';
|
||||
import {SongService} from './song.service';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {Timestamp} from '@angular/fire/firestore';
|
||||
|
||||
describe('SongService', () => {
|
||||
const songs = [{title: 'title1'}];
|
||||
let service: SongService;
|
||||
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||
const song = {
|
||||
id: 'song-1',
|
||||
title: 'Amazing Grace',
|
||||
edits: [],
|
||||
} as never;
|
||||
|
||||
const mockSongDataService = {
|
||||
list: () => of(songs),
|
||||
};
|
||||
beforeEach(() => {
|
||||
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$', 'update$', 'add', 'delete'], {
|
||||
list$: of([song]),
|
||||
});
|
||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['currentUser']);
|
||||
|
||||
beforeEach(
|
||||
() =>
|
||||
void TestBed.configureTestingModule({
|
||||
providers: [{provide: SongDataService, useValue: mockSongDataService}],
|
||||
})
|
||||
);
|
||||
songDataServiceSpy.read$.and.returnValue(of(song));
|
||||
songDataServiceSpy.update$.and.resolveTo();
|
||||
songDataServiceSpy.add.and.resolveTo('song-2');
|
||||
songDataServiceSpy.delete.and.resolveTo();
|
||||
userServiceSpy.currentUser.and.resolveTo({name: 'Benjamin'} as never);
|
||||
|
||||
it('should be created', () => {
|
||||
const service: SongService = TestBed.inject(SongService);
|
||||
void expect(service).toBeTruthy();
|
||||
void TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: SongDataService, useValue: songDataServiceSpy},
|
||||
{provide: UserService, useValue: userServiceSpy},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(SongService);
|
||||
});
|
||||
|
||||
it('should list songs', waitForAsync(() => {
|
||||
const service: SongService = TestBed.inject(SongService);
|
||||
service.list$().subscribe(s => {
|
||||
void expect(s[0].title).toEqual('title1');
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should list songs from the data service', done => {
|
||||
service.list$().subscribe(songs => {
|
||||
expect(songs).toEqual([song]);
|
||||
done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should delegate reads to the data service', async () => {
|
||||
await expectAsync(service.read('song-1')).toBeResolvedTo(song);
|
||||
expect(songDataServiceSpy.read$).toHaveBeenCalledWith('song-1');
|
||||
});
|
||||
|
||||
it('should append an edit with the current user when updating a song', async () => {
|
||||
const timestamp = {seconds: 1} as never;
|
||||
spyOn(Timestamp, 'now').and.returnValue(timestamp);
|
||||
|
||||
await service.update$('song-1', {title: 'Updated'});
|
||||
|
||||
expect(songDataServiceSpy.update$).toHaveBeenCalled();
|
||||
const [, payload] = songDataServiceSpy.update$.calls.mostRecent().args as unknown as [string, Record<string, unknown>];
|
||||
expect(payload.title).toBe('Updated');
|
||||
expect(payload.edits).toEqual([{username: 'Benjamin', timestamp}]);
|
||||
});
|
||||
|
||||
it('should not update when the song does not exist', async () => {
|
||||
songDataServiceSpy.read$.and.returnValue(of(null));
|
||||
|
||||
await service.update$('missing-song', {title: 'Updated'});
|
||||
|
||||
expect(songDataServiceSpy.update$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update when no current user is available', async () => {
|
||||
userServiceSpy.currentUser.and.resolveTo(null);
|
||||
|
||||
await service.update$('song-1', {title: 'Updated'});
|
||||
|
||||
expect(songDataServiceSpy.update$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create new songs with the expected defaults', async () => {
|
||||
await expectAsync(service.new(42, 'New Song')).toBeResolvedTo('song-2');
|
||||
|
||||
expect(songDataServiceSpy.add).toHaveBeenCalledWith({
|
||||
number: 42,
|
||||
title: 'New Song',
|
||||
status: 'draft',
|
||||
legalType: 'open',
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete songs via the data service', async () => {
|
||||
await service.delete('song-1');
|
||||
|
||||
expect(songDataServiceSpy.delete).toHaveBeenCalledWith('song-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ Cool bridge without any chords
|
||||
void expect(sections[1].lines[2].type).toBe(LineType.chord);
|
||||
void expect(sections[1].lines[2].text).toBe(' a d e f g a h c b');
|
||||
void expect(sections[2].lines[0].type).toBe(LineType.chord);
|
||||
void expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
|
||||
void expect(sections[2].lines[0].text).toBe('c c♯ d♭ c7 cmaj7 c/e');
|
||||
|
||||
// c c# db c7 cmaj7 c/e
|
||||
void expect(sections[2].lines[0].chords).toEqual([
|
||||
@@ -93,7 +93,7 @@ g# F# E g# F# E
|
||||
text`;
|
||||
const sections = service.parse(text, null);
|
||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||
void expect(sections[0].lines[0].text).toBe('g# F# E g# F# E');
|
||||
void expect(sections[0].lines[0].text).toBe('g♯ F♯ E g♯ F♯ E');
|
||||
void expect(sections[0].lines[1].type).toBe(LineType.text);
|
||||
void expect(sections[0].lines[1].text).toBe('text');
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('TransposeService', () => {
|
||||
|
||||
void expect(distance).toBe(1);
|
||||
void expect(map?.['H']).toBe('C');
|
||||
void expect(map?.['B']).toBe('C#');
|
||||
void expect(map?.['B']).toBe('H');
|
||||
});
|
||||
|
||||
it('should render unknown chords as X', () => {
|
||||
|
||||
@@ -11,6 +11,19 @@ type ScaleVariants = [string[], string[]];
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TransposeService {
|
||||
private readonly enharmonicAliases: Array<[string, string]> = [
|
||||
['C#', 'Db'],
|
||||
['D#', 'Eb'],
|
||||
['F#', 'Gb'],
|
||||
['G#', 'Ab'],
|
||||
['A#', 'B'],
|
||||
['c#', 'db'],
|
||||
['d#', 'eb'],
|
||||
['f#', 'gb'],
|
||||
['g#', 'ab'],
|
||||
['a#', 'b'],
|
||||
];
|
||||
|
||||
private readonly keyToSemitone: Record<string, number> = {
|
||||
C: 0,
|
||||
'C#': 1,
|
||||
@@ -105,6 +118,15 @@ export class TransposeService {
|
||||
}
|
||||
});
|
||||
|
||||
this.enharmonicAliases.forEach(([left, right]) => {
|
||||
if (map[left] && !map[right]) {
|
||||
map[right] = map[left];
|
||||
}
|
||||
if (map[right] && !map[left]) {
|
||||
map[left] = map[right];
|
||||
}
|
||||
});
|
||||
|
||||
this.mapCache.set(cacheKey, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||
import {Upload} from './upload';
|
||||
import {FileDataService} from './file-data.service';
|
||||
import {ref, Storage, uploadBytesResumable} from '@angular/fire/storage';
|
||||
@@ -11,14 +11,19 @@ import {FileServer} from './fileServer';
|
||||
export class UploadService extends FileBase {
|
||||
private fileDataService = inject(FileDataService);
|
||||
private storage = inject(Storage);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
public pushUpload(songId: string, upload: Upload): void {
|
||||
const directory = this.directory(songId);
|
||||
const filePath = `${directory}/${upload.file.name}`;
|
||||
upload.path = directory;
|
||||
|
||||
const storageRef = ref(this.storage, filePath);
|
||||
const task = uploadBytesResumable(storageRef, upload.file);
|
||||
const {task} = runInInjectionContext(this.environmentInjector, () => {
|
||||
const storageRef = ref(this.storage, filePath);
|
||||
return {
|
||||
task: uploadBytesResumable(storageRef, upload.file),
|
||||
};
|
||||
});
|
||||
|
||||
task.on(
|
||||
'state_changed',
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('SongListComponent', () => {
|
||||
const songs = [{title: 'title1'}];
|
||||
|
||||
const mockSongService = {
|
||||
list: () => of(songs),
|
||||
list$: () => of(songs),
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('SaveDialogComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
void TestBed.configureTestingModule({
|
||||
declarations: [SaveDialogComponent],
|
||||
imports: [SaveDialogComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, Input, inject} from '@angular/core';
|
||||
import {Component, EnvironmentInjector, Input, inject, runInInjectionContext} from '@angular/core';
|
||||
import {File} from '../../services/file';
|
||||
import {getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
||||
import {from, Observable} from 'rxjs';
|
||||
@@ -12,13 +12,14 @@ import {AsyncPipe} from '@angular/common';
|
||||
})
|
||||
export class FileComponent {
|
||||
private storage = inject(Storage);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
public url$: Observable<string> | null = null;
|
||||
public name = '';
|
||||
|
||||
@Input()
|
||||
public set file(file: File) {
|
||||
this.url$ = from(getDownloadURL(ref(this.storage, file.path + '/' + file.name)));
|
||||
this.url$ = from(runInInjectionContext(this.environmentInjector, () => getDownloadURL(ref(this.storage, file.path + '/' + file.name))));
|
||||
this.name = file.name;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user