fix unit tests
This commit is contained in:
@@ -1,33 +1,36 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Firestore} from '@angular/fire/firestore';
|
||||
import {of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {DbService} from 'src/app/services/db.service';
|
||||
import {GuestShowDataService} from './guest-show-data.service';
|
||||
|
||||
describe('GuestShowDataService', () => {
|
||||
type DocumentRefStub = {update: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn>};
|
||||
type CollectionRefStub = {add: ReturnType<typeof vi.fn>};
|
||||
type PathSpy<T> = ReturnType<typeof vi.fn> & ((path: string) => T);
|
||||
|
||||
let service: GuestShowDataService;
|
||||
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>;
|
||||
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||
let docDeleteSpy: ReturnType<typeof vi.fn>;
|
||||
let docSpy: PathSpy<DocumentRefStub>;
|
||||
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||
let colSpy: PathSpy<CollectionRefStub>;
|
||||
let dbServiceSpy: {col$: ReturnType<typeof vi.fn>; doc$: ReturnType<typeof vi.fn>; doc: ReturnType<typeof vi.fn>; col: ReturnType<typeof vi.fn>};
|
||||
let firestoreStub: Firestore;
|
||||
|
||||
beforeEach(async () => {
|
||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||
docSpy = jasmine.createSpy('doc').and.returnValue({
|
||||
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docSpy = vi.fn().mockReturnValue({
|
||||
update: docUpdateSpy,
|
||||
delete: docDeleteSpy,
|
||||
});
|
||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'guest-2'});
|
||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||
dbServiceSpy.col$.and.returnValue(of([{id: 'guest-1'}]) as never);
|
||||
dbServiceSpy.doc$.and.returnValue(of({id: 'guest-1'}) as never);
|
||||
dbServiceSpy.doc.and.callFake(docSpy);
|
||||
dbServiceSpy.col.and.callFake(colSpy);
|
||||
}) as PathSpy<DocumentRefStub>;
|
||||
colAddSpy = vi.fn().mockResolvedValue({id: 'guest-2'});
|
||||
colSpy = vi.fn().mockReturnValue({add: colAddSpy}) as PathSpy<CollectionRefStub>;
|
||||
dbServiceSpy = {col$: vi.fn(), doc$: vi.fn(), doc: docSpy, col: colSpy};
|
||||
dbServiceSpy.col$.mockReturnValue(of([{id: 'guest-1'}]) as never);
|
||||
dbServiceSpy.doc$.mockReturnValue(of({id: 'guest-1'}) as never);
|
||||
firestoreStub = {} as Firestore;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -60,15 +63,15 @@ describe('GuestShowDataService', () => {
|
||||
await service.update$('guest-7', {published: true} as never);
|
||||
|
||||
expect(docSpy).toHaveBeenCalledWith('guest/guest-7');
|
||||
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [updatePayload] = docUpdateSpy.mock.calls.at(-1) as [Record<string, unknown>];
|
||||
expect(updatePayload).toEqual({published: true});
|
||||
});
|
||||
|
||||
it('should add a guest show and return the created id', async () => {
|
||||
await expectAsync(service.add({published: false} as never)).toBeResolvedTo('guest-2');
|
||||
await expect(service.add({published: false} as never)).resolves.toEqual('guest-2');
|
||||
|
||||
expect(colSpy).toHaveBeenCalledWith('guest');
|
||||
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [addPayload] = colAddSpy.mock.calls.at(-1) as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual({published: false});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ 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';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
describe('GuestShowService', () => {
|
||||
let service: GuestShowService;
|
||||
let guestShowDataServiceSpy: jasmine.SpyObj<GuestShowDataService>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let guestShowDataServiceSpy: {add: ReturnType<typeof vi.fn>; update$: ReturnType<typeof vi.fn>};
|
||||
let showServiceSpy: {update$: ReturnType<typeof vi.fn>};
|
||||
|
||||
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();
|
||||
guestShowDataServiceSpy = {add: vi.fn(), update$: vi.fn()};
|
||||
showServiceSpy = {update$: vi.fn()};
|
||||
guestShowDataServiceSpy.add.mockResolvedValue('share-1');
|
||||
guestShowDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -36,17 +37,17 @@ describe('GuestShowService', () => {
|
||||
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);
|
||||
await expect(service.share(show, songs)).resolves.toEqual(expectedUrl);
|
||||
|
||||
const [addPayload] = guestShowDataServiceSpy.add.calls.mostRecent().args as [Record<string, unknown>];
|
||||
const [addPayload] = guestShowDataServiceSpy.add.mock.calls.at(-1) as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual(
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
showType: 'service-worship',
|
||||
date: show.date,
|
||||
songs,
|
||||
})
|
||||
);
|
||||
expect(addPayload['updatedAt']).toEqual(jasmine.any(Date));
|
||||
expect(addPayload['updatedAt']).toEqual(expect.any(Date));
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {shareId: 'share-1'});
|
||||
});
|
||||
|
||||
@@ -55,18 +56,18 @@ describe('GuestShowService', () => {
|
||||
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);
|
||||
await expect(service.share(show, songs)).resolves.toEqual(expectedUrl);
|
||||
|
||||
const [shareId, updatePayload] = guestShowDataServiceSpy.update$.calls.mostRecent().args as [string, Record<string, unknown>];
|
||||
const [shareId, updatePayload] = guestShowDataServiceSpy.update$.mock.calls.at(-1) as [string, Record<string, unknown>];
|
||||
expect(shareId).toBe('share-9');
|
||||
expect(updatePayload).toEqual(
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
showType: 'service-worship',
|
||||
date: show.date,
|
||||
songs,
|
||||
})
|
||||
);
|
||||
expect(updatePayload['updatedAt']).toEqual(jasmine.any(Date));
|
||||
expect(updatePayload['updatedAt']).toEqual(expect.any(Date));
|
||||
expect(guestShowDataServiceSpy.add).not.toHaveBeenCalled();
|
||||
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {GuestComponent} from './guest.component';
|
||||
import {GuestShowDataService} from './guest-show-data.service';
|
||||
|
||||
describe('GuestComponent', () => {
|
||||
let component: GuestComponent;
|
||||
let fixture: ComponentFixture<GuestComponent>;
|
||||
let guestShowDataServiceSpy: jasmine.SpyObj<GuestShowDataService>;
|
||||
let guestShowDataServiceSpy: {read: ReturnType<typeof vi.fn>; read$: ReturnType<typeof vi.fn>};
|
||||
let guestShowSubject: BehaviorSubject<unknown>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -26,8 +27,8 @@ describe('GuestComponent', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
guestShowDataServiceSpy = jasmine.createSpyObj<GuestShowDataService>('GuestShowDataService', ['read', 'read$']);
|
||||
guestShowDataServiceSpy.read.and.resolveTo({
|
||||
guestShowDataServiceSpy = {read: vi.fn(), read$: vi.fn()};
|
||||
guestShowDataServiceSpy.read.mockResolvedValue({
|
||||
id: 'guest-1',
|
||||
showType: 'service-worship',
|
||||
date: {
|
||||
@@ -42,7 +43,7 @@ describe('GuestComponent', () => {
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
guestShowDataServiceSpy.read$.and.returnValue(guestShowSubject.asObservable() as never);
|
||||
guestShowDataServiceSpy.read$.mockReturnValue(guestShowSubject.asObservable() as never);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GuestComponent],
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<app-page-frame title="Hilfe" [withMenu]="false">
|
||||
<div content>
|
||||
<app-card [heading]="heading">
|
||||
<div (click)="onContentClick($event)" [innerHTML]="renderedHtml" class="help-content"></div>
|
||||
<div (click)="onContentClick($event)"
|
||||
(keydown)="onContentKeydown($event)"
|
||||
[innerHTML]="renderedHtml"
|
||||
class="help-content"
|
||||
tabindex="0"></div>
|
||||
|
||||
<!-- @if (loading && !hasLoadedContent) {-->
|
||||
<!-- <div class="help-state">Hilfe wird geladen.</div>-->
|
||||
|
||||
@@ -6,6 +6,7 @@ describe('HelpComponent', () => {
|
||||
let component: HelpComponent;
|
||||
let fixture: ComponentFixture<HelpComponent>;
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
type FetchResponse = {ok: boolean; text: () => Promise<string>};
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock = vi.fn().mockResolvedValue({
|
||||
@@ -40,11 +41,11 @@ describe('HelpComponent', () => {
|
||||
});
|
||||
|
||||
it('should keep the current content visible while the next page loads', async () => {
|
||||
let resolveFetch: ((value: {ok: boolean; text: () => Promise<string>}) => void) | null = null;
|
||||
let resolveFetch!: (value: FetchResponse) => void;
|
||||
|
||||
fetchMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
new Promise<FetchResponse>(resolve => {
|
||||
resolveFetch = resolve;
|
||||
})
|
||||
);
|
||||
@@ -52,19 +53,21 @@ describe('HelpComponent', () => {
|
||||
const oldHtml = component.renderedHtml;
|
||||
const oldHeading = component.heading;
|
||||
|
||||
void (component as {loadDocument(path: string): Promise<void>}).loadDocument('man/pages/lieder-liste.md');
|
||||
const loadPromise = (component as unknown as {loadDocument(path: string): Promise<void>}).loadDocument(
|
||||
'man/pages/lieder-liste.md'
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.loading).toBe(true);
|
||||
expect(component.renderedHtml).toBe(oldHtml);
|
||||
expect(component.heading).toBe(oldHeading);
|
||||
|
||||
resolveFetch?.({
|
||||
resolveFetch({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('# Liedliste\n\nInhalt'),
|
||||
});
|
||||
|
||||
await fixture.whenStable();
|
||||
await loadPromise;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.loading).toBe(false);
|
||||
|
||||
@@ -46,6 +46,14 @@ export class HelpComponent implements OnInit {
|
||||
void this.loadDocument(resolvedPath);
|
||||
}
|
||||
|
||||
public onContentKeydown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onContentClick(event as unknown as MouseEvent);
|
||||
}
|
||||
|
||||
private async loadDocument(path: string): Promise<void> {
|
||||
if (!path.startsWith('man/')) {
|
||||
if (!this.hasLoadedContent) {
|
||||
@@ -300,14 +308,18 @@ export class HelpComponent implements OnInit {
|
||||
return `<a href="${safeHref}">${safeLabel}</a>`;
|
||||
});
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/(^|[^\*])\*([^*]+)\*/g, '$1<em>$2</em>');
|
||||
html = html.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
private decodePlainText(text: string): string {
|
||||
const html = this.renderInlineMarkdown(text);
|
||||
const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html)?.replace(/<[^>]+>/g, '').trim() ?? text;
|
||||
const html = this.renderInlineMarkdown(this.decodeHtmlEntities(text));
|
||||
const sanitized =
|
||||
this.sanitizer
|
||||
.sanitize(SecurityContext.HTML, html)
|
||||
?.replace(/<[^>]+>/g, '')
|
||||
.trim() ?? text;
|
||||
return this.decodeHtmlEntities(sanitized);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Router} from '@angular/router';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
||||
import {ShowService} from '../../shows/services/show.service';
|
||||
import {SelectComponent} from './select.component';
|
||||
@@ -8,29 +9,47 @@ import {SelectComponent} from './select.component';
|
||||
describe('SelectComponent', () => {
|
||||
let component: SelectComponent;
|
||||
let fixture: ComponentFixture<SelectComponent>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let globalSettingsServiceSpy: jasmine.SpyObj<GlobalSettingsService>;
|
||||
let routerSpy: jasmine.SpyObj<Router>;
|
||||
const createShow = (id: string, isoDate: string) => ({id, date: {toDate: () => new Date(isoDate)}});
|
||||
let showServiceSpy: {
|
||||
list$: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let globalSettingsServiceSpy: {
|
||||
set: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let routerSpy: {
|
||||
navigateByUrl: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
const createShow = (id: string, daysAgo: number) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - daysAgo);
|
||||
|
||||
return {id, date: {toDate: () => date}};
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['list$', 'update$']);
|
||||
globalSettingsServiceSpy = jasmine.createSpyObj<GlobalSettingsService>('GlobalSettingsService', ['set']);
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||
|
||||
showServiceSpy.list$.and.returnValue(
|
||||
of([createShow('older', '2025-12-15T00:00:00Z'), createShow('recent-a', '2026-03-01T00:00:00Z'), createShow('recent-b', '2026-02-20T00:00:00Z')]) as never
|
||||
);
|
||||
showServiceSpy.update$.and.resolveTo();
|
||||
globalSettingsServiceSpy.set.and.resolveTo();
|
||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||
showServiceSpy = {
|
||||
list$: vi.fn().mockReturnValue(
|
||||
of([
|
||||
createShow('older', 45),
|
||||
createShow('recent-a', 7),
|
||||
createShow('recent-b', 14),
|
||||
]) as never
|
||||
),
|
||||
update$: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
globalSettingsServiceSpy = {
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
routerSpy = {
|
||||
navigateByUrl: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SelectComponent],
|
||||
providers: [
|
||||
{provide: ShowService, useValue: showServiceSpy},
|
||||
{provide: GlobalSettingsService, useValue: globalSettingsServiceSpy},
|
||||
{provide: Router, useValue: routerSpy},
|
||||
{provide: ShowService, useValue: showServiceSpy as unknown as ShowService},
|
||||
{provide: GlobalSettingsService, useValue: globalSettingsServiceSpy as unknown as GlobalSettingsService},
|
||||
{provide: Router, useValue: routerSpy as unknown as Router},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -45,7 +64,7 @@ describe('SelectComponent', () => {
|
||||
it('should become visible on init', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.visible).toBeTrue();
|
||||
expect(component.visible).toBe(true);
|
||||
});
|
||||
|
||||
it('should expose recent shows sorted descending by date', async () => {
|
||||
@@ -61,7 +80,7 @@ describe('SelectComponent', () => {
|
||||
|
||||
await component.selectShow(show);
|
||||
|
||||
expect(component.visible).toBeFalse();
|
||||
expect(component.visible).toBe(false);
|
||||
expect(globalSettingsServiceSpy.set).toHaveBeenCalledWith({currentShow: 'show-1'});
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {presentationSongId: 'title'});
|
||||
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/presentation/remote');
|
||||
|
||||
@@ -31,10 +31,10 @@ describe('ReportDialogComponent', () => {
|
||||
});
|
||||
|
||||
it('should mark numbers as opened locally', () => {
|
||||
expect(component.wasOpened('12345')).toBeFalse();
|
||||
expect(component.wasOpened('12345')).toBe(false);
|
||||
|
||||
component.markOpened('12345');
|
||||
|
||||
expect(component.wasOpened('12345')).toBeTrue();
|
||||
expect(component.wasOpened('12345')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ShareDialogComponent} from './share-dialog.component';
|
||||
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||
import {vi} from 'vitest';
|
||||
|
||||
describe('ShareDialogComponent', () => {
|
||||
let component: ShareDialogComponent;
|
||||
let fixture: ComponentFixture<ShareDialogComponent>;
|
||||
type ShareDialogComponentInternals = ShareDialogComponent & {
|
||||
type ShareDialogComponentInternals = {
|
||||
generateQrCode: () => Promise<string>;
|
||||
};
|
||||
|
||||
@@ -28,10 +29,14 @@ describe('ShareDialogComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(ShareDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as ShareDialogComponentInternals, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test');
|
||||
vi.spyOn(component as unknown as ShareDialogComponentInternals, 'generateQrCode').mockResolvedValue('data:image/jpeg;base64,test');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
void expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Timestamp} from '@angular/fire/firestore';
|
||||
import {of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {ShowDataService} from '../services/show-data.service';
|
||||
import {ShowService} from '../services/show.service';
|
||||
import {EditComponent} from './edit.component';
|
||||
@@ -9,25 +10,35 @@ import {EditComponent} from './edit.component';
|
||||
describe('EditComponent', () => {
|
||||
let component: EditComponent;
|
||||
let fixture: ComponentFixture<EditComponent>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let showServiceSpy: {
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let showDataServiceStub: Pick<ShowDataService, 'list$'>;
|
||||
let routerSpy: jasmine.SpyObj<Router>;
|
||||
let routerSpy: {
|
||||
navigateByUrl: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
const createDate = (isoDate: string) => ({toDate: () => new Date(isoDate)});
|
||||
|
||||
beforeEach(async () => {
|
||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['read$', 'update$']);
|
||||
showServiceSpy = {
|
||||
read$: vi.fn(),
|
||||
update$: vi.fn(),
|
||||
};
|
||||
showDataServiceStub = {list$: of([]) as ShowDataService['list$']};
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||
routerSpy = {
|
||||
navigateByUrl: vi.fn(),
|
||||
};
|
||||
|
||||
showServiceSpy.read$.and.returnValue(
|
||||
showServiceSpy.read$.mockReturnValue(
|
||||
of({
|
||||
id: 'show-1',
|
||||
showType: 'service-worship',
|
||||
date: createDate('2026-03-10T00:00:00Z'),
|
||||
} as never)
|
||||
);
|
||||
showServiceSpy.update$.and.resolveTo();
|
||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||
routerSpy.navigateByUrl.mockResolvedValue(true);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EditComponent],
|
||||
@@ -43,6 +54,10 @@ describe('EditComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
@@ -68,7 +83,7 @@ describe('EditComponent', () => {
|
||||
it('should update the show and navigate back to the detail page', async () => {
|
||||
const date = new Date('2026-03-11T00:00:00Z');
|
||||
const firestoreTimestamp = {seconds: 1} as never;
|
||||
spyOn(Timestamp, 'fromDate').and.returnValue(firestoreTimestamp);
|
||||
vi.spyOn(Timestamp, 'fromDate').mockReturnValue(firestoreTimestamp);
|
||||
component.form.setValue({id: 'show-1', date, showType: 'home-group'});
|
||||
|
||||
await component.onSave();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {vi} from 'vitest';
|
||||
import {DocxService} from './docx.service';
|
||||
|
||||
describe('DocxService', () => {
|
||||
@@ -15,7 +16,7 @@ describe('DocxService', () => {
|
||||
type DocxModuleLike = {
|
||||
Packer: {toBlob: (document: unknown) => Promise<Blob>};
|
||||
};
|
||||
type DocxServiceInternals = DocxService & {
|
||||
type DocxServiceInternals = {
|
||||
prepareData: (showId: string) => Promise<PreparedData | null>;
|
||||
renderTitle: (docx: unknown, title: string) => unknown[];
|
||||
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
|
||||
@@ -29,14 +30,18 @@ describe('DocxService', () => {
|
||||
service = TestBed.inject(DocxService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
void expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not try to save a document when the required data cannot be prepared', async () => {
|
||||
const serviceInternals = service as DocxServiceInternals;
|
||||
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo(null);
|
||||
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
|
||||
const serviceInternals = service as unknown as DocxServiceInternals;
|
||||
const prepareDataSpy = vi.spyOn(serviceInternals, 'prepareData').mockResolvedValue(null);
|
||||
const saveAsSpy = vi.spyOn(serviceInternals, 'saveAs');
|
||||
|
||||
await service.create('show-1');
|
||||
|
||||
@@ -48,11 +53,11 @@ describe('DocxService', () => {
|
||||
const blob = new Blob(['docx']);
|
||||
const docxModule: DocxModuleLike = {
|
||||
Packer: {
|
||||
toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob),
|
||||
toBlob: vi.fn().mockResolvedValue(blob),
|
||||
},
|
||||
};
|
||||
const serviceInternals = service as DocxServiceInternals;
|
||||
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo({
|
||||
const serviceInternals = service as unknown as DocxServiceInternals;
|
||||
const prepareDataSpy = vi.spyOn(serviceInternals, 'prepareData').mockResolvedValue({
|
||||
show: {
|
||||
showType: 'service-worship',
|
||||
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
|
||||
@@ -61,17 +66,17 @@ describe('DocxService', () => {
|
||||
user: {name: 'Benjamin'},
|
||||
config: {ccliLicenseId: '12345'},
|
||||
});
|
||||
spyOn(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
|
||||
spyOn(serviceInternals, 'renderTitle').and.returnValue([]);
|
||||
spyOn(serviceInternals, 'renderSongs').and.returnValue([]);
|
||||
const prepareNewDocumentSpy = spyOn(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
|
||||
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
|
||||
vi.spyOn(serviceInternals, 'loadDocx').mockResolvedValue(docxModule);
|
||||
vi.spyOn(serviceInternals, 'renderTitle').mockReturnValue([]);
|
||||
vi.spyOn(serviceInternals, 'renderSongs').mockReturnValue([]);
|
||||
const prepareNewDocumentSpy = vi.spyOn(serviceInternals, 'prepareNewDocument').mockReturnValue({doc: true});
|
||||
const saveAsSpy = vi.spyOn(serviceInternals, 'saveAs');
|
||||
|
||||
await service.create('show-1', {copyright: true});
|
||||
|
||||
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
||||
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
||||
expect(docxModule.Packer.toBlob).toHaveBeenCalledWith({doc: true} as never);
|
||||
expect(saveAsSpy).toHaveBeenCalledWith(blob, jasmine.stringMatching(/\.docx$/));
|
||||
expect(saveAsSpy).toHaveBeenCalledWith(blob, expect.stringMatching(/\.docx$/));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {firstValueFrom, of, Subject} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {vi} from 'vitest';
|
||||
import {DbService} from '../../../services/db.service';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
|
||||
describe('ShowDataService', () => {
|
||||
let service: ShowDataService;
|
||||
let shows$: Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>;
|
||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||
let docSpy: jasmine.Spy;
|
||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||
let colSpy: jasmine.Spy;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||
let docSpy: ReturnType<typeof vi.fn>;
|
||||
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||
let colSpy: ReturnType<typeof vi.fn>;
|
||||
let dbServiceSpy: {
|
||||
col$: ReturnType<typeof vi.fn>;
|
||||
doc$: ReturnType<typeof vi.fn>;
|
||||
doc: ReturnType<typeof vi.fn>;
|
||||
col: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
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});
|
||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-3'});
|
||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||
dbServiceSpy.col$.and.returnValue(shows$.asObservable());
|
||||
dbServiceSpy.doc$.and.returnValue(of(null));
|
||||
dbServiceSpy.doc.and.callFake(docSpy);
|
||||
dbServiceSpy.col.and.callFake(colSpy);
|
||||
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docSpy = vi.fn().mockReturnValue({update: docUpdateSpy});
|
||||
colAddSpy = vi.fn().mockResolvedValue({id: 'show-3'});
|
||||
colSpy = vi.fn().mockReturnValue({add: colAddSpy});
|
||||
dbServiceSpy = {
|
||||
col$: vi.fn().mockReturnValue(shows$.asObservable()),
|
||||
doc$: vi.fn().mockReturnValue(of(null)),
|
||||
doc: docSpy,
|
||||
col: colSpy,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -77,12 +84,12 @@ 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'}]);
|
||||
dbServiceSpy.col$.and.returnValue(publicShows$ as never);
|
||||
dbServiceSpy.col$.mockReturnValue(publicShows$ as never);
|
||||
|
||||
const result = await firstValueFrom(service.listPublicSince$(3));
|
||||
|
||||
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows', jasmine.any(Array));
|
||||
const [, queryConstraints] = dbServiceSpy.col$.calls.mostRecent().args as [string, unknown[]];
|
||||
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows', expect.any(Array));
|
||||
const [, queryConstraints] = dbServiceSpy.col$.mock.lastCall as [string, unknown[]];
|
||||
expect(queryConstraints.length).toBe(3);
|
||||
expect(result.map(show => show.id)).toEqual(['show-1', 'show-3']);
|
||||
});
|
||||
@@ -97,15 +104,15 @@ describe('ShowDataService', () => {
|
||||
await service.update('show-8', {archived: true});
|
||||
|
||||
expect(docSpy).toHaveBeenCalledWith('shows/show-8');
|
||||
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [updatePayload] = docUpdateSpy.mock.lastCall as [Record<string, unknown>];
|
||||
expect(updatePayload).toEqual({archived: true});
|
||||
});
|
||||
|
||||
it('should add a show to the shows collection and return the new id', async () => {
|
||||
await expectAsync(service.add({published: true})).toBeResolvedTo('show-3');
|
||||
await expect(service.add({published: true})).resolves.toBe('show-3');
|
||||
|
||||
expect(colSpy).toHaveBeenCalledWith('shows');
|
||||
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [addPayload] = colAddSpy.mock.lastCall as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual({published: true});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {DbService} from '../../../services/db.service';
|
||||
import {ShowSongDataService} from './show-song-data.service';
|
||||
|
||||
describe('ShowSongDataService', () => {
|
||||
let service: ShowSongDataService;
|
||||
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>;
|
||||
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||
let docDeleteSpy: ReturnType<typeof vi.fn>;
|
||||
let docSpy: ReturnType<typeof vi.fn>;
|
||||
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||
let colSpy: ReturnType<typeof vi.fn>;
|
||||
let dbServiceSpy: {
|
||||
col$: ReturnType<typeof vi.fn>;
|
||||
doc$: ReturnType<typeof vi.fn>;
|
||||
doc: ReturnType<typeof vi.fn>;
|
||||
col: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||
docSpy = jasmine.createSpy('doc').and.returnValue({
|
||||
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docSpy = vi.fn().mockReturnValue({
|
||||
update: docUpdateSpy,
|
||||
delete: docDeleteSpy,
|
||||
});
|
||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-song-3'});
|
||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||
dbServiceSpy.col$.and.callFake(() => of([{id: 'show-song-1'}]) as never);
|
||||
dbServiceSpy.doc$.and.returnValue(of({id: 'show-song-1'}) as never);
|
||||
dbServiceSpy.doc.and.callFake(docSpy);
|
||||
dbServiceSpy.col.and.callFake(colSpy);
|
||||
colAddSpy = vi.fn().mockResolvedValue({id: 'show-song-3'});
|
||||
colSpy = vi.fn().mockReturnValue({add: colAddSpy});
|
||||
dbServiceSpy = {
|
||||
col$: vi.fn().mockImplementation(() => of([{id: 'show-song-1'}]) as never),
|
||||
doc$: vi.fn().mockReturnValue(of({id: 'show-song-1'}) as never),
|
||||
doc: docSpy,
|
||||
col: colSpy,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -77,7 +84,7 @@ describe('ShowSongDataService', () => {
|
||||
await service.update$('show-4', 'song-5', {title: 'Updated'} as never);
|
||||
|
||||
expect(docSpy).toHaveBeenCalledWith('shows/show-4/songs/song-5');
|
||||
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [updatePayload] = docUpdateSpy.mock.lastCall as [Record<string, unknown>];
|
||||
expect(updatePayload).toEqual({title: 'Updated'});
|
||||
});
|
||||
|
||||
@@ -89,10 +96,10 @@ describe('ShowSongDataService', () => {
|
||||
});
|
||||
|
||||
it('should add a song to the nested show songs collection and return the id', async () => {
|
||||
await expectAsync(service.add('show-4', {songId: 'song-5'} as never)).toBeResolvedTo('show-song-3');
|
||||
await expect(service.add('show-4', {songId: 'song-5'} as never)).resolves.toBe('show-song-3');
|
||||
|
||||
expect(colSpy).toHaveBeenCalledWith('shows/show-4/songs');
|
||||
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [addPayload] = colAddSpy.mock.lastCall as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual({songId: 'song-5'});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
import {ShowSongDataService} from './show-song-data.service';
|
||||
import {ShowSongIndexService} from './show-song-index.service';
|
||||
@@ -8,20 +9,34 @@ import {User} from '../../../services/user/user';
|
||||
|
||||
describe('ShowSongIndexService', () => {
|
||||
let service: ShowSongIndexService;
|
||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||
let showDataServiceSpy: {
|
||||
listRaw$: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let showSongDataServiceSpy: {
|
||||
list$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let sessionSpy: {
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
user$: Observable<User>;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$', 'update']);
|
||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
||||
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
||||
showDataServiceSpy = {
|
||||
listRaw$: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
showSongDataServiceSpy = {
|
||||
list$: vi.fn(),
|
||||
};
|
||||
sessionSpy = {
|
||||
update$: vi.fn(),
|
||||
user$: of({id: 'admin-1', name: 'Admin', role: 'admin', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
});
|
||||
};
|
||||
|
||||
showDataServiceSpy.listRaw$.and.returnValue(of([{id: 'show-1'}, {id: 'show-2'}] as never) as unknown as ReturnType<ShowDataService['listRaw$']>);
|
||||
showDataServiceSpy.update.and.resolveTo();
|
||||
showSongDataServiceSpy.list$.and.callFake((showId: string) => {
|
||||
showDataServiceSpy.listRaw$.mockReturnValue(of([{id: 'show-1'}, {id: 'show-2'}] as never) as unknown as ReturnType<ShowDataService['listRaw$']>);
|
||||
showDataServiceSpy.update.mockResolvedValue(undefined);
|
||||
showSongDataServiceSpy.list$.mockImplementation((showId: string) => {
|
||||
if (showId === 'show-1') {
|
||||
return of([{songId: 'song-1'}, {songId: 'song-2'}, {songId: 'song-1'}] as never) as never;
|
||||
}
|
||||
@@ -41,7 +56,7 @@ describe('ShowSongIndexService', () => {
|
||||
});
|
||||
|
||||
it('should rebuild the distinct songIds index for all shows', async () => {
|
||||
await expectAsync(service.rebuildShowSongIds()).toBeResolvedTo({
|
||||
await expect(service.rebuildShowSongIds()).resolves.toEqual({
|
||||
showsProcessed: 2,
|
||||
showSongsProcessed: 4,
|
||||
});
|
||||
@@ -55,6 +70,6 @@ describe('ShowSongIndexService', () => {
|
||||
value: of({id: 'user-1', name: 'User', role: 'leader', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
});
|
||||
|
||||
await expectAsync(service.rebuildShowSongIds()).toBeRejectedWithError('Admin role required to rebuild show song ids.');
|
||||
await expect(service.rebuildShowSongIds()).rejects.toThrow('Admin role required to rebuild show song ids.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {SongDataService} from '../../songs/services/song-data.service';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {ShowService} from './show.service';
|
||||
@@ -12,10 +13,25 @@ import {User} from '../../../services/user/user';
|
||||
|
||||
describe('ShowSongService', () => {
|
||||
let service: ShowSongService;
|
||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let showSongDataServiceSpy: {
|
||||
add: ReturnType<typeof vi.fn>;
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
list$: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let songDataServiceSpy: {
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let userServiceSpy: {
|
||||
incSongCount: ReturnType<typeof vi.fn>;
|
||||
decSongCount: ReturnType<typeof vi.fn>;
|
||||
user$: ReturnType<BehaviorSubject<User | null>['asObservable']>;
|
||||
};
|
||||
let showServiceSpy: {
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
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;
|
||||
@@ -23,23 +39,36 @@ describe('ShowSongService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
user$ = new BehaviorSubject<User | null>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'onlyFirst', songUsage: {}});
|
||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['add', 'read$', 'list$', 'delete', 'update$']);
|
||||
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$']);
|
||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
||||
showSongDataServiceSpy = {
|
||||
add: vi.fn(),
|
||||
read$: vi.fn(),
|
||||
list$: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
update$: vi.fn(),
|
||||
};
|
||||
songDataServiceSpy = {
|
||||
read$: vi.fn(),
|
||||
};
|
||||
userServiceSpy = {
|
||||
incSongCount: vi.fn(),
|
||||
decSongCount: vi.fn(),
|
||||
user$: user$.asObservable(),
|
||||
});
|
||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['read$', 'update$']);
|
||||
};
|
||||
showServiceSpy = {
|
||||
read$: vi.fn(),
|
||||
update$: vi.fn(),
|
||||
};
|
||||
|
||||
showSongDataServiceSpy.add.and.resolveTo('show-song-2');
|
||||
showSongDataServiceSpy.read$.and.returnValue(of(showSong));
|
||||
showSongDataServiceSpy.list$.and.returnValue(of([showSong]));
|
||||
showSongDataServiceSpy.delete.and.resolveTo();
|
||||
showSongDataServiceSpy.update$.and.resolveTo();
|
||||
songDataServiceSpy.read$.and.returnValue(of(song));
|
||||
userServiceSpy.incSongCount.and.resolveTo();
|
||||
userServiceSpy.decSongCount.and.resolveTo();
|
||||
showServiceSpy.read$.and.returnValue(of(show));
|
||||
showServiceSpy.update$.and.resolveTo();
|
||||
showSongDataServiceSpy.add.mockResolvedValue('show-song-2');
|
||||
showSongDataServiceSpy.read$.mockReturnValue(of(showSong));
|
||||
showSongDataServiceSpy.list$.mockReturnValue(of([showSong]));
|
||||
showSongDataServiceSpy.delete.mockResolvedValue(undefined);
|
||||
showSongDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||
songDataServiceSpy.read$.mockReturnValue(of(song));
|
||||
userServiceSpy.incSongCount.mockResolvedValue(undefined);
|
||||
userServiceSpy.decSongCount.mockResolvedValue(undefined);
|
||||
showServiceSpy.read$.mockReturnValue(of(show));
|
||||
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -58,7 +87,7 @@ describe('ShowSongService', () => {
|
||||
});
|
||||
|
||||
it('should create a show song from the song and current user', async () => {
|
||||
await expectAsync(service.new$('show-1', 'song-1', true)).toBeResolvedTo('show-song-2');
|
||||
await expect(service.new$('show-1', 'song-1', true)).resolves.toBe('show-song-2');
|
||||
|
||||
expect(userServiceSpy.incSongCount).toHaveBeenCalledWith('song-1');
|
||||
expect(showSongDataServiceSpy.add).toHaveBeenCalledWith('show-1', {
|
||||
@@ -72,9 +101,9 @@ describe('ShowSongService', () => {
|
||||
});
|
||||
|
||||
it('should return null when the song is missing', async () => {
|
||||
songDataServiceSpy.read$.and.returnValue(of(null));
|
||||
songDataServiceSpy.read$.mockReturnValue(of(null));
|
||||
|
||||
await expectAsync(service.new$('show-1', 'song-1')).toBeResolvedTo(null);
|
||||
await expect(service.new$('show-1', 'song-1')).resolves.toBe(null);
|
||||
|
||||
expect(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||
@@ -83,19 +112,19 @@ describe('ShowSongService', () => {
|
||||
it('should return null when the current user is missing', async () => {
|
||||
user$.next(null);
|
||||
|
||||
await expectAsync(service.new$('show-1', 'song-1')).toBeResolvedTo(null);
|
||||
await expect(service.new$('show-1', 'song-1')).resolves.toBe(null);
|
||||
|
||||
expect(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delegate reads to the data service', async () => {
|
||||
await expectAsync(service.read('show-1', 'show-song-1')).toBeResolvedTo(showSong);
|
||||
await expect(service.read('show-1', 'show-song-1')).resolves.toBe(showSong);
|
||||
expect(showSongDataServiceSpy.read$).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||
});
|
||||
|
||||
it('should delegate list access to the data service', async () => {
|
||||
await expectAsync(service.list('show-1')).toBeResolvedTo([showSong]);
|
||||
await expect(service.list('show-1')).resolves.toEqual([showSong]);
|
||||
expect(showSongDataServiceSpy.list$).toHaveBeenCalledWith('show-1');
|
||||
});
|
||||
|
||||
@@ -103,12 +132,12 @@ describe('ShowSongService', () => {
|
||||
await service.delete$('show-1', 'show-song-1', 0);
|
||||
|
||||
expect(showSongDataServiceSpy.delete).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', jasmine.objectContaining({order: ['show-song-2']}));
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', expect.objectContaining({order: ['show-song-2']}));
|
||||
expect(userServiceSpy.decSongCount).toHaveBeenCalledWith('song-1');
|
||||
});
|
||||
|
||||
it('should stop delete when the show is missing', async () => {
|
||||
showServiceSpy.read$.and.returnValue(of(null));
|
||||
showServiceSpy.read$.mockReturnValue(of(null));
|
||||
|
||||
await service.delete$('show-1', 'show-song-1', 0);
|
||||
|
||||
@@ -118,7 +147,7 @@ describe('ShowSongService', () => {
|
||||
});
|
||||
|
||||
it('should stop delete when the show song is missing', async () => {
|
||||
showSongDataServiceSpy.read$.and.returnValue(of(null));
|
||||
showSongDataServiceSpy.read$.mockReturnValue(of(null));
|
||||
|
||||
await service.delete$('show-1', 'show-song-1', 0);
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
import {ShowService} from './show.service';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
|
||||
describe('ShowService', () => {
|
||||
let service: ShowService;
|
||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||
let showDataServiceSpy: {
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
listPublicSince$: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
add: ReturnType<typeof vi.fn>;
|
||||
list$: ReturnType<BehaviorSubject<unknown[]>['asObservable']>;
|
||||
};
|
||||
let user$: BehaviorSubject<unknown>;
|
||||
let shows$: BehaviorSubject<unknown[]>;
|
||||
const shows = [
|
||||
@@ -18,13 +25,17 @@ describe('ShowService', () => {
|
||||
beforeEach(async () => {
|
||||
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||
shows$ = new BehaviorSubject<unknown[]>(shows as unknown[]);
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
|
||||
showDataServiceSpy = {
|
||||
read$: vi.fn(),
|
||||
listPublicSince$: vi.fn(),
|
||||
update: vi.fn(),
|
||||
add: vi.fn(),
|
||||
list$: shows$.asObservable() as unknown as ShowDataService['list$'],
|
||||
});
|
||||
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
||||
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
||||
showDataServiceSpy.update.and.resolveTo();
|
||||
showDataServiceSpy.add.and.resolveTo('new-show-id');
|
||||
};
|
||||
showDataServiceSpy.read$.mockReturnValue(of(shows[0]));
|
||||
showDataServiceSpy.listPublicSince$.mockReturnValue(of([shows[1]]));
|
||||
showDataServiceSpy.update.mockResolvedValue(undefined);
|
||||
showDataServiceSpy.add.mockResolvedValue('new-show-id');
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -63,12 +74,12 @@ describe('ShowService', () => {
|
||||
});
|
||||
|
||||
it('should delegate public listing to the data service', async () => {
|
||||
await expectAsync(firstValueFrom(service.listPublicSince$(6))).toBeResolvedTo([shows[1]]);
|
||||
await expect(firstValueFrom(service.listPublicSince$(6))).resolves.toEqual([shows[1]]);
|
||||
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
|
||||
});
|
||||
|
||||
it('should delegate reads to the data service', async () => {
|
||||
await expectAsync(firstValueFrom(service.read$('show-1'))).toBeResolvedTo(shows[0]);
|
||||
await expect(firstValueFrom(service.read$('show-1'))).resolves.toBe(shows[0]);
|
||||
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
||||
});
|
||||
|
||||
@@ -79,7 +90,7 @@ describe('ShowService', () => {
|
||||
});
|
||||
|
||||
it('should return null when creating a show without showType', async () => {
|
||||
await expectAsync(service.new$({published: true})).toBeResolvedTo(null);
|
||||
await expect(service.new$({published: true})).resolves.toBe(null);
|
||||
|
||||
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -87,7 +98,7 @@ describe('ShowService', () => {
|
||||
it('should return null when no user is available for show creation', async () => {
|
||||
user$.next(null);
|
||||
|
||||
await expectAsync(service.new$({showType: 'misc-public'})).toBeResolvedTo(null);
|
||||
await expect(service.new$({showType: 'misc-public'})).resolves.toBe(null);
|
||||
|
||||
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {ShowComponent} from './show.component';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {ShowService} from '../services/show.service';
|
||||
@@ -13,23 +14,39 @@ import {GuestShowService} from '../../guest/guest-show.service';
|
||||
describe('ShowComponent', () => {
|
||||
let component: ShowComponent;
|
||||
let fixture: ComponentFixture<ShowComponent>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let showSongServiceSpy: jasmine.SpyObj<ShowSongService>;
|
||||
let dialogSpy: jasmine.SpyObj<MatDialog>;
|
||||
let showServiceSpy: {
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let showSongServiceSpy: {
|
||||
list$: ReturnType<typeof vi.fn>;
|
||||
list: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let dialogSpy: {
|
||||
open: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let user$: BehaviorSubject<unknown>;
|
||||
let userId$: BehaviorSubject<string | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['update$', 'read$']);
|
||||
showSongServiceSpy = jasmine.createSpyObj<ShowSongService>('ShowSongService', ['list$', 'list']);
|
||||
dialogSpy = jasmine.createSpyObj<MatDialog>('MatDialog', ['open']);
|
||||
showServiceSpy = {
|
||||
update$: vi.fn(),
|
||||
read$: vi.fn(),
|
||||
};
|
||||
showSongServiceSpy = {
|
||||
list$: vi.fn(),
|
||||
list: vi.fn(),
|
||||
};
|
||||
dialogSpy = {
|
||||
open: vi.fn(),
|
||||
};
|
||||
user$ = new BehaviorSubject<unknown>({id: 'user-1', role: ['leader']});
|
||||
userId$ = new BehaviorSubject<string | null>('user-1');
|
||||
|
||||
showServiceSpy.read$.and.returnValue(of(null));
|
||||
showServiceSpy.update$.and.resolveTo();
|
||||
showSongServiceSpy.list$.and.returnValue(of([]));
|
||||
showSongServiceSpy.list.and.resolveTo([]);
|
||||
showServiceSpy.read$.mockReturnValue(of(null));
|
||||
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||
showSongServiceSpy.list$.mockReturnValue(of([]));
|
||||
showSongServiceSpy.list.mockResolvedValue([]);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ShowComponent],
|
||||
@@ -38,8 +55,8 @@ describe('ShowComponent', () => {
|
||||
{provide: ShowService, useValue: showServiceSpy},
|
||||
{provide: SongService, useValue: {list$: () => of([])}},
|
||||
{provide: ShowSongService, useValue: showSongServiceSpy},
|
||||
{provide: DocxService, useValue: {create: jasmine.createSpy('create').and.resolveTo()}},
|
||||
{provide: Router, useValue: {navigateByUrl: jasmine.createSpy('navigateByUrl')}},
|
||||
{provide: DocxService, useValue: {create: vi.fn().mockResolvedValue(undefined)}},
|
||||
{provide: Router, useValue: {navigateByUrl: vi.fn()}},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
@@ -49,7 +66,7 @@ describe('ShowComponent', () => {
|
||||
},
|
||||
},
|
||||
{provide: MatDialog, useValue: dialogSpy},
|
||||
{provide: GuestShowService, useValue: {share: jasmine.createSpy('share').and.resolveTo('https://example.invalid')}},
|
||||
{provide: GuestShowService, useValue: {share: vi.fn().mockResolvedValue('https://example.invalid')}},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -74,7 +91,7 @@ describe('ShowComponent', () => {
|
||||
});
|
||||
|
||||
it('should set pending for public shows with reportable CCLI songs', async () => {
|
||||
showSongServiceSpy.list.and.resolveTo([{legalOwner: 'CCLI', legalOwnerId: '123'}] as never);
|
||||
showSongServiceSpy.list.mockResolvedValue([{legalOwner: 'CCLI', legalOwnerId: '123'}] as never);
|
||||
|
||||
await component.onPublish({id: 'show-1', public: true} as never, true);
|
||||
|
||||
@@ -82,7 +99,7 @@ describe('ShowComponent', () => {
|
||||
});
|
||||
|
||||
it('should set not-required for public shows without reportable CCLI songs', async () => {
|
||||
showSongServiceSpy.list.and.resolveTo([{legalOwner: 'CCLI', legalOwnerId: ''}] as never);
|
||||
showSongServiceSpy.list.mockResolvedValue([{legalOwner: 'CCLI', legalOwnerId: ''}] as never);
|
||||
|
||||
await component.onPublish({id: 'show-1', public: true} as never, true);
|
||||
|
||||
@@ -96,11 +113,11 @@ describe('ShowComponent', () => {
|
||||
{id: 'show-song-3', songId: 'song-2', title: 'Beta', legalOwner: 'other', legalOwnerId: '456'},
|
||||
{id: 'show-song-4', songId: 'song-3', title: 'Gamma', legalOwner: 'CCLI', legalOwnerId: '789'},
|
||||
] as never;
|
||||
dialogSpy.open.and.returnValue({afterClosed: () => of(true)} as never);
|
||||
dialogSpy.open.mockReturnValue({afterClosed: () => of(true)} as never);
|
||||
|
||||
component.onReport({id: 'show-1', order: ['show-song-1', 'show-song-2', 'show-song-3', 'show-song-4']} as never);
|
||||
|
||||
expect(dialogSpy.open).toHaveBeenCalledWith(jasmine.any(Function), {
|
||||
expect(dialogSpy.open).toHaveBeenCalledWith(expect.any(Function), {
|
||||
width: '640px',
|
||||
data: {
|
||||
songs: [
|
||||
|
||||
@@ -5,30 +5,29 @@ import {FileDataService} from './file-data.service';
|
||||
|
||||
describe('FileDataService', () => {
|
||||
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>;
|
||||
let filesCollectionValueChangesSpy: ReturnType<typeof vi.fn<() => unknown>>;
|
||||
let filesCollectionAddSpy: ReturnType<typeof vi.fn<() => Promise<{id: string}>>>;
|
||||
let songDocCollectionSpy: ReturnType<typeof vi.fn<() => {add: typeof filesCollectionAddSpy; valueChanges: typeof filesCollectionValueChangesSpy}>>;
|
||||
let songDocSpy: ReturnType<typeof vi.fn<(path: string) => unknown>>;
|
||||
let fileDeleteSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||
let dbServiceSpy: {doc: typeof songDocSpy};
|
||||
|
||||
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({
|
||||
filesCollectionValueChangesSpy = vi.fn().mockReturnValue(of([{id: 'file-1', name: 'plan.pdf'}]));
|
||||
filesCollectionAddSpy = vi.fn().mockResolvedValue({id: 'file-2'});
|
||||
songDocCollectionSpy = vi.fn().mockReturnValue({
|
||||
add: filesCollectionAddSpy,
|
||||
valueChanges: filesCollectionValueChangesSpy,
|
||||
});
|
||||
songDocSpy = jasmine.createSpy('songDoc').and.callFake((path: string) => {
|
||||
songDocSpy = vi.fn().mockImplementation((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);
|
||||
fileDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||
dbServiceSpy = {doc: songDocSpy};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -44,7 +43,7 @@ describe('FileDataService', () => {
|
||||
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');
|
||||
await expect(service.set('song-1', file)).resolves.toEqual('file-2');
|
||||
|
||||
expect(songDocSpy).toHaveBeenCalledWith('songs/song-1');
|
||||
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||
|
||||
@@ -5,18 +5,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>;
|
||||
};
|
||||
let fileDataServiceSpy: {delete: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['delete']);
|
||||
fileDataServiceSpy.delete.and.resolveTo();
|
||||
fileDataServiceSpy = {
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
FileService,
|
||||
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||
],
|
||||
@@ -25,20 +23,26 @@ describe('FileService', () => {
|
||||
service = TestBed.inject(FileService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should resolve download urls via AngularFire storage helpers', async () => {
|
||||
const resolveSpy = spyOn(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf');
|
||||
const resolveSpy = vi.fn<(path: string) => Promise<string>>().mockResolvedValue('https://cdn.example/file.pdf');
|
||||
Object.assign(service, {resolveDownloadUrl: resolveSpy});
|
||||
|
||||
await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf');
|
||||
await expect(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).resolves.toEqual('https://cdn.example/file.pdf');
|
||||
|
||||
expect(resolveSpy).toHaveBeenCalledWith('songs/song-1/file.pdf');
|
||||
});
|
||||
|
||||
it('should delete the file from storage and metadata from firestore', async () => {
|
||||
const deleteFromStorageSpy = spyOn(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo();
|
||||
const deleteFromStorageSpy = vi.fn<(path: string) => Promise<void>>().mockResolvedValue(undefined);
|
||||
Object.assign(service, {deleteFromStorage: deleteFromStorageSpy});
|
||||
|
||||
await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('key.helper', () => {
|
||||
});
|
||||
|
||||
it('should detect whether a root has any available stored key', () => {
|
||||
void expect(hasAvailableKeyForRoot('C', ['a'])).toBe(true);
|
||||
void expect(hasAvailableKeyForRoot('C', ['C'])).toBe(true);
|
||||
void expect(hasAvailableKeyForRoot('E', ['F'])).toBe(true);
|
||||
void expect(hasAvailableKeyForRoot('H', ['C'])).toBe(true);
|
||||
void expect(hasAvailableKeyForRoot('D', ['F', 'a'])).toBe(false);
|
||||
|
||||
@@ -7,30 +7,36 @@ import {SongDataService} from './song-data.service';
|
||||
describe('SongDataService', () => {
|
||||
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>;
|
||||
let docUpdateSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||
let docDeleteSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||
let docSpy: ReturnType<typeof vi.fn<(path: string) => {update: typeof docUpdateSpy; delete: typeof docDeleteSpy}>>;
|
||||
let colAddSpy: ReturnType<typeof vi.fn<() => Promise<{id: string}>>>;
|
||||
let colSpy: ReturnType<typeof vi.fn<(path: string) => {add: typeof colAddSpy}>>;
|
||||
let dbServiceSpy: {
|
||||
col$: ReturnType<typeof vi.fn>;
|
||||
doc$: ReturnType<typeof vi.fn>;
|
||||
doc: typeof docSpy;
|
||||
col: typeof colSpy;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
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(() => ({
|
||||
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||
docSpy = vi.fn().mockImplementation(() => ({
|
||||
update: docUpdateSpy,
|
||||
delete: docDeleteSpy,
|
||||
}));
|
||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'song-3'});
|
||||
colSpy = jasmine.createSpy('col').and.returnValue({
|
||||
colAddSpy = vi.fn().mockResolvedValue({id: 'song-3'});
|
||||
colSpy = vi.fn().mockReturnValue({
|
||||
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);
|
||||
dbServiceSpy = {
|
||||
col$: vi.fn().mockReturnValue(songs$.asObservable()),
|
||||
doc$: vi.fn().mockReturnValue(songs$.asObservable() as never),
|
||||
doc: docSpy,
|
||||
col: colSpy,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -83,15 +89,15 @@ describe('SongDataService', () => {
|
||||
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>];
|
||||
const [updatePayload] = docUpdateSpy.mock.calls.at(-1) 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');
|
||||
await expect(service.add({title: 'New Song'})).resolves.toEqual('song-3');
|
||||
|
||||
expect(colSpy).toHaveBeenCalledWith('songs');
|
||||
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||
const [addPayload] = colAddSpy.mock.calls.at(-1) as unknown as [Record<string, unknown>];
|
||||
expect(addPayload).toEqual({title: 'New Song'});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {UserSessionService} from '../../../services/user/user-session.service';
|
||||
import {User} from '../../../services/user/user';
|
||||
import {SongDataService} from './song-data.service';
|
||||
import {SongDownloadService} from './song-download.service';
|
||||
|
||||
describe('SongDownloadService', () => {
|
||||
let service: SongDownloadService;
|
||||
let songDataServiceSpy: {listLoaded$: ReturnType<typeof vi.fn>};
|
||||
let sessionSpy: {update$: ReturnType<typeof vi.fn>; user$: unknown};
|
||||
let createObjectUrlSpy: ReturnType<typeof vi.spyOn>;
|
||||
let revokeObjectUrlSpy: ReturnType<typeof vi.spyOn>;
|
||||
let clickSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
songDataServiceSpy = {
|
||||
listLoaded$: vi.fn(),
|
||||
};
|
||||
sessionSpy = {
|
||||
update$: vi.fn(),
|
||||
user$: of({id: 'admin-1', name: 'Admin', role: 'admin', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
};
|
||||
songDataServiceSpy.listLoaded$.mockReturnValue(
|
||||
of([
|
||||
{id: 'song-2', number: 2, title: 'Zweiter Song'},
|
||||
{id: 'song-1', number: 1, title: 'Erster Song'},
|
||||
] as never)
|
||||
);
|
||||
createObjectUrlSpy = vi.spyOn(window.URL, 'createObjectURL').mockReturnValue('blob:songs');
|
||||
revokeObjectUrlSpy = vi.spyOn(window.URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
||||
clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: SongDataService, useValue: songDataServiceSpy},
|
||||
{provide: UserSessionService, useValue: sessionSpy},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(SongDownloadService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should download all songs as sorted JSON', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const result = await service.downloadSongs();
|
||||
|
||||
const [blob, fileName] = createObjectUrlSpy.mock.calls.at(-1) as unknown as [Blob, string];
|
||||
|
||||
expect(blob.type).toBe('application/json');
|
||||
expect(fileName).toBeUndefined();
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
expect(result?.songsDownloaded).toBe(2);
|
||||
expect(result?.fileName).toMatch(/^songs-\d{4}-\d{2}-\d{2}\.json$/);
|
||||
|
||||
const text = await blob.text();
|
||||
expect(JSON.parse(text).map((song: {id: string}) => song.id)).toEqual(['song-1', 'song-2']);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(revokeObjectUrlSpy).toHaveBeenCalledWith('blob:songs');
|
||||
});
|
||||
|
||||
it('should reject downloads for non-admin users', async () => {
|
||||
Object.defineProperty(sessionSpy, 'user$', {
|
||||
value: of({id: 'user-1', name: 'User', role: 'leader', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
});
|
||||
|
||||
await expect(service.downloadSongs()).rejects.toThrow('Admin role required to download songs.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {firstValueFrom} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {UserSessionService} from '../../../services/user/user-session.service';
|
||||
import {SongDataService} from './song-data.service';
|
||||
|
||||
export interface SongDownloadResult {
|
||||
fileName: string;
|
||||
songsDownloaded: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SongDownloadService {
|
||||
private session = inject(UserSessionService);
|
||||
private songDataService = inject(SongDataService);
|
||||
|
||||
public async downloadSongs(): Promise<SongDownloadResult> {
|
||||
const currentUser = await firstValueFrom(this.session.user$.pipe(take(1)));
|
||||
if (!currentUser || !this.hasAdminRole(currentUser.role)) {
|
||||
throw new Error('Admin role required to download songs.');
|
||||
}
|
||||
|
||||
const songs = await firstValueFrom(this.songDataService.listLoaded$());
|
||||
const sortedSongs = [...songs].sort((left, right) => {
|
||||
const numberCompare = (left.number ?? Number.MAX_SAFE_INTEGER) - (right.number ?? Number.MAX_SAFE_INTEGER);
|
||||
if (numberCompare !== 0) {
|
||||
return numberCompare;
|
||||
}
|
||||
|
||||
return (left.title ?? '').localeCompare(right.title ?? '', 'de');
|
||||
});
|
||||
const fileName = `songs-${this.formatDate(new Date())}.json`;
|
||||
|
||||
this.saveAs(new Blob([JSON.stringify(sortedSongs, null, 2)], {type: 'application/json'}), fileName);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
songsDownloaded: sortedSongs.length,
|
||||
};
|
||||
}
|
||||
|
||||
private saveAs(blob: Blob, fileName: string) {
|
||||
const a = document.createElement('a');
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.setAttribute('target', '_self');
|
||||
a.style.display = 'none';
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private formatDate(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
private hasAdminRole(role: string | null | undefined): boolean {
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return role.split(';').includes('admin');
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import {SongListResolver} from './song-list.resolver';
|
||||
|
||||
describe('SongListResolver', () => {
|
||||
let resolver: SongListResolver;
|
||||
let songServiceSpy: jasmine.SpyObj<SongService>;
|
||||
let songServiceSpy: {listLoaded$: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['listLoaded$']);
|
||||
songServiceSpy.listLoaded$.and.returnValue(of([{id: 'song-1', title: 'Amazing Grace'}]) as never);
|
||||
songServiceSpy = {
|
||||
listLoaded$: vi.fn().mockReturnValue(of([{id: 'song-1', title: 'Amazing Grace'}]) as never),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: SongService, useValue: songServiceSpy}],
|
||||
@@ -23,7 +24,7 @@ describe('SongListResolver', () => {
|
||||
});
|
||||
|
||||
it('should resolve the first emitted song list from the service', async () => {
|
||||
await expectAsync(firstValueFrom(resolver.resolve())).toBeResolvedTo([{id: 'song-1', title: 'Amazing Grace'}] as never);
|
||||
await expect(firstValueFrom(resolver.resolve())).resolves.toEqual([{id: 'song-1', title: 'Amazing Grace'}] as never);
|
||||
expect(songServiceSpy.listLoaded$).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,14 @@ import {Timestamp} from '@angular/fire/firestore';
|
||||
|
||||
describe('SongService', () => {
|
||||
let service: SongService;
|
||||
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||
let songDataServiceSpy: {
|
||||
read$: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
add: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
list$: unknown;
|
||||
};
|
||||
let userServiceSpy: {currentUser: ReturnType<typeof vi.fn>};
|
||||
const song = {
|
||||
id: 'song-1',
|
||||
title: 'Amazing Grace',
|
||||
@@ -16,16 +22,22 @@ describe('SongService', () => {
|
||||
} as never;
|
||||
|
||||
beforeEach(async () => {
|
||||
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$', 'update$', 'add', 'delete'], {
|
||||
songDataServiceSpy = {
|
||||
read$: vi.fn(),
|
||||
update$: vi.fn(),
|
||||
add: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
list$: of([song]),
|
||||
});
|
||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['currentUser']);
|
||||
};
|
||||
userServiceSpy = {
|
||||
currentUser: vi.fn(),
|
||||
};
|
||||
|
||||
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);
|
||||
songDataServiceSpy.read$.mockReturnValue(of(song));
|
||||
songDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||
songDataServiceSpy.add.mockResolvedValue('song-2');
|
||||
songDataServiceSpy.delete.mockResolvedValue(undefined);
|
||||
userServiceSpy.currentUser.mockResolvedValue({name: 'Benjamin'} as never);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -37,33 +49,37 @@ describe('SongService', () => {
|
||||
service = TestBed.inject(SongService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should list songs from the data service', async () => {
|
||||
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([song]);
|
||||
await expect(firstValueFrom(service.list$())).resolves.toEqual([song]);
|
||||
});
|
||||
|
||||
it('should delegate reads to the data service', async () => {
|
||||
await expectAsync(service.read('song-1')).toBeResolvedTo(song);
|
||||
await expect(service.read('song-1')).resolves.toEqual(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);
|
||||
vi.spyOn(Timestamp, 'now').mockReturnValue(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>];
|
||||
const [, payload] = songDataServiceSpy.update$.mock.calls.at(-1) 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));
|
||||
songDataServiceSpy.read$.mockReturnValue(of(null));
|
||||
|
||||
await service.update$('missing-song', {title: 'Updated'});
|
||||
|
||||
@@ -71,7 +87,7 @@ describe('SongService', () => {
|
||||
});
|
||||
|
||||
it('should not update when no current user is available', async () => {
|
||||
userServiceSpy.currentUser.and.resolveTo(null);
|
||||
userServiceSpy.currentUser.mockResolvedValue(null);
|
||||
|
||||
await service.update$('song-1', {title: 'Updated'});
|
||||
|
||||
@@ -79,7 +95,7 @@ describe('SongService', () => {
|
||||
});
|
||||
|
||||
it('should create new songs with the expected defaults', async () => {
|
||||
await expectAsync(service.new(42, 'New Song')).toBeResolvedTo('song-2');
|
||||
await expect(service.new(42, 'New Song')).resolves.toEqual('song-2');
|
||||
|
||||
expect(songDataServiceSpy.add).toHaveBeenCalledWith({
|
||||
number: 42,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {ChordAddDescriptor} from './chord';
|
||||
|
||||
describe('TextRenderingService', () => {
|
||||
const descriptor = (raw: string, partial: Partial<ChordAddDescriptor>) =>
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
raw,
|
||||
quality: null,
|
||||
extensions: [],
|
||||
@@ -198,12 +198,12 @@ Text`;
|
||||
void expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
|
||||
|
||||
void expect(sections[2].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'c', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'c#', length: 2, position: 2, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'db', length: 2, position: 5, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'c', length: 2, position: 8, add: '7', slashChord: null, addDescriptor: descriptor('7', {extensions: ['7']})}),
|
||||
jasmine.objectContaining({chord: 'c', length: 5, position: 13, add: 'maj7', slashChord: null, addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
jasmine.objectContaining({chord: 'c', length: 3, position: 22, add: null, slashChord: 'e', addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'c', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'c#', length: 2, position: 2, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'db', length: 2, position: 5, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'c', length: 2, position: 8, add: '7', slashChord: null, addDescriptor: descriptor('7', {extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'c', length: 5, position: 13, add: 'maj7', slashChord: null, addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'c', length: 3, position: 22, add: null, slashChord: 'e', addDescriptor: null}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -228,9 +228,9 @@ Text`;
|
||||
const sections = service.parse(text, null);
|
||||
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'G', length: 3, position: 8, add: null, slashChord: 'B', addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'A', length: 2, position: 17, add: 'm', slashChord: null, addDescriptor: descriptor('m', {quality: 'minor'})}),
|
||||
expect.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'G', length: 3, position: 8, add: null, slashChord: 'B', addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'A', length: 2, position: 17, add: 'm', slashChord: null, addDescriptor: descriptor('m', {quality: 'minor'})}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -244,11 +244,11 @@ Text`;
|
||||
|
||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'C', length: 5, position: 0, add: 'maj7', slashChord: null, addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
jasmine.objectContaining({chord: 'D', length: 3, position: 6, add: 'm7', slashChord: null, addDescriptor: descriptor('m7', {quality: 'minor', extensions: ['7']})}),
|
||||
jasmine.objectContaining({chord: 'G', length: 5, position: 10, add: 'sus4', slashChord: null, addDescriptor: descriptor('sus4', {suspensions: ['4']})}),
|
||||
jasmine.objectContaining({chord: 'A', length: 5, position: 16, add: 'add9', slashChord: null, addDescriptor: descriptor('add9', {additions: ['9']})}),
|
||||
jasmine.objectContaining({chord: 'C', length: 7, position: 22, add: 'maj7', slashChord: 'E', addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'C', length: 5, position: 0, add: 'maj7', slashChord: null, addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'D', length: 3, position: 6, add: 'm7', slashChord: null, addDescriptor: descriptor('m7', {quality: 'minor', extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'G', length: 5, position: 10, add: 'sus4', slashChord: null, addDescriptor: descriptor('sus4', {suspensions: ['4']})}),
|
||||
expect.objectContaining({chord: 'A', length: 5, position: 16, add: 'add9', slashChord: null, addDescriptor: descriptor('add9', {additions: ['9']})}),
|
||||
expect.objectContaining({chord: 'C', length: 7, position: 22, add: 'maj7', slashChord: 'E', addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -262,10 +262,10 @@ Text`;
|
||||
|
||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'H', length: 5, position: 0, add: 'moll', slashChord: null, addDescriptor: descriptor('moll', {quality: 'minor'})}),
|
||||
jasmine.objectContaining({chord: 'E', length: 4, position: 6, add: 'dur', slashChord: null, addDescriptor: descriptor('dur', {quality: 'major'})}),
|
||||
jasmine.objectContaining({chord: 'C', length: 5, position: 11, add: 'verm', slashChord: null, addDescriptor: descriptor('verm', {quality: 'diminished'})}),
|
||||
jasmine.objectContaining({chord: 'F', length: 4, position: 17, add: 'aug', slashChord: null, addDescriptor: descriptor('aug', {quality: 'augmented'})}),
|
||||
expect.objectContaining({chord: 'H', length: 5, position: 0, add: 'moll', slashChord: null, addDescriptor: descriptor('moll', {quality: 'minor'})}),
|
||||
expect.objectContaining({chord: 'E', length: 4, position: 6, add: 'dur', slashChord: null, addDescriptor: descriptor('dur', {quality: 'major'})}),
|
||||
expect.objectContaining({chord: 'C', length: 5, position: 11, add: 'verm', slashChord: null, addDescriptor: descriptor('verm', {quality: 'diminished'})}),
|
||||
expect.objectContaining({chord: 'F', length: 4, position: 17, add: 'aug', slashChord: null, addDescriptor: descriptor('aug', {quality: 'augmented'})}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -278,12 +278,12 @@ Text`;
|
||||
const sections = service.parse(text, null);
|
||||
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'C', length: 2, position: 0, add: '7', slashChord: null, addDescriptor: descriptor('7', {extensions: ['7']})}),
|
||||
jasmine.objectContaining({chord: 'D', length: 2, position: 3, add: '9', slashChord: null, addDescriptor: descriptor('9', {extensions: ['9']})}),
|
||||
jasmine.objectContaining({chord: 'E', length: 3, position: 6, add: '11', slashChord: null, addDescriptor: descriptor('11', {extensions: ['11']})}),
|
||||
jasmine.objectContaining({chord: 'F', length: 3, position: 10, add: '13', slashChord: null, addDescriptor: descriptor('13', {extensions: ['13']})}),
|
||||
jasmine.objectContaining({chord: 'G', length: 6, position: 14, add: 'add#9', slashChord: null, addDescriptor: descriptor('add#9', {additions: ['#9']})}),
|
||||
jasmine.objectContaining({chord: 'A', length: 4, position: 21, add: '7-5', slashChord: null, addDescriptor: descriptor('7-5', {extensions: ['7'], alterations: ['-5']})}),
|
||||
expect.objectContaining({chord: 'C', length: 2, position: 0, add: '7', slashChord: null, addDescriptor: descriptor('7', {extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'D', length: 2, position: 3, add: '9', slashChord: null, addDescriptor: descriptor('9', {extensions: ['9']})}),
|
||||
expect.objectContaining({chord: 'E', length: 3, position: 6, add: '11', slashChord: null, addDescriptor: descriptor('11', {extensions: ['11']})}),
|
||||
expect.objectContaining({chord: 'F', length: 3, position: 10, add: '13', slashChord: null, addDescriptor: descriptor('13', {extensions: ['13']})}),
|
||||
expect.objectContaining({chord: 'G', length: 6, position: 14, add: 'add#9', slashChord: null, addDescriptor: descriptor('add#9', {additions: ['#9']})}),
|
||||
expect.objectContaining({chord: 'A', length: 4, position: 21, add: '7-5', slashChord: null, addDescriptor: descriptor('7-5', {extensions: ['7'], alterations: ['-5']})}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -296,9 +296,9 @@ Text`;
|
||||
const sections = service.parse(text, null);
|
||||
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'e', length: 5, position: 0, add: 'moll', slashChord: null, addDescriptor: descriptor('moll', {quality: 'minor'})}),
|
||||
jasmine.objectContaining({chord: 'd', length: 4, position: 6, add: null, slashChord: 'F#', addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'c', length: 7, position: 11, add: 'maj7', slashChord: 'e', addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
expect.objectContaining({chord: 'e', length: 5, position: 0, add: 'moll', slashChord: null, addDescriptor: descriptor('moll', {quality: 'minor'})}),
|
||||
expect.objectContaining({chord: 'd', length: 4, position: 6, add: null, slashChord: 'F#', addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'c', length: 7, position: 11, add: 'maj7', slashChord: 'e', addDescriptor: descriptor('maj7', {quality: 'major', extensions: ['7']})}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -333,7 +333,7 @@ Text`;
|
||||
|
||||
void expect(sections[0].lines[0].text).toBe('Cmaj7(add9)');
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
chord: 'C',
|
||||
length: 11,
|
||||
position: 0,
|
||||
@@ -362,13 +362,13 @@ Text`;
|
||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||
void expect(sections[0].lines[0].text).toBe('C F G e C (F G)');
|
||||
void expect(sections[0].lines[0].chords).toEqual([
|
||||
jasmine.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'F', length: 1, position: 2, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'G', length: 1, position: 4, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'e', length: 1, position: 6, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'C', length: 1, position: 8, add: null, slashChord: null, addDescriptor: null}),
|
||||
jasmine.objectContaining({chord: 'F', length: 2, position: 11, add: null, slashChord: null, addDescriptor: null, prefix: '('}),
|
||||
jasmine.objectContaining({chord: 'G', length: 2, position: 14, add: null, slashChord: null, addDescriptor: null, suffix: ')'}),
|
||||
expect.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'F', length: 1, position: 2, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'G', length: 1, position: 4, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'e', length: 1, position: 6, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'C', length: 1, position: 8, add: null, slashChord: null, addDescriptor: null}),
|
||||
expect.objectContaining({chord: 'F', length: 2, position: 11, add: null, slashChord: null, addDescriptor: null, prefix: '('}),
|
||||
expect.objectContaining({chord: 'G', length: 2, position: 14, add: null, slashChord: null, addDescriptor: null, suffix: ')'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -389,8 +389,8 @@ Text`;
|
||||
it('should call the transpose service when a transpose mode is provided', () => {
|
||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||
const transposeService = TestBed.inject(TransposeService);
|
||||
const transposeSpy = spyOn(transposeService, 'transpose').and.callThrough();
|
||||
const renderSpy = spyOn(transposeService, 'renderChords').and.callThrough();
|
||||
const transposeSpy = vi.spyOn(transposeService, 'transpose');
|
||||
const renderSpy = vi.spyOn(transposeService, 'renderChords');
|
||||
const text = `Strophe
|
||||
C D E
|
||||
Text`;
|
||||
@@ -404,8 +404,8 @@ Text`;
|
||||
it('should use renderChords when no transpose mode is provided', () => {
|
||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||
const transposeService = TestBed.inject(TransposeService);
|
||||
const transposeSpy = spyOn(transposeService, 'transpose').and.callThrough();
|
||||
const renderSpy = spyOn(transposeService, 'renderChords').and.callThrough();
|
||||
const transposeSpy = vi.spyOn(transposeService, 'transpose');
|
||||
const renderSpy = vi.spyOn(transposeService, 'renderChords');
|
||||
const text = `Strophe
|
||||
C D E
|
||||
Text`;
|
||||
@@ -434,9 +434,9 @@ Am Dm7 cdur C
|
||||
Text`;
|
||||
|
||||
void expect(service.validateChordNotation(text)).toEqual([
|
||||
jasmine.objectContaining({lineNumber: 2, token: 'Am', suggestion: 'a', reason: 'minor_format'}),
|
||||
jasmine.objectContaining({lineNumber: 2, token: 'Dm7', suggestion: 'd7', reason: 'minor_format'}),
|
||||
jasmine.objectContaining({lineNumber: 2, token: 'cdur', suggestion: 'C', reason: 'major_format'}),
|
||||
expect.objectContaining({lineNumber: 2, token: 'Am', suggestion: 'a', reason: 'minor_format'}),
|
||||
expect.objectContaining({lineNumber: 2, token: 'Dm7', suggestion: 'd7', reason: 'minor_format'}),
|
||||
expect.objectContaining({lineNumber: 2, token: 'cdur', suggestion: 'C', reason: 'major_format'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -447,8 +447,8 @@ Am/C# Dm7/F#
|
||||
Text`;
|
||||
|
||||
void expect(service.validateChordNotation(text)).toEqual([
|
||||
jasmine.objectContaining({lineNumber: 2, token: 'Am/C#', suggestion: 'a/C#', reason: 'minor_format'}),
|
||||
jasmine.objectContaining({lineNumber: 2, token: 'Dm7/F#', suggestion: 'd7/F#', reason: 'minor_format'}),
|
||||
expect.objectContaining({lineNumber: 2, token: 'Am/C#', suggestion: 'a/C#', reason: 'minor_format'}),
|
||||
expect.objectContaining({lineNumber: 2, token: 'Dm7/F#', suggestion: 'd7/F#', reason: 'minor_format'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -462,7 +462,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: 'unknown_token', suggestion: null})]);
|
||||
void expect(service.validateChordNotation(text)).toEqual([expect.objectContaining({lineNumber: 2, token: 'Es', reason: 'unknown_token', suggestion: null})]);
|
||||
});
|
||||
|
||||
it('should flag unknown tokens on mostly chord lines', () => {
|
||||
@@ -471,7 +471,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([expect.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null})]);
|
||||
});
|
||||
|
||||
it('should reject tabs on chord lines', () => {
|
||||
@@ -480,7 +480,7 @@ Text`;
|
||||
|
||||
void expect(service.validateChordNotation(text)).toEqual(
|
||||
expect.arrayContaining([
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
lineNumber: 2,
|
||||
token: '\t',
|
||||
reason: 'tab_character',
|
||||
|
||||
@@ -80,6 +80,6 @@ describe('TransposeService', () => {
|
||||
const rendered = service.renderChords(line);
|
||||
|
||||
void expect(rendered.text.length).toBe(121);
|
||||
void expect(rendered.text.endsWith('C')).toBeTrue();
|
||||
void expect(rendered.text.endsWith('C')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,20 +6,19 @@ import {UploadService} from './upload.service';
|
||||
|
||||
describe('UploadService', () => {
|
||||
let service: UploadService;
|
||||
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
|
||||
let fileDataServiceSpy: {set: ReturnType<typeof vi.fn>};
|
||||
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(async () => {
|
||||
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['set']);
|
||||
fileDataServiceSpy.set.and.resolveTo('file-1');
|
||||
fileDataServiceSpy = {
|
||||
set: vi.fn().mockResolvedValue('file-1'),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
UploadService,
|
||||
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||
],
|
||||
@@ -28,6 +27,10 @@ describe('UploadService', () => {
|
||||
service = TestBed.inject(UploadService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
@@ -39,7 +42,8 @@ describe('UploadService', () => {
|
||||
success();
|
||||
},
|
||||
};
|
||||
const uploadSpy = spyOn(service as UploadServiceInternals, 'startUpload').and.returnValue(task);
|
||||
const uploadSpy = vi.fn<(path: string, file: File) => UploadTaskLike>().mockReturnValue(task);
|
||||
Object.assign(service, {startUpload: uploadSpy});
|
||||
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
|
||||
|
||||
service.pushUpload('song-1', upload);
|
||||
@@ -50,10 +54,10 @@ describe('UploadService', () => {
|
||||
expect(upload.path).toBe('/attachments/song-1');
|
||||
expect(fileDataServiceSpy.set).toHaveBeenCalledWith(
|
||||
'song-1',
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
name: 'test.pdf',
|
||||
path: '/attachments/song-1',
|
||||
createdAt: jasmine.any(Date),
|
||||
createdAt: expect.any(Date),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,16 +16,16 @@ describe('EditSongGuard', () => {
|
||||
it('should allow navigation when there is no nested editSongComponent', () => {
|
||||
const result = guard.canDeactivate({editSongComponent: null} as never, {} as never, {} as never, {} as never);
|
||||
|
||||
expect(result).toBeTrue();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should delegate to askForSave on the nested editSongComponent', async () => {
|
||||
const nextState = {url: '/songs'} as never;
|
||||
const askForSave = jasmine.createSpy('askForSave').and.resolveTo(true);
|
||||
const askForSave = vi.fn().mockResolvedValue(true);
|
||||
|
||||
const result = await guard.canDeactivate({editSongComponent: {askForSave}} as never, {} as never, {} as never, nextState);
|
||||
|
||||
expect(askForSave).toHaveBeenCalledWith(nextState);
|
||||
expect(result).toBeTrue();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,20 +18,27 @@ describe('SongComponent', () => {
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
|
||||
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
|
||||
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
||||
const songServiceSpy = {
|
||||
read$: vi.fn().mockReturnValue(of({id: '4711', title: 'Test Song', number: '1', text: '', showType: '', flags: ''} as never)),
|
||||
};
|
||||
const fileDataServiceSpy = {
|
||||
read$: vi.fn().mockReturnValue(of([])),
|
||||
};
|
||||
const userServiceSpy = {
|
||||
incSongCount: vi.fn(),
|
||||
decSongCount: vi.fn(),
|
||||
user$: of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'onlyFirst', songUsage: {'4711': 2}}),
|
||||
userId$: of('user-1'),
|
||||
});
|
||||
const showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['list$', 'update$']);
|
||||
const showSongServiceSpy = jasmine.createSpyObj<ShowSongService>('ShowSongService', ['new$']);
|
||||
|
||||
songServiceSpy.read$.and.returnValue(of({id: '4711', title: 'Test Song', number: '1', text: '', showType: '', flags: ''} as never));
|
||||
fileDataServiceSpy.read$.and.returnValue(of([]));
|
||||
showServiceSpy.list$.and.returnValue(of([]));
|
||||
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
|
||||
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
|
||||
loggedIn$: vi.fn().mockReturnValue(of(true)),
|
||||
getUserbyId$: vi.fn().mockReturnValue(of({name: 'Benjamin'})),
|
||||
};
|
||||
const showServiceSpy = {
|
||||
list$: vi.fn().mockReturnValue(of([])),
|
||||
update$: vi.fn(),
|
||||
};
|
||||
const showSongServiceSpy = {
|
||||
new$: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SongComponent],
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {AdminService} from './admin.service';
|
||||
import {SongDownloadService} from '../../modules/songs/services/song-download.service';
|
||||
import {ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
||||
import {UserSongUsageService} from '../user/user-song-usage.service';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
describe('AdminService', () => {
|
||||
let service: AdminService;
|
||||
let songDownloadSpy: {downloadSongs: ReturnType<typeof vi.fn>};
|
||||
let songUsageSpy: {rebuildSongUsage: ReturnType<typeof vi.fn>};
|
||||
let showSongIndexSpy: {rebuildShowSongIds: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
songDownloadSpy = {downloadSongs: vi.fn()};
|
||||
songUsageSpy = {rebuildSongUsage: vi.fn()};
|
||||
showSongIndexSpy = {rebuildShowSongIds: vi.fn()};
|
||||
|
||||
songDownloadSpy.downloadSongs.mockResolvedValue({fileName: 'songs-2026-06-09.json', songsDownloaded: 4});
|
||||
songUsageSpy.rebuildSongUsage.mockResolvedValue({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||
showSongIndexSpy.rebuildShowSongIds.mockResolvedValue({showsProcessed: 2, showSongsProcessed: 3});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: SongDownloadService, useValue: songDownloadSpy},
|
||||
{provide: UserSongUsageService, useValue: songUsageSpy},
|
||||
{provide: ShowSongIndexService, useValue: showSongIndexSpy},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(AdminService);
|
||||
});
|
||||
|
||||
it('should delegate admin operations to their domain services', async () => {
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await expect(service.downloadSongs()).resolves.toEqual({fileName: 'songs-2026-06-09.json', songsDownloaded: 4});
|
||||
await expect(service.rebuildSongUsage()).resolves.toEqual({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||
await expect(service.rebuildShowSongIds(onProgress)).resolves.toEqual({showsProcessed: 2, showSongsProcessed: 3});
|
||||
|
||||
expect(songDownloadSpy.downloadSongs).toHaveBeenCalled();
|
||||
expect(songUsageSpy.rebuildSongUsage).toHaveBeenCalled();
|
||||
expect(showSongIndexSpy.rebuildShowSongIds).toHaveBeenCalledWith(onProgress);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {SongDownloadResult, SongDownloadService} from '../../modules/songs/services/song-download.service';
|
||||
import {MigrationProgress, ShowSongIndexMigrationResult, ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
||||
import {SongUsageMigrationResult, UserSongUsageService} from '../user/user-song-usage.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AdminService {
|
||||
private songDownload = inject(SongDownloadService);
|
||||
private songUsage = inject(UserSongUsageService);
|
||||
private showSongIndex = inject(ShowSongIndexService);
|
||||
|
||||
public downloadSongs = (): Promise<SongDownloadResult> => this.songDownload.downloadSongs();
|
||||
public rebuildSongUsage = (): Promise<SongUsageMigrationResult> => this.songUsage.rebuildSongUsage();
|
||||
public rebuildShowSongIds = (onProgress?: (progress: MigrationProgress) => void): Promise<ShowSongIndexMigrationResult> => this.showSongIndex.rebuildShowSongIds(onProgress);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {DbService} from './db.service';
|
||||
import {ConfigService} from './config.service';
|
||||
|
||||
describe('ConfigService', () => {
|
||||
let service: ConfigService;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
let dbServiceSpy: {doc$: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$']);
|
||||
dbServiceSpy.doc$.and.returnValue(of({copyright: 'CCLI'}) as never);
|
||||
dbServiceSpy = {doc$: vi.fn()};
|
||||
dbServiceSpy.doc$.mockReturnValue(of({copyright: 'CCLI'}) as never);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -28,10 +29,10 @@ describe('ConfigService', () => {
|
||||
});
|
||||
|
||||
it('should expose the shared config stream via get$', async () => {
|
||||
await expectAsync(firstValueFrom(service.get$())).toBeResolvedTo({copyright: 'CCLI'} as never);
|
||||
await expect(firstValueFrom(service.get$())).resolves.toEqual({copyright: 'CCLI'} as never);
|
||||
});
|
||||
|
||||
it('should resolve the current config via get()', async () => {
|
||||
await expectAsync(service.get()).toBeResolvedTo({copyright: 'CCLI'} as never);
|
||||
await expect(service.get()).resolves.toEqual({copyright: 'CCLI'} as never);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {DbService} from './db.service';
|
||||
import {GlobalSettingsService} from './global-settings.service';
|
||||
|
||||
describe('GlobalSettingsService', () => {
|
||||
let service: GlobalSettingsService;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
let updateSpy: jasmine.Spy;
|
||||
let dbServiceSpy: {doc$: ReturnType<typeof vi.fn>; doc: ReturnType<typeof vi.fn>};
|
||||
let updateSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
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);
|
||||
updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
dbServiceSpy = {doc$: vi.fn(), doc: vi.fn()};
|
||||
dbServiceSpy.doc$.mockReturnValue(of({churchName: 'ICF'}) as never);
|
||||
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||
@@ -31,7 +32,7 @@ describe('GlobalSettingsService', () => {
|
||||
});
|
||||
|
||||
it('should expose the shared settings stream via the getter', async () => {
|
||||
await expectAsync(firstValueFrom(service.get$)).toBeResolvedTo({churchName: 'ICF'} as never);
|
||||
await expect(firstValueFrom(service.get$)).resolves.toEqual({churchName: 'ICF'} as never);
|
||||
});
|
||||
|
||||
it('should update the static global settings document', async () => {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {UserService} from '../user.service';
|
||||
import {UserNameComponent} from './user-name.component';
|
||||
|
||||
describe('UserNameComponent', () => {
|
||||
let component: UserNameComponent;
|
||||
let fixture: ComponentFixture<UserNameComponent>;
|
||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||
let userServiceSpy: {getUserbyId$: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
|
||||
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
|
||||
userServiceSpy = {getUserbyId$: vi.fn()};
|
||||
userServiceSpy.getUserbyId$.mockReturnValue(of({name: 'Benjamin'} as never));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserNameComponent],
|
||||
@@ -32,14 +33,14 @@ describe('UserNameComponent', () => {
|
||||
component.userId = 'user-1';
|
||||
|
||||
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1');
|
||||
await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo('Benjamin');
|
||||
await expect(firstValueFrom(component.name$!)).resolves.toEqual('Benjamin');
|
||||
});
|
||||
|
||||
it('should map missing users to null names', async () => {
|
||||
userServiceSpy.getUserbyId$.and.returnValue(of(null));
|
||||
userServiceSpy.getUserbyId$.mockReturnValue(of(null));
|
||||
|
||||
component.userId = 'missing-user';
|
||||
|
||||
await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo(null);
|
||||
await expect(firstValueFrom(component.name$!)).resolves.toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,23 +2,24 @@ import {TestBed} from '@angular/core/testing';
|
||||
import {Router} from '@angular/router';
|
||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||
import {Auth} from '@angular/fire/auth';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {DbService} from '../db.service';
|
||||
import {UserSessionService} from './user-session.service';
|
||||
|
||||
describe('UserSessionService', () => {
|
||||
let service: UserSessionService;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
let routerSpy: jasmine.SpyObj<Router>;
|
||||
let dbServiceSpy: {col$: ReturnType<typeof vi.fn>; doc$: ReturnType<typeof vi.fn>; doc: ReturnType<typeof vi.fn>};
|
||||
let routerSpy: {navigateByUrl: ReturnType<typeof vi.fn>};
|
||||
let authStateSubject: BehaviorSubject<unknown>;
|
||||
let createAuthStateSpy: jasmine.Spy;
|
||||
let runInFirebaseContextSpy: jasmine.Spy;
|
||||
let createAuthStateSpy: ReturnType<typeof vi.fn>;
|
||||
let runInFirebaseContextSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
authStateSubject = new BehaviorSubject<unknown>(null);
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc']);
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||
dbServiceSpy.col$.and.returnValue(of([{id: 'user-1'}]) as never);
|
||||
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||
dbServiceSpy = {col$: vi.fn(), doc$: vi.fn(), doc: vi.fn()};
|
||||
routerSpy = {navigateByUrl: vi.fn()};
|
||||
dbServiceSpy.col$.mockReturnValue(of([{id: 'user-1'}]) as never);
|
||||
dbServiceSpy.doc$.mockImplementation((path: string) => {
|
||||
if (path === 'users/user-1') {
|
||||
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters', songUsage: {}}) as never;
|
||||
}
|
||||
@@ -28,15 +29,15 @@ describe('UserSessionService', () => {
|
||||
|
||||
return of(null) as never;
|
||||
});
|
||||
dbServiceSpy.doc.and.returnValue({
|
||||
update: jasmine.createSpy('update').and.resolveTo(),
|
||||
set: jasmine.createSpy('set').and.resolveTo(),
|
||||
dbServiceSpy.doc.mockReturnValue({
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||
routerSpy.navigateByUrl.mockResolvedValue(true);
|
||||
|
||||
createAuthStateSpy = spyOn(UserSessionService.prototype as UserSessionService & {createAuthState$: () => unknown}, 'createAuthState$').and.returnValue(
|
||||
authStateSubject.asObservable() as never
|
||||
);
|
||||
createAuthStateSpy = vi
|
||||
.spyOn(UserSessionService.prototype as unknown as {createAuthState$: () => unknown}, 'createAuthState$')
|
||||
.mockReturnValue(authStateSubject.asObservable() as never) as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -47,7 +48,13 @@ describe('UserSessionService', () => {
|
||||
});
|
||||
|
||||
service = TestBed.inject(UserSessionService);
|
||||
runInFirebaseContextSpy = spyOn(service as UserSessionService & {runInFirebaseContext: (...args: unknown[]) => Promise<unknown>}, 'runInFirebaseContext');
|
||||
runInFirebaseContextSpy = vi.spyOn(service as unknown as {runInFirebaseContext: (...args: unknown[]) => Promise<unknown>}, 'runInFirebaseContext') as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
@@ -58,14 +65,14 @@ describe('UserSessionService', () => {
|
||||
it('should derive userId$ and loggedIn$ from authState', async () => {
|
||||
authStateSubject.next({uid: 'user-1'});
|
||||
|
||||
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
||||
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
||||
await expect(firstValueFrom(service.userId$)).resolves.toEqual('user-1');
|
||||
await expect(firstValueFrom(service.loggedIn$())).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
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 expect(firstValueFrom(service.user$)).resolves.toEqual(expect.objectContaining({id: 'user-1', name: 'Benjamin'}) as never);
|
||||
});
|
||||
|
||||
it('should cache user lookups by id', async () => {
|
||||
@@ -73,30 +80,30 @@ describe('UserSessionService', () => {
|
||||
const second$ = service.getUserbyId$('user-2');
|
||||
|
||||
expect(first$).toBe(second$);
|
||||
await expectAsync(firstValueFrom(first$)).toBeResolvedTo(jasmine.objectContaining({id: 'user-2'}) as never);
|
||||
await expect(firstValueFrom(first$)).resolves.toEqual(expect.objectContaining({id: 'user-2'}) as never);
|
||||
});
|
||||
|
||||
it('should login and initialize songUsage when missing', async () => {
|
||||
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||
dbServiceSpy.doc$.mockImplementation((path: string) => {
|
||||
if (path === 'users/user-1') {
|
||||
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters'}) as never;
|
||||
}
|
||||
|
||||
return of(null) as never;
|
||||
});
|
||||
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
||||
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
|
||||
const updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||
runInFirebaseContextSpy.mockResolvedValue({user: {uid: 'user-1'}});
|
||||
authStateSubject.next({uid: 'user-1'});
|
||||
|
||||
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
|
||||
await expect(service.login('mail', 'secret')).resolves.toEqual('user-1');
|
||||
|
||||
expect(runInFirebaseContextSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledWith({songUsage: {}});
|
||||
});
|
||||
|
||||
it('should wait for auth state propagation before resolving login', async () => {
|
||||
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
|
||||
runInFirebaseContextSpy.mockResolvedValue({user: {uid: 'user-1'}});
|
||||
|
||||
let resolved = false;
|
||||
const loginPromise = service.login('mail', 'secret').then(result => {
|
||||
@@ -105,15 +112,15 @@ describe('UserSessionService', () => {
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
expect(resolved).toBeFalse();
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
authStateSubject.next({uid: 'user-1'});
|
||||
|
||||
await expectAsync(loginPromise).toBeResolvedTo('user-1');
|
||||
await expect(loginPromise).resolves.toEqual('user-1');
|
||||
});
|
||||
|
||||
it('should delegate logout and password reset to AngularFire auth APIs', async () => {
|
||||
runInFirebaseContextSpy.and.resolveTo();
|
||||
runInFirebaseContextSpy.mockResolvedValue(undefined);
|
||||
|
||||
await service.logout();
|
||||
await service.changePassword('mail@example.com');
|
||||
@@ -122,16 +129,16 @@ describe('UserSessionService', () => {
|
||||
});
|
||||
|
||||
it('should create a new user document and navigate afterwards', async () => {
|
||||
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||
dbServiceSpy.doc$.mockImplementation((path: string) => {
|
||||
if (path === 'users/user-3') {
|
||||
return of({id: 'user-3', name: 'New User', role: 'user', chordMode: 'onlyFirst', songUsage: {}}) as never;
|
||||
}
|
||||
|
||||
return of(null) as never;
|
||||
});
|
||||
const setSpy = jasmine.createSpy('set').and.resolveTo();
|
||||
dbServiceSpy.doc.and.returnValue({set: setSpy} as never);
|
||||
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-3'}});
|
||||
const setSpy = vi.fn().mockResolvedValue(undefined);
|
||||
dbServiceSpy.doc.mockReturnValue({set: setSpy} as never);
|
||||
runInFirebaseContextSpy.mockResolvedValue({user: {uid: 'user-3'}});
|
||||
|
||||
await service.createNewUser('mail@example.com', 'New User', 'secret');
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {DbService} from '../db.service';
|
||||
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
||||
import {ShowSongDataService} from '../../modules/shows/services/show-song-data.service';
|
||||
@@ -8,31 +9,32 @@ import {UserSongUsageService} from './user-song-usage.service';
|
||||
|
||||
describe('UserSongUsageService', () => {
|
||||
let service: UserSongUsageService;
|
||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||
let dbServiceSpy: {doc: ReturnType<typeof vi.fn>};
|
||||
let sessionSpy: {update$: ReturnType<typeof vi.fn>; user$: unknown; users$: unknown};
|
||||
let showDataServiceSpy: {listRaw$: ReturnType<typeof vi.fn>};
|
||||
let showSongDataServiceSpy: {list$: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
||||
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
||||
dbServiceSpy = {doc: vi.fn()};
|
||||
sessionSpy = {
|
||||
update$: vi.fn(),
|
||||
user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never,
|
||||
users$: of([{id: 'user-1'}, {id: 'user-2'}]) as never,
|
||||
});
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$']);
|
||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
||||
};
|
||||
showDataServiceSpy = {listRaw$: vi.fn()};
|
||||
showSongDataServiceSpy = {list$: vi.fn()};
|
||||
|
||||
sessionSpy.update$.and.resolveTo();
|
||||
showDataServiceSpy.listRaw$.and.returnValue(
|
||||
sessionSpy.update$.mockResolvedValue(undefined);
|
||||
showDataServiceSpy.listRaw$.mockReturnValue(
|
||||
of([
|
||||
{id: 'show-1', owner: 'user-1', archived: false},
|
||||
{id: 'show-2', owner: 'user-2', archived: true},
|
||||
]) as never
|
||||
);
|
||||
showSongDataServiceSpy.list$.and.callFake(
|
||||
showSongDataServiceSpy.list$.mockImplementation(
|
||||
(showId: string) => of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never
|
||||
);
|
||||
dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never);
|
||||
dbServiceSpy.doc.mockReturnValue({update: vi.fn().mockResolvedValue(undefined)} as never);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -51,19 +53,19 @@ describe('UserSongUsageService', () => {
|
||||
});
|
||||
|
||||
it('should increment and decrement song usage for the current user', async () => {
|
||||
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
||||
const updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||
|
||||
await service.incSongCount('song-1');
|
||||
await service.decSongCount('song-2');
|
||||
|
||||
expect(dbServiceSpy.doc).toHaveBeenCalledWith('users/user-1');
|
||||
expect(updateSpy.calls.argsFor(0)[0]).toEqual({'songUsage.song-1': jasmine.anything()});
|
||||
expect(updateSpy.calls.argsFor(1)[0]).toEqual({'songUsage.song-2': jasmine.anything()});
|
||||
expect(updateSpy.mock.calls[0]?.[0]).toEqual({'songUsage.song-1': expect.anything()});
|
||||
expect(updateSpy.mock.calls[1]?.[0]).toEqual({'songUsage.song-2': expect.anything()});
|
||||
});
|
||||
|
||||
it('should rebuild song usage for all users based on owned show songs', async () => {
|
||||
await expectAsync(service.rebuildSongUsage()).toBeResolvedTo({
|
||||
await expect(service.rebuildSongUsage()).resolves.toEqual({
|
||||
usersProcessed: 2,
|
||||
showsProcessed: 2,
|
||||
showSongsProcessed: 3,
|
||||
@@ -76,6 +78,6 @@ describe('UserSongUsageService', () => {
|
||||
it('should reject song usage rebuilds for non-admin users', async () => {
|
||||
Object.defineProperty(sessionSpy, 'user$', {value: of({id: 'user-1', role: 'leader'})});
|
||||
|
||||
await expectAsync(service.rebuildSongUsage()).toBeRejectedWithError('Admin role required to rebuild songUsage.');
|
||||
await expect(service.rebuildSongUsage()).rejects.toThrow('Admin role required to rebuild songUsage.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,49 +1,64 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {UserService} from './user.service';
|
||||
import {UserSessionService} from './user-session.service';
|
||||
import {UserSongUsageService} from './user-song-usage.service';
|
||||
import {ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||
let songUsageSpy: jasmine.SpyObj<UserSongUsageService>;
|
||||
let showSongIndexSpy: jasmine.SpyObj<ShowSongIndexService>;
|
||||
let sessionSpy: {
|
||||
currentUser: ReturnType<typeof vi.fn>;
|
||||
getUserbyId: ReturnType<typeof vi.fn>;
|
||||
getUserbyId$: ReturnType<typeof vi.fn>;
|
||||
login: ReturnType<typeof vi.fn>;
|
||||
loggedIn$: ReturnType<typeof vi.fn>;
|
||||
list$: ReturnType<typeof vi.fn>;
|
||||
logout: ReturnType<typeof vi.fn>;
|
||||
update$: ReturnType<typeof vi.fn>;
|
||||
changePassword: ReturnType<typeof vi.fn>;
|
||||
createNewUser: ReturnType<typeof vi.fn>;
|
||||
users$: unknown;
|
||||
userId$: unknown;
|
||||
user$: unknown;
|
||||
};
|
||||
let songUsageSpy: {incSongCount: ReturnType<typeof vi.fn>; decSongCount: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(async () => {
|
||||
sessionSpy = jasmine.createSpyObj<UserSessionService>(
|
||||
'UserSessionService',
|
||||
['currentUser', 'getUserbyId', 'getUserbyId$', 'login', 'loggedIn$', 'list$', 'logout', 'update$', 'changePassword', 'createNewUser'],
|
||||
{
|
||||
sessionSpy = {
|
||||
currentUser: vi.fn(),
|
||||
getUserbyId: vi.fn(),
|
||||
getUserbyId$: vi.fn(),
|
||||
login: vi.fn(),
|
||||
loggedIn$: vi.fn(),
|
||||
list$: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
update$: vi.fn(),
|
||||
changePassword: vi.fn(),
|
||||
createNewUser: vi.fn(),
|
||||
users$: of([{id: 'user-1'}]) as never,
|
||||
userId$: of('user-1'),
|
||||
user$: of({id: 'user-1'}) as never,
|
||||
}
|
||||
);
|
||||
songUsageSpy = jasmine.createSpyObj<UserSongUsageService>('UserSongUsageService', ['incSongCount', 'decSongCount', 'rebuildSongUsage']);
|
||||
showSongIndexSpy = jasmine.createSpyObj<ShowSongIndexService>('ShowSongIndexService', ['rebuildShowSongIds']);
|
||||
};
|
||||
songUsageSpy = {incSongCount: vi.fn(), decSongCount: vi.fn()};
|
||||
|
||||
sessionSpy.currentUser.and.resolveTo({id: 'user-1'} as never);
|
||||
sessionSpy.getUserbyId.and.resolveTo({id: 'user-2'} as never);
|
||||
sessionSpy.getUserbyId$.and.returnValue(of({id: 'user-2'}) as never);
|
||||
sessionSpy.login.and.resolveTo('user-1');
|
||||
sessionSpy.loggedIn$.and.returnValue(of(true));
|
||||
sessionSpy.list$.and.returnValue(of([{id: 'user-1'}]) as never);
|
||||
sessionSpy.logout.and.resolveTo();
|
||||
sessionSpy.update$.and.resolveTo();
|
||||
sessionSpy.changePassword.and.resolveTo();
|
||||
sessionSpy.createNewUser.and.resolveTo();
|
||||
songUsageSpy.incSongCount.and.resolveTo();
|
||||
songUsageSpy.decSongCount.and.resolveTo();
|
||||
songUsageSpy.rebuildSongUsage.and.resolveTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||
showSongIndexSpy.rebuildShowSongIds.and.resolveTo({showsProcessed: 2, showSongsProcessed: 3});
|
||||
sessionSpy.currentUser.mockResolvedValue({id: 'user-1'} as never);
|
||||
sessionSpy.getUserbyId.mockResolvedValue({id: 'user-2'} as never);
|
||||
sessionSpy.getUserbyId$.mockReturnValue(of({id: 'user-2'}) as never);
|
||||
sessionSpy.login.mockResolvedValue('user-1');
|
||||
sessionSpy.loggedIn$.mockReturnValue(of(true));
|
||||
sessionSpy.list$.mockReturnValue(of([{id: 'user-1'}]) as never);
|
||||
sessionSpy.logout.mockResolvedValue(undefined);
|
||||
sessionSpy.update$.mockResolvedValue(undefined);
|
||||
sessionSpy.changePassword.mockResolvedValue(undefined);
|
||||
sessionSpy.createNewUser.mockResolvedValue(undefined);
|
||||
songUsageSpy.incSongCount.mockResolvedValue(undefined);
|
||||
songUsageSpy.decSongCount.mockResolvedValue(undefined);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: UserSessionService, useValue: sessionSpy},
|
||||
{provide: UserSongUsageService, useValue: songUsageSpy},
|
||||
{provide: ShowSongIndexService, useValue: showSongIndexSpy},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -55,15 +70,15 @@ describe('UserService', () => {
|
||||
});
|
||||
|
||||
it('should expose the session streams directly', async () => {
|
||||
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
||||
await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo({id: 'user-1'} as never);
|
||||
await expectAsync(firstValueFrom(service.users$)).toBeResolvedTo([{id: 'user-1'}] as never);
|
||||
await expect(firstValueFrom(service.userId$)).resolves.toEqual('user-1');
|
||||
await expect(firstValueFrom(service.user$)).resolves.toEqual({id: 'user-1'} as never);
|
||||
await expect(firstValueFrom(service.users$)).resolves.toEqual([{id: 'user-1'}] as never);
|
||||
});
|
||||
|
||||
it('should delegate session operations to UserSessionService', async () => {
|
||||
await expectAsync(service.currentUser()).toBeResolvedTo({id: 'user-1'} as never);
|
||||
await expectAsync(service.getUserbyId('user-2')).toBeResolvedTo({id: 'user-2'} as never);
|
||||
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
|
||||
await expect(service.currentUser()).resolves.toEqual({id: 'user-1'} as never);
|
||||
await expect(service.getUserbyId('user-2')).resolves.toEqual({id: 'user-2'} as never);
|
||||
await expect(service.login('mail', 'secret')).resolves.toEqual('user-1');
|
||||
await service.logout();
|
||||
await service.update$('user-1', {name: 'Benjamin'} as never);
|
||||
await service.changePassword('mail');
|
||||
@@ -79,9 +94,9 @@ describe('UserService', () => {
|
||||
});
|
||||
|
||||
it('should delegate user lookup and loggedIn/list streams to UserSessionService', async () => {
|
||||
await expectAsync(firstValueFrom(service.getUserbyId$('user-2'))).toBeResolvedTo({id: 'user-2'} as never);
|
||||
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
||||
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([{id: 'user-1'}] as never);
|
||||
await expect(firstValueFrom(service.getUserbyId$('user-2'))).resolves.toEqual({id: 'user-2'} as never);
|
||||
await expect(firstValueFrom(service.loggedIn$())).resolves.toEqual(true);
|
||||
await expect(firstValueFrom(service.list$())).resolves.toEqual([{id: 'user-1'}] as never);
|
||||
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
||||
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
||||
expect(sessionSpy.list$).toHaveBeenCalled();
|
||||
@@ -90,12 +105,8 @@ describe('UserService', () => {
|
||||
it('should delegate song usage operations to UserSongUsageService', async () => {
|
||||
await service.incSongCount('song-1');
|
||||
await service.decSongCount('song-2');
|
||||
await expectAsync(service.rebuildSongUsage()).toBeResolvedTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||
await expectAsync(service.rebuildShowSongIds()).toBeResolvedTo({showsProcessed: 2, showSongsProcessed: 3});
|
||||
|
||||
expect(songUsageSpy.incSongCount).toHaveBeenCalledWith('song-1');
|
||||
expect(songUsageSpy.decSongCount).toHaveBeenCalledWith('song-2');
|
||||
expect(songUsageSpy.rebuildSongUsage).toHaveBeenCalled();
|
||||
expect(showSongIndexSpy.rebuildShowSongIds).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {User} from './user';
|
||||
import {SongUsageMigrationResult, UserSongUsageService} from './user-song-usage.service';
|
||||
import {UserSongUsageService} from './user-song-usage.service';
|
||||
import {UserSessionService} from './user-session.service';
|
||||
import {MigrationProgress, ShowSongIndexMigrationResult, ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -11,7 +10,6 @@ import {MigrationProgress, ShowSongIndexMigrationResult, ShowSongIndexService} f
|
||||
export class UserService {
|
||||
private session = inject(UserSessionService);
|
||||
private songUsage = inject(UserSongUsageService);
|
||||
private showSongIndex = inject(ShowSongIndexService);
|
||||
|
||||
public users$ = this.session.users$;
|
||||
|
||||
@@ -35,6 +33,4 @@ export class UserService {
|
||||
public createNewUser = (user: string, name: string, password: string): Promise<void> => this.session.createNewUser(user, name, password);
|
||||
public incSongCount = (songId: string): Promise<void | null> => this.songUsage.incSongCount(songId);
|
||||
public decSongCount = (songId: string): Promise<void | null> => this.songUsage.decSongCount(songId);
|
||||
public rebuildSongUsage = (): Promise<SongUsageMigrationResult> => this.songUsage.rebuildSongUsage();
|
||||
public rebuildShowSongIds = (onProgress?: (progress: MigrationProgress) => void): Promise<ShowSongIndexMigrationResult> => this.showSongIndex.rebuildShowSongIds(onProgress);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ describe('SidebarComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(PageFrameComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('title', 'Test');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
@@ -21,12 +23,12 @@ describe('SidebarComponent', () => {
|
||||
});
|
||||
|
||||
it('should toggle and close the sidebar', () => {
|
||||
expect(component.collapsed).toBeTrue();
|
||||
expect(component.collapsed).toBe(true);
|
||||
|
||||
component.toggle();
|
||||
expect(component.collapsed).toBeFalse();
|
||||
expect(component.collapsed).toBe(false);
|
||||
|
||||
component.close();
|
||||
expect(component.collapsed).toBeTrue();
|
||||
expect(component.collapsed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Router} from '@angular/router';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {UserService} from '../../services/user/user.service';
|
||||
import {RoleGuard} from './role.guard';
|
||||
|
||||
describe('RoleGuard', () => {
|
||||
let guard: RoleGuard;
|
||||
let routerSpy: jasmine.SpyObj<Router>;
|
||||
let createUrlTreeSpy: ReturnType<typeof vi.fn>;
|
||||
let routerSpy: Pick<Router, 'createUrlTree'>;
|
||||
|
||||
beforeEach(async () => {
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
routerSpy.createUrlTree.and.callFake(commands => ({commands}) as never);
|
||||
createUrlTreeSpy = vi.fn().mockImplementation(commands => ({commands}) as never);
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -31,12 +33,13 @@ describe('RoleGuard', () => {
|
||||
});
|
||||
|
||||
it('should deny access when there is no current user', async () => {
|
||||
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo({commands: ['brand', 'new-user']} as never);
|
||||
await expect(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).resolves.toEqual({commands: ['brand', 'new-user']} as never);
|
||||
});
|
||||
|
||||
it('should allow admins regardless of requiredRoles', async () => {
|
||||
TestBed.resetTestingModule();
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
createUrlTreeSpy = vi.fn();
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Router, useValue: routerSpy},
|
||||
@@ -45,12 +48,13 @@ describe('RoleGuard', () => {
|
||||
});
|
||||
guard = TestBed.inject(RoleGuard);
|
||||
|
||||
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
|
||||
await expect(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
it('should allow users with a matching required role', async () => {
|
||||
TestBed.resetTestingModule();
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
createUrlTreeSpy = vi.fn();
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Router, useValue: routerSpy},
|
||||
@@ -59,13 +63,13 @@ describe('RoleGuard', () => {
|
||||
});
|
||||
guard = TestBed.inject(RoleGuard);
|
||||
|
||||
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
|
||||
await expect(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
it('should redirect users without the required role to their role default route', async () => {
|
||||
TestBed.resetTestingModule();
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
|
||||
createUrlTreeSpy = vi.fn().mockReturnValue({redirect: ['presentation']} as never);
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Router, useValue: routerSpy},
|
||||
@@ -75,14 +79,14 @@ describe('RoleGuard', () => {
|
||||
guard = TestBed.inject(RoleGuard);
|
||||
|
||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never));
|
||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']);
|
||||
expect(createUrlTreeSpy).toHaveBeenCalledWith(['presentation']);
|
||||
expect(result).toEqual({redirect: ['presentation']} as never);
|
||||
});
|
||||
|
||||
it('should redirect members to shows instead of new-user', async () => {
|
||||
TestBed.resetTestingModule();
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
|
||||
createUrlTreeSpy = vi.fn().mockReturnValue({redirect: ['shows']} as never);
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Router, useValue: routerSpy},
|
||||
@@ -92,14 +96,14 @@ describe('RoleGuard', () => {
|
||||
guard = TestBed.inject(RoleGuard);
|
||||
|
||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['user']}} as never));
|
||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
||||
expect(createUrlTreeSpy).toHaveBeenCalledWith(['shows']);
|
||||
expect(result).toEqual({redirect: ['shows']} as never);
|
||||
});
|
||||
|
||||
it('should choose a matching default route from all assigned roles', async () => {
|
||||
TestBed.resetTestingModule();
|
||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
|
||||
createUrlTreeSpy = vi.fn().mockImplementation(commands => ({redirect: commands}) as never);
|
||||
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Router, useValue: routerSpy},
|
||||
@@ -109,7 +113,7 @@ describe('RoleGuard', () => {
|
||||
guard = TestBed.inject(RoleGuard);
|
||||
|
||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['presenter']}} as never));
|
||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
||||
expect(createUrlTreeSpy).toHaveBeenCalledWith(['shows']);
|
||||
expect(result).toEqual({redirect: ['shows']} as never);
|
||||
});
|
||||
});
|
||||
|
||||
+11
-4
@@ -13,11 +13,12 @@ import {provideAuth} from '@angular/fire/auth';
|
||||
import {initializeApp} from 'firebase/app';
|
||||
import {getAuth} from 'firebase/auth';
|
||||
import {initializeFirestore, persistentLocalCache, persistentMultipleTabManager} from 'firebase/firestore';
|
||||
import {UserService} from './app/services/user/user.service';
|
||||
import {AdminService} from './app/services/admin/admin.service';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
wgeneratorAdmin?: {
|
||||
downloadSongs(): Promise<unknown>;
|
||||
rebuildSongUsage(): Promise<unknown>;
|
||||
rebuildShowSongIds(): Promise<unknown>;
|
||||
};
|
||||
@@ -50,12 +51,18 @@ bootstrapApplication(AppComponent, {
|
||||
],
|
||||
})
|
||||
.then(appRef => {
|
||||
const userService = appRef.injector.get(UserService);
|
||||
const adminService = appRef.injector.get(AdminService);
|
||||
window.wgeneratorAdmin = {
|
||||
rebuildSongUsage: () => userService.rebuildSongUsage(),
|
||||
downloadSongs: async () => {
|
||||
console.info('[wgeneratorAdmin] downloadSongs started');
|
||||
const result = await adminService.downloadSongs();
|
||||
console.info('[wgeneratorAdmin] downloadSongs finished', result);
|
||||
return result;
|
||||
},
|
||||
rebuildSongUsage: () => adminService.rebuildSongUsage(),
|
||||
rebuildShowSongIds: async () => {
|
||||
console.info('[wgeneratorAdmin] rebuildShowSongIds started');
|
||||
const result = await userService.rebuildShowSongIds(progress => {
|
||||
const result = await adminService.rebuildShowSongIds(progress => {
|
||||
console.info(`[wgeneratorAdmin] rebuildShowSongIds ${progress.processed}/${progress.total} shows processed`, {
|
||||
showId: progress.showId,
|
||||
showSongsProcessed: progress.showSongsProcessed,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {expect, vi} from 'vitest';
|
||||
|
||||
import 'zone.js/testing';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
||||
@@ -25,20 +23,6 @@ type DocumentStub = {
|
||||
collection: () => CollectionStub;
|
||||
};
|
||||
|
||||
type MockFunction = ReturnType<typeof vi.fn> & {
|
||||
and: {
|
||||
returnValue: (value: unknown) => MockFunction;
|
||||
resolveTo: (value: unknown) => MockFunction;
|
||||
rejectWith: (value: unknown) => MockFunction;
|
||||
callFake: (fn: (...args: unknown[]) => unknown) => MockFunction;
|
||||
callThrough: () => MockFunction;
|
||||
};
|
||||
calls: {
|
||||
argsFor: (index: number) => unknown[];
|
||||
mostRecent: () => {args: unknown[]};
|
||||
};
|
||||
};
|
||||
|
||||
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||
|
||||
@@ -92,130 +76,3 @@ const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef
|
||||
return originalConfigureTestingModule(mergedModuleDef);
|
||||
};
|
||||
TestBed.configureTestingModule = configureTestingModule;
|
||||
|
||||
function decorateMock<T extends ReturnType<typeof vi.fn>>(mock: T): T & MockFunction {
|
||||
const decorated = mock as T & MockFunction;
|
||||
|
||||
Object.defineProperty(decorated, 'and', {
|
||||
configurable: true,
|
||||
get: () => ({
|
||||
returnValue(value: unknown) {
|
||||
decorated.mockReturnValue(value);
|
||||
return decorated;
|
||||
},
|
||||
resolveTo(value: unknown) {
|
||||
decorated.mockResolvedValue(value);
|
||||
return decorated;
|
||||
},
|
||||
rejectWith(value: unknown) {
|
||||
decorated.mockRejectedValue(value);
|
||||
return decorated;
|
||||
},
|
||||
callFake(fn: (...args: unknown[]) => unknown) {
|
||||
decorated.mockImplementation(fn);
|
||||
return decorated;
|
||||
},
|
||||
callThrough() {
|
||||
return decorated;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
Object.defineProperty(decorated, 'calls', {
|
||||
configurable: true,
|
||||
get: () => ({
|
||||
argsFor(index: number) {
|
||||
const calls = decorated.mock.calls as unknown[][];
|
||||
return calls[index] ?? [];
|
||||
},
|
||||
mostRecent() {
|
||||
const args = decorated.mock.lastCall ?? [];
|
||||
return {args};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return decorated;
|
||||
}
|
||||
|
||||
function createSpy(name?: string): MockFunction {
|
||||
const spy = decorateMock(vi.fn());
|
||||
if (name) {
|
||||
spy.mockName(name);
|
||||
}
|
||||
|
||||
return spy;
|
||||
}
|
||||
|
||||
function createSpyObj<T>(baseName: string, methodNames: string[] | Record<string, unknown>, propertyValues?: Record<string, unknown>): T {
|
||||
const result: Record<string, unknown> = {};
|
||||
const methods = Array.isArray(methodNames) ? methodNames : Object.keys(methodNames);
|
||||
|
||||
for (const methodName of methods) {
|
||||
result[methodName] = createSpy(`${baseName}.${methodName}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(methodNames)) {
|
||||
for (const [key, value] of Object.entries(methodNames)) {
|
||||
(result[key] as MockFunction).and.returnValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyValues) {
|
||||
for (const [key, value] of Object.entries(propertyValues)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
function spyOnCompat<T extends object, K extends keyof T>(object: T, methodName: K): MockFunction {
|
||||
const spy = vi.spyOn(object as Record<PropertyKey, (...args: unknown[]) => unknown>, methodName as PropertyKey);
|
||||
return decorateMock(spy as unknown as ReturnType<typeof vi.fn>);
|
||||
}
|
||||
|
||||
function expectAsyncCompat<T>(value: Promise<T>) {
|
||||
return {
|
||||
async toBeResolvedTo(expected: T) {
|
||||
await expect(value).resolves.toEqual(expected);
|
||||
},
|
||||
async toBeRejectedWithError(expected?: string | RegExp | Error) {
|
||||
if (expected instanceof Error) {
|
||||
await expect(value).rejects.toThrowError(expected.message);
|
||||
} else if (expected !== undefined) {
|
||||
await expect(value).rejects.toThrowError(expected);
|
||||
} else {
|
||||
await expect(value).rejects.toThrowError();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toBeTrue(received: unknown) {
|
||||
return {
|
||||
pass: received === true,
|
||||
message: () => `expected ${String(received)} to be true`,
|
||||
};
|
||||
},
|
||||
toBeFalse(received: unknown) {
|
||||
return {
|
||||
pass: received === false,
|
||||
message: () => `expected ${String(received)} to be false`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(globalThis, {
|
||||
spyOn: spyOnCompat,
|
||||
expectAsync: expectAsyncCompat,
|
||||
jasmine: {
|
||||
createSpy,
|
||||
createSpyObj,
|
||||
any: expect.any,
|
||||
anything: expect.anything,
|
||||
objectContaining: expect.objectContaining,
|
||||
stringMatching: expect.stringMatching,
|
||||
},
|
||||
});
|
||||
|
||||
Vendored
-43
@@ -1,43 +0,0 @@
|
||||
type UnknownFunction = (...args: unknown[]) => unknown;
|
||||
|
||||
type SpyAnd = {
|
||||
returnValue(value?: unknown): jasmine.Spy;
|
||||
resolveTo(value?: unknown): jasmine.Spy;
|
||||
rejectWith(value?: unknown): jasmine.Spy;
|
||||
callFake(fn: UnknownFunction): jasmine.Spy;
|
||||
callThrough(): jasmine.Spy;
|
||||
};
|
||||
|
||||
type SpyCalls = {
|
||||
argsFor(index: number): unknown[];
|
||||
mostRecent(): {args: unknown[]};
|
||||
};
|
||||
|
||||
declare global {
|
||||
function spyOn<T extends object, K extends keyof T>(object: T, methodName: K): jasmine.Spy;
|
||||
function expectAsync<T>(value: Promise<T>): {
|
||||
toBeResolvedTo(expected: T): Promise<void>;
|
||||
toBeRejectedWithError(expected?: string | RegExp | Error): Promise<void>;
|
||||
};
|
||||
|
||||
namespace jasmine {
|
||||
type Spy<T extends UnknownFunction = UnknownFunction> = T &
|
||||
ReturnType<(typeof import('vitest'))['vi']['fn']> & {
|
||||
and: SpyAnd;
|
||||
calls: SpyCalls;
|
||||
};
|
||||
|
||||
type SpyObj<T> = {
|
||||
[K in keyof T]: T[K] extends UnknownFunction ? Spy<T[K]> : T[K];
|
||||
};
|
||||
|
||||
function createSpy(name?: string): Spy;
|
||||
function createSpyObj<T>(baseName: string, methodNames: string[] | Record<string, unknown>, propertyValues?: Record<string, unknown>): SpyObj<T>;
|
||||
function any(expectedClass: unknown): unknown;
|
||||
function anything(): unknown;
|
||||
function objectContaining<T>(value: Partial<T>): unknown;
|
||||
function stringMatching(value: string | RegExp): unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
import 'vitest';
|
||||
|
||||
declare module 'vitest' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interface Assertion<T = any> {
|
||||
toBeTrue(): T;
|
||||
toBeFalse(): T;
|
||||
}
|
||||
|
||||
interface AsymmetricMatchersContaining {
|
||||
toBeTrue(): void;
|
||||
toBeFalse(): void;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user