fix unit tests
This commit is contained in:
@@ -1,33 +1,36 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Firestore} from '@angular/fire/firestore';
|
import {Firestore} from '@angular/fire/firestore';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {DbService} from 'src/app/services/db.service';
|
import {DbService} from 'src/app/services/db.service';
|
||||||
import {GuestShowDataService} from './guest-show-data.service';
|
import {GuestShowDataService} from './guest-show-data.service';
|
||||||
|
|
||||||
describe('GuestShowDataService', () => {
|
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 service: GuestShowDataService;
|
||||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||||
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
let docDeleteSpy: ReturnType<typeof vi.fn>;
|
||||||
let docSpy: jasmine.Spy;
|
let docSpy: PathSpy<DocumentRefStub>;
|
||||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||||
let colSpy: jasmine.Spy;
|
let colSpy: PathSpy<CollectionRefStub>;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
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;
|
let firestoreStub: Firestore;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docSpy = jasmine.createSpy('doc').and.returnValue({
|
docSpy = vi.fn().mockReturnValue({
|
||||||
update: docUpdateSpy,
|
update: docUpdateSpy,
|
||||||
delete: docDeleteSpy,
|
delete: docDeleteSpy,
|
||||||
});
|
}) as PathSpy<DocumentRefStub>;
|
||||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'guest-2'});
|
colAddSpy = vi.fn().mockResolvedValue({id: 'guest-2'});
|
||||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
colSpy = vi.fn().mockReturnValue({add: colAddSpy}) as PathSpy<CollectionRefStub>;
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
dbServiceSpy = {col$: vi.fn(), doc$: vi.fn(), doc: docSpy, col: colSpy};
|
||||||
dbServiceSpy.col$.and.returnValue(of([{id: 'guest-1'}]) as never);
|
dbServiceSpy.col$.mockReturnValue(of([{id: 'guest-1'}]) as never);
|
||||||
dbServiceSpy.doc$.and.returnValue(of({id: 'guest-1'}) as never);
|
dbServiceSpy.doc$.mockReturnValue(of({id: 'guest-1'}) as never);
|
||||||
dbServiceSpy.doc.and.callFake(docSpy);
|
|
||||||
dbServiceSpy.col.and.callFake(colSpy);
|
|
||||||
firestoreStub = {} as Firestore;
|
firestoreStub = {} as Firestore;
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -60,15 +63,15 @@ describe('GuestShowDataService', () => {
|
|||||||
await service.update$('guest-7', {published: true} as never);
|
await service.update$('guest-7', {published: true} as never);
|
||||||
|
|
||||||
expect(docSpy).toHaveBeenCalledWith('guest/guest-7');
|
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});
|
expect(updatePayload).toEqual({published: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a guest show and return the created id', async () => {
|
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');
|
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});
|
expect(addPayload).toEqual({published: false});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import {GuestShowService} from './guest-show.service';
|
|||||||
import {ShowService} from '../shows/services/show.service';
|
import {ShowService} from '../shows/services/show.service';
|
||||||
import {Show} from '../shows/services/show';
|
import {Show} from '../shows/services/show';
|
||||||
import {Song} from '../songs/services/song';
|
import {Song} from '../songs/services/song';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
|
|
||||||
describe('GuestShowService', () => {
|
describe('GuestShowService', () => {
|
||||||
let service: GuestShowService;
|
let service: GuestShowService;
|
||||||
let guestShowDataServiceSpy: jasmine.SpyObj<GuestShowDataService>;
|
let guestShowDataServiceSpy: {add: ReturnType<typeof vi.fn>; update$: ReturnType<typeof vi.fn>};
|
||||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
let showServiceSpy: {update$: ReturnType<typeof vi.fn>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
guestShowDataServiceSpy = jasmine.createSpyObj<GuestShowDataService>('GuestShowDataService', ['add', 'update$']);
|
guestShowDataServiceSpy = {add: vi.fn(), update$: vi.fn()};
|
||||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['update$']);
|
showServiceSpy = {update$: vi.fn()};
|
||||||
guestShowDataServiceSpy.add.and.resolveTo('share-1');
|
guestShowDataServiceSpy.add.mockResolvedValue('share-1');
|
||||||
guestShowDataServiceSpy.update$.and.resolveTo();
|
guestShowDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
showServiceSpy.update$.and.resolveTo();
|
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -36,17 +37,17 @@ describe('GuestShowService', () => {
|
|||||||
const songs = [{id: 'song-1'}] as unknown as Song[];
|
const songs = [{id: 'song-1'}] as unknown as Song[];
|
||||||
const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-1';
|
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(
|
expect(addPayload).toEqual(
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
showType: 'service-worship',
|
showType: 'service-worship',
|
||||||
date: show.date,
|
date: show.date,
|
||||||
songs,
|
songs,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(addPayload['updatedAt']).toEqual(jasmine.any(Date));
|
expect(addPayload['updatedAt']).toEqual(expect.any(Date));
|
||||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {shareId: 'share-1'});
|
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 songs = [{id: 'song-1'}] as unknown as Song[];
|
||||||
const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-9';
|
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(shareId).toBe('share-9');
|
||||||
expect(updatePayload).toEqual(
|
expect(updatePayload).toEqual(
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
showType: 'service-worship',
|
showType: 'service-worship',
|
||||||
date: show.date,
|
date: show.date,
|
||||||
songs,
|
songs,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(updatePayload['updatedAt']).toEqual(jasmine.any(Date));
|
expect(updatePayload['updatedAt']).toEqual(expect.any(Date));
|
||||||
expect(guestShowDataServiceSpy.add).not.toHaveBeenCalled();
|
expect(guestShowDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {GuestComponent} from './guest.component';
|
import {GuestComponent} from './guest.component';
|
||||||
import {GuestShowDataService} from './guest-show-data.service';
|
import {GuestShowDataService} from './guest-show-data.service';
|
||||||
|
|
||||||
describe('GuestComponent', () => {
|
describe('GuestComponent', () => {
|
||||||
let component: GuestComponent;
|
let component: GuestComponent;
|
||||||
let fixture: ComponentFixture<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>;
|
let guestShowSubject: BehaviorSubject<unknown>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -26,8 +27,8 @@ describe('GuestComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
guestShowDataServiceSpy = jasmine.createSpyObj<GuestShowDataService>('GuestShowDataService', ['read', 'read$']);
|
guestShowDataServiceSpy = {read: vi.fn(), read$: vi.fn()};
|
||||||
guestShowDataServiceSpy.read.and.resolveTo({
|
guestShowDataServiceSpy.read.mockResolvedValue({
|
||||||
id: 'guest-1',
|
id: 'guest-1',
|
||||||
showType: 'service-worship',
|
showType: 'service-worship',
|
||||||
date: {
|
date: {
|
||||||
@@ -42,7 +43,7 @@ describe('GuestComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as never);
|
} as never);
|
||||||
guestShowDataServiceSpy.read$.and.returnValue(guestShowSubject.asObservable() as never);
|
guestShowDataServiceSpy.read$.mockReturnValue(guestShowSubject.asObservable() as never);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [GuestComponent],
|
imports: [GuestComponent],
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<app-page-frame title="Hilfe" [withMenu]="false">
|
<app-page-frame title="Hilfe" [withMenu]="false">
|
||||||
<div content>
|
<div content>
|
||||||
<app-card [heading]="heading">
|
<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) {-->
|
<!-- @if (loading && !hasLoadedContent) {-->
|
||||||
<!-- <div class="help-state">Hilfe wird geladen.</div>-->
|
<!-- <div class="help-state">Hilfe wird geladen.</div>-->
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ describe('HelpComponent', () => {
|
|||||||
let component: HelpComponent;
|
let component: HelpComponent;
|
||||||
let fixture: ComponentFixture<HelpComponent>;
|
let fixture: ComponentFixture<HelpComponent>;
|
||||||
let fetchMock: ReturnType<typeof vi.fn>;
|
let fetchMock: ReturnType<typeof vi.fn>;
|
||||||
|
type FetchResponse = {ok: boolean; text: () => Promise<string>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fetchMock = vi.fn().mockResolvedValue({
|
fetchMock = vi.fn().mockResolvedValue({
|
||||||
@@ -40,11 +41,11 @@ describe('HelpComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should keep the current content visible while the next page loads', async () => {
|
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(
|
fetchMock.mockImplementationOnce(
|
||||||
() =>
|
() =>
|
||||||
new Promise(resolve => {
|
new Promise<FetchResponse>(resolve => {
|
||||||
resolveFetch = resolve;
|
resolveFetch = resolve;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -52,19 +53,21 @@ describe('HelpComponent', () => {
|
|||||||
const oldHtml = component.renderedHtml;
|
const oldHtml = component.renderedHtml;
|
||||||
const oldHeading = component.heading;
|
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();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(component.loading).toBe(true);
|
expect(component.loading).toBe(true);
|
||||||
expect(component.renderedHtml).toBe(oldHtml);
|
expect(component.renderedHtml).toBe(oldHtml);
|
||||||
expect(component.heading).toBe(oldHeading);
|
expect(component.heading).toBe(oldHeading);
|
||||||
|
|
||||||
resolveFetch?.({
|
resolveFetch({
|
||||||
ok: true,
|
ok: true,
|
||||||
text: () => Promise.resolve('# Liedliste\n\nInhalt'),
|
text: () => Promise.resolve('# Liedliste\n\nInhalt'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await fixture.whenStable();
|
await loadPromise;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(component.loading).toBe(false);
|
expect(component.loading).toBe(false);
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export class HelpComponent implements OnInit {
|
|||||||
void this.loadDocument(resolvedPath);
|
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> {
|
private async loadDocument(path: string): Promise<void> {
|
||||||
if (!path.startsWith('man/')) {
|
if (!path.startsWith('man/')) {
|
||||||
if (!this.hasLoadedContent) {
|
if (!this.hasLoadedContent) {
|
||||||
@@ -300,14 +308,18 @@ export class HelpComponent implements OnInit {
|
|||||||
return `<a href="${safeHref}">${safeLabel}</a>`;
|
return `<a href="${safeHref}">${safeLabel}</a>`;
|
||||||
});
|
});
|
||||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
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;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
private decodePlainText(text: string): string {
|
private decodePlainText(text: string): string {
|
||||||
const html = this.renderInlineMarkdown(text);
|
const html = this.renderInlineMarkdown(this.decodeHtmlEntities(text));
|
||||||
const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html)?.replace(/<[^>]+>/g, '').trim() ?? text;
|
const sanitized =
|
||||||
|
this.sanitizer
|
||||||
|
.sanitize(SecurityContext.HTML, html)
|
||||||
|
?.replace(/<[^>]+>/g, '')
|
||||||
|
.trim() ?? text;
|
||||||
return this.decodeHtmlEntities(sanitized);
|
return this.decodeHtmlEntities(sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
||||||
import {ShowService} from '../../shows/services/show.service';
|
import {ShowService} from '../../shows/services/show.service';
|
||||||
import {SelectComponent} from './select.component';
|
import {SelectComponent} from './select.component';
|
||||||
@@ -8,29 +9,47 @@ import {SelectComponent} from './select.component';
|
|||||||
describe('SelectComponent', () => {
|
describe('SelectComponent', () => {
|
||||||
let component: SelectComponent;
|
let component: SelectComponent;
|
||||||
let fixture: ComponentFixture<SelectComponent>;
|
let fixture: ComponentFixture<SelectComponent>;
|
||||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
let showServiceSpy: {
|
||||||
let globalSettingsServiceSpy: jasmine.SpyObj<GlobalSettingsService>;
|
list$: ReturnType<typeof vi.fn>;
|
||||||
let routerSpy: jasmine.SpyObj<Router>;
|
update$: ReturnType<typeof vi.fn>;
|
||||||
const createShow = (id: string, isoDate: string) => ({id, date: {toDate: () => new Date(isoDate)}});
|
};
|
||||||
|
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 () => {
|
beforeEach(async () => {
|
||||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['list$', 'update$']);
|
showServiceSpy = {
|
||||||
globalSettingsServiceSpy = jasmine.createSpyObj<GlobalSettingsService>('GlobalSettingsService', ['set']);
|
list$: vi.fn().mockReturnValue(
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
of([
|
||||||
|
createShow('older', 45),
|
||||||
showServiceSpy.list$.and.returnValue(
|
createShow('recent-a', 7),
|
||||||
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
|
createShow('recent-b', 14),
|
||||||
);
|
]) as never
|
||||||
showServiceSpy.update$.and.resolveTo();
|
),
|
||||||
globalSettingsServiceSpy.set.and.resolveTo();
|
update$: vi.fn().mockResolvedValue(undefined),
|
||||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
};
|
||||||
|
globalSettingsServiceSpy = {
|
||||||
|
set: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
routerSpy = {
|
||||||
|
navigateByUrl: vi.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SelectComponent],
|
imports: [SelectComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ShowService, useValue: showServiceSpy},
|
{provide: ShowService, useValue: showServiceSpy as unknown as ShowService},
|
||||||
{provide: GlobalSettingsService, useValue: globalSettingsServiceSpy},
|
{provide: GlobalSettingsService, useValue: globalSettingsServiceSpy as unknown as GlobalSettingsService},
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy as unknown as Router},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
@@ -45,7 +64,7 @@ describe('SelectComponent', () => {
|
|||||||
it('should become visible on init', () => {
|
it('should become visible on init', () => {
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
|
|
||||||
expect(component.visible).toBeTrue();
|
expect(component.visible).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expose recent shows sorted descending by date', async () => {
|
it('should expose recent shows sorted descending by date', async () => {
|
||||||
@@ -61,7 +80,7 @@ describe('SelectComponent', () => {
|
|||||||
|
|
||||||
await component.selectShow(show);
|
await component.selectShow(show);
|
||||||
|
|
||||||
expect(component.visible).toBeFalse();
|
expect(component.visible).toBe(false);
|
||||||
expect(globalSettingsServiceSpy.set).toHaveBeenCalledWith({currentShow: 'show-1'});
|
expect(globalSettingsServiceSpy.set).toHaveBeenCalledWith({currentShow: 'show-1'});
|
||||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {presentationSongId: 'title'});
|
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {presentationSongId: 'title'});
|
||||||
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/presentation/remote');
|
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/presentation/remote');
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ describe('ReportDialogComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should mark numbers as opened locally', () => {
|
it('should mark numbers as opened locally', () => {
|
||||||
expect(component.wasOpened('12345')).toBeFalse();
|
expect(component.wasOpened('12345')).toBe(false);
|
||||||
|
|
||||||
component.markOpened('12345');
|
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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {ShareDialogComponent} from './share-dialog.component';
|
import {ShareDialogComponent} from './share-dialog.component';
|
||||||
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
|
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||||
|
import {vi} from 'vitest';
|
||||||
|
|
||||||
describe('ShareDialogComponent', () => {
|
describe('ShareDialogComponent', () => {
|
||||||
let component: ShareDialogComponent;
|
let component: ShareDialogComponent;
|
||||||
let fixture: ComponentFixture<ShareDialogComponent>;
|
let fixture: ComponentFixture<ShareDialogComponent>;
|
||||||
type ShareDialogComponentInternals = ShareDialogComponent & {
|
type ShareDialogComponentInternals = {
|
||||||
generateQrCode: () => Promise<string>;
|
generateQrCode: () => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,10 +29,14 @@ describe('ShareDialogComponent', () => {
|
|||||||
|
|
||||||
fixture = TestBed.createComponent(ShareDialogComponent);
|
fixture = TestBed.createComponent(ShareDialogComponent);
|
||||||
component = fixture.componentInstance;
|
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();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
void expect(component).toBeTruthy();
|
void expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
|
|||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {Timestamp} from '@angular/fire/firestore';
|
import {Timestamp} from '@angular/fire/firestore';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {ShowDataService} from '../services/show-data.service';
|
import {ShowDataService} from '../services/show-data.service';
|
||||||
import {ShowService} from '../services/show.service';
|
import {ShowService} from '../services/show.service';
|
||||||
import {EditComponent} from './edit.component';
|
import {EditComponent} from './edit.component';
|
||||||
@@ -9,25 +10,35 @@ import {EditComponent} from './edit.component';
|
|||||||
describe('EditComponent', () => {
|
describe('EditComponent', () => {
|
||||||
let component: EditComponent;
|
let component: EditComponent;
|
||||||
let fixture: ComponentFixture<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 showDataServiceStub: Pick<ShowDataService, 'list$'>;
|
||||||
let routerSpy: jasmine.SpyObj<Router>;
|
let routerSpy: {
|
||||||
|
navigateByUrl: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
const createDate = (isoDate: string) => ({toDate: () => new Date(isoDate)});
|
const createDate = (isoDate: string) => ({toDate: () => new Date(isoDate)});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['read$', 'update$']);
|
showServiceSpy = {
|
||||||
|
read$: vi.fn(),
|
||||||
|
update$: vi.fn(),
|
||||||
|
};
|
||||||
showDataServiceStub = {list$: of([]) as ShowDataService['list$']};
|
showDataServiceStub = {list$: of([]) as ShowDataService['list$']};
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
routerSpy = {
|
||||||
|
navigateByUrl: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
showServiceSpy.read$.and.returnValue(
|
showServiceSpy.read$.mockReturnValue(
|
||||||
of({
|
of({
|
||||||
id: 'show-1',
|
id: 'show-1',
|
||||||
showType: 'service-worship',
|
showType: 'service-worship',
|
||||||
date: createDate('2026-03-10T00:00:00Z'),
|
date: createDate('2026-03-10T00:00:00Z'),
|
||||||
} as never)
|
} as never)
|
||||||
);
|
);
|
||||||
showServiceSpy.update$.and.resolveTo();
|
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
routerSpy.navigateByUrl.mockResolvedValue(true);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [EditComponent],
|
imports: [EditComponent],
|
||||||
@@ -43,6 +54,10 @@ describe('EditComponent', () => {
|
|||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -68,7 +83,7 @@ describe('EditComponent', () => {
|
|||||||
it('should update the show and navigate back to the detail page', async () => {
|
it('should update the show and navigate back to the detail page', async () => {
|
||||||
const date = new Date('2026-03-11T00:00:00Z');
|
const date = new Date('2026-03-11T00:00:00Z');
|
||||||
const firestoreTimestamp = {seconds: 1} as never;
|
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'});
|
component.form.setValue({id: 'show-1', date, showType: 'home-group'});
|
||||||
|
|
||||||
await component.onSave();
|
await component.onSave();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {DocxService} from './docx.service';
|
import {DocxService} from './docx.service';
|
||||||
|
|
||||||
describe('DocxService', () => {
|
describe('DocxService', () => {
|
||||||
@@ -15,7 +16,7 @@ describe('DocxService', () => {
|
|||||||
type DocxModuleLike = {
|
type DocxModuleLike = {
|
||||||
Packer: {toBlob: (document: unknown) => Promise<Blob>};
|
Packer: {toBlob: (document: unknown) => Promise<Blob>};
|
||||||
};
|
};
|
||||||
type DocxServiceInternals = DocxService & {
|
type DocxServiceInternals = {
|
||||||
prepareData: (showId: string) => Promise<PreparedData | null>;
|
prepareData: (showId: string) => Promise<PreparedData | null>;
|
||||||
renderTitle: (docx: unknown, title: string) => unknown[];
|
renderTitle: (docx: unknown, title: string) => unknown[];
|
||||||
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
|
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
|
||||||
@@ -29,14 +30,18 @@ describe('DocxService', () => {
|
|||||||
service = TestBed.inject(DocxService);
|
service = TestBed.inject(DocxService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
void expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not try to save a document when the required data cannot be prepared', async () => {
|
it('should not try to save a document when the required data cannot be prepared', async () => {
|
||||||
const serviceInternals = service as DocxServiceInternals;
|
const serviceInternals = service as unknown as DocxServiceInternals;
|
||||||
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo(null);
|
const prepareDataSpy = vi.spyOn(serviceInternals, 'prepareData').mockResolvedValue(null);
|
||||||
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
|
const saveAsSpy = vi.spyOn(serviceInternals, 'saveAs');
|
||||||
|
|
||||||
await service.create('show-1');
|
await service.create('show-1');
|
||||||
|
|
||||||
@@ -48,11 +53,11 @@ describe('DocxService', () => {
|
|||||||
const blob = new Blob(['docx']);
|
const blob = new Blob(['docx']);
|
||||||
const docxModule: DocxModuleLike = {
|
const docxModule: DocxModuleLike = {
|
||||||
Packer: {
|
Packer: {
|
||||||
toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob),
|
toBlob: vi.fn().mockResolvedValue(blob),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const serviceInternals = service as DocxServiceInternals;
|
const serviceInternals = service as unknown as DocxServiceInternals;
|
||||||
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo({
|
const prepareDataSpy = vi.spyOn(serviceInternals, 'prepareData').mockResolvedValue({
|
||||||
show: {
|
show: {
|
||||||
showType: 'service-worship',
|
showType: 'service-worship',
|
||||||
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
|
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
|
||||||
@@ -61,17 +66,17 @@ describe('DocxService', () => {
|
|||||||
user: {name: 'Benjamin'},
|
user: {name: 'Benjamin'},
|
||||||
config: {ccliLicenseId: '12345'},
|
config: {ccliLicenseId: '12345'},
|
||||||
});
|
});
|
||||||
spyOn(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
|
vi.spyOn(serviceInternals, 'loadDocx').mockResolvedValue(docxModule);
|
||||||
spyOn(serviceInternals, 'renderTitle').and.returnValue([]);
|
vi.spyOn(serviceInternals, 'renderTitle').mockReturnValue([]);
|
||||||
spyOn(serviceInternals, 'renderSongs').and.returnValue([]);
|
vi.spyOn(serviceInternals, 'renderSongs').mockReturnValue([]);
|
||||||
const prepareNewDocumentSpy = spyOn(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
|
const prepareNewDocumentSpy = vi.spyOn(serviceInternals, 'prepareNewDocument').mockReturnValue({doc: true});
|
||||||
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
|
const saveAsSpy = vi.spyOn(serviceInternals, 'saveAs');
|
||||||
|
|
||||||
await service.create('show-1', {copyright: true});
|
await service.create('show-1', {copyright: true});
|
||||||
|
|
||||||
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
||||||
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
||||||
expect(docxModule.Packer.toBlob).toHaveBeenCalledWith({doc: true} as never);
|
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 {TestBed} from '@angular/core/testing';
|
||||||
import {firstValueFrom, of, Subject} from 'rxjs';
|
import {firstValueFrom, of, Subject} from 'rxjs';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {DbService} from '../../../services/db.service';
|
import {DbService} from '../../../services/db.service';
|
||||||
import {ShowDataService} from './show-data.service';
|
import {ShowDataService} from './show-data.service';
|
||||||
|
|
||||||
describe('ShowDataService', () => {
|
describe('ShowDataService', () => {
|
||||||
let service: ShowDataService;
|
let service: ShowDataService;
|
||||||
let shows$: Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>;
|
let shows$: Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>;
|
||||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||||
let docSpy: jasmine.Spy;
|
let docSpy: ReturnType<typeof vi.fn>;
|
||||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||||
let colSpy: jasmine.Spy;
|
let colSpy: ReturnType<typeof vi.fn>;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {
|
||||||
|
col$: ReturnType<typeof vi.fn>;
|
||||||
|
doc$: ReturnType<typeof vi.fn>;
|
||||||
|
doc: ReturnType<typeof vi.fn>;
|
||||||
|
col: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
shows$ = new Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>();
|
shows$ = new Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>();
|
||||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docSpy = jasmine.createSpy('doc').and.returnValue({update: docUpdateSpy});
|
docSpy = vi.fn().mockReturnValue({update: docUpdateSpy});
|
||||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-3'});
|
colAddSpy = vi.fn().mockResolvedValue({id: 'show-3'});
|
||||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
colSpy = vi.fn().mockReturnValue({add: colAddSpy});
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
dbServiceSpy = {
|
||||||
dbServiceSpy.col$.and.returnValue(shows$.asObservable());
|
col$: vi.fn().mockReturnValue(shows$.asObservable()),
|
||||||
dbServiceSpy.doc$.and.returnValue(of(null));
|
doc$: vi.fn().mockReturnValue(of(null)),
|
||||||
dbServiceSpy.doc.and.callFake(docSpy);
|
doc: docSpy,
|
||||||
dbServiceSpy.col.and.callFake(colSpy);
|
col: colSpy,
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
@@ -77,12 +84,12 @@ describe('ShowDataService', () => {
|
|||||||
|
|
||||||
it('should request only published recent shows and filter archived entries', async () => {
|
it('should request only published recent shows and filter archived entries', async () => {
|
||||||
const publicShows$ = of([{id: 'show-1', archived: false}, {id: 'show-2', archived: true}, {id: 'show-3'}]);
|
const publicShows$ = of([{id: 'show-1', archived: false}, {id: 'show-2', archived: true}, {id: 'show-3'}]);
|
||||||
dbServiceSpy.col$.and.returnValue(publicShows$ as never);
|
dbServiceSpy.col$.mockReturnValue(publicShows$ as never);
|
||||||
|
|
||||||
const result = await firstValueFrom(service.listPublicSince$(3));
|
const result = await firstValueFrom(service.listPublicSince$(3));
|
||||||
|
|
||||||
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows', jasmine.any(Array));
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows', expect.any(Array));
|
||||||
const [, queryConstraints] = dbServiceSpy.col$.calls.mostRecent().args as [string, unknown[]];
|
const [, queryConstraints] = dbServiceSpy.col$.mock.lastCall as [string, unknown[]];
|
||||||
expect(queryConstraints.length).toBe(3);
|
expect(queryConstraints.length).toBe(3);
|
||||||
expect(result.map(show => show.id)).toEqual(['show-1', 'show-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});
|
await service.update('show-8', {archived: true});
|
||||||
|
|
||||||
expect(docSpy).toHaveBeenCalledWith('shows/show-8');
|
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});
|
expect(updatePayload).toEqual({archived: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a show to the shows collection and return the new id', async () => {
|
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');
|
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});
|
expect(addPayload).toEqual({published: true});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {DbService} from '../../../services/db.service';
|
import {DbService} from '../../../services/db.service';
|
||||||
import {ShowSongDataService} from './show-song-data.service';
|
import {ShowSongDataService} from './show-song-data.service';
|
||||||
|
|
||||||
describe('ShowSongDataService', () => {
|
describe('ShowSongDataService', () => {
|
||||||
let service: ShowSongDataService;
|
let service: ShowSongDataService;
|
||||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
let docUpdateSpy: ReturnType<typeof vi.fn>;
|
||||||
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
let docDeleteSpy: ReturnType<typeof vi.fn>;
|
||||||
let docSpy: jasmine.Spy;
|
let docSpy: ReturnType<typeof vi.fn>;
|
||||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
let colAddSpy: ReturnType<typeof vi.fn>;
|
||||||
let colSpy: jasmine.Spy;
|
let colSpy: ReturnType<typeof vi.fn>;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {
|
||||||
|
col$: ReturnType<typeof vi.fn>;
|
||||||
|
doc$: ReturnType<typeof vi.fn>;
|
||||||
|
doc: ReturnType<typeof vi.fn>;
|
||||||
|
col: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docSpy = jasmine.createSpy('doc').and.returnValue({
|
docSpy = vi.fn().mockReturnValue({
|
||||||
update: docUpdateSpy,
|
update: docUpdateSpy,
|
||||||
delete: docDeleteSpy,
|
delete: docDeleteSpy,
|
||||||
});
|
});
|
||||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-song-3'});
|
colAddSpy = vi.fn().mockResolvedValue({id: 'show-song-3'});
|
||||||
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
colSpy = vi.fn().mockReturnValue({add: colAddSpy});
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
dbServiceSpy = {
|
||||||
dbServiceSpy.col$.and.callFake(() => of([{id: 'show-song-1'}]) as never);
|
col$: vi.fn().mockImplementation(() => of([{id: 'show-song-1'}]) as never),
|
||||||
dbServiceSpy.doc$.and.returnValue(of({id: 'show-song-1'}) as never);
|
doc$: vi.fn().mockReturnValue(of({id: 'show-song-1'}) as never),
|
||||||
dbServiceSpy.doc.and.callFake(docSpy);
|
doc: docSpy,
|
||||||
dbServiceSpy.col.and.callFake(colSpy);
|
col: colSpy,
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
@@ -77,7 +84,7 @@ describe('ShowSongDataService', () => {
|
|||||||
await service.update$('show-4', 'song-5', {title: 'Updated'} as never);
|
await service.update$('show-4', 'song-5', {title: 'Updated'} as never);
|
||||||
|
|
||||||
expect(docSpy).toHaveBeenCalledWith('shows/show-4/songs/song-5');
|
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'});
|
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 () => {
|
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');
|
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'});
|
expect(addPayload).toEqual({songId: 'song-5'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
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 {ShowDataService} from './show-data.service';
|
||||||
import {ShowSongDataService} from './show-song-data.service';
|
import {ShowSongDataService} from './show-song-data.service';
|
||||||
import {ShowSongIndexService} from './show-song-index.service';
|
import {ShowSongIndexService} from './show-song-index.service';
|
||||||
@@ -8,20 +9,34 @@ import {User} from '../../../services/user/user';
|
|||||||
|
|
||||||
describe('ShowSongIndexService', () => {
|
describe('ShowSongIndexService', () => {
|
||||||
let service: ShowSongIndexService;
|
let service: ShowSongIndexService;
|
||||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
let showDataServiceSpy: {
|
||||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
listRaw$: ReturnType<typeof vi.fn>;
|
||||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
update: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
let showSongDataServiceSpy: {
|
||||||
|
list$: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
let sessionSpy: {
|
||||||
|
update$: ReturnType<typeof vi.fn>;
|
||||||
|
user$: Observable<User>;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$', 'update']);
|
showDataServiceSpy = {
|
||||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
listRaw$: vi.fn(),
|
||||||
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
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),
|
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.listRaw$.mockReturnValue(of([{id: 'show-1'}, {id: 'show-2'}] as never) as unknown as ReturnType<ShowDataService['listRaw$']>);
|
||||||
showDataServiceSpy.update.and.resolveTo();
|
showDataServiceSpy.update.mockResolvedValue(undefined);
|
||||||
showSongDataServiceSpy.list$.and.callFake((showId: string) => {
|
showSongDataServiceSpy.list$.mockImplementation((showId: string) => {
|
||||||
if (showId === 'show-1') {
|
if (showId === 'show-1') {
|
||||||
return of([{songId: 'song-1'}, {songId: 'song-2'}, {songId: 'song-1'}] as never) as never;
|
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 () => {
|
it('should rebuild the distinct songIds index for all shows', async () => {
|
||||||
await expectAsync(service.rebuildShowSongIds()).toBeResolvedTo({
|
await expect(service.rebuildShowSongIds()).resolves.toEqual({
|
||||||
showsProcessed: 2,
|
showsProcessed: 2,
|
||||||
showSongsProcessed: 4,
|
showSongsProcessed: 4,
|
||||||
});
|
});
|
||||||
@@ -55,6 +70,6 @@ describe('ShowSongIndexService', () => {
|
|||||||
value: of({id: 'user-1', name: 'User', role: 'leader', chordMode: 'onlyFirst', songUsage: {}} as User),
|
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 {TestBed} from '@angular/core/testing';
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {SongDataService} from '../../songs/services/song-data.service';
|
import {SongDataService} from '../../songs/services/song-data.service';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
import {ShowService} from './show.service';
|
import {ShowService} from './show.service';
|
||||||
@@ -12,10 +13,25 @@ import {User} from '../../../services/user/user';
|
|||||||
|
|
||||||
describe('ShowSongService', () => {
|
describe('ShowSongService', () => {
|
||||||
let service: ShowSongService;
|
let service: ShowSongService;
|
||||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
let showSongDataServiceSpy: {
|
||||||
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
add: ReturnType<typeof vi.fn>;
|
||||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
read$: ReturnType<typeof vi.fn>;
|
||||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
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>;
|
let user$: BehaviorSubject<User | null>;
|
||||||
const song = {id: 'song-1', key: 'G', title: 'Amazing Grace'} as unknown as Song;
|
const song = {id: 'song-1', key: 'G', title: 'Amazing Grace'} as unknown as Song;
|
||||||
const showSong = {id: 'show-song-1', songId: 'song-1'} as unknown as ShowSong;
|
const showSong = {id: 'show-song-1', songId: 'song-1'} as unknown as ShowSong;
|
||||||
@@ -23,23 +39,36 @@ describe('ShowSongService', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user$ = new BehaviorSubject<User | null>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'onlyFirst', songUsage: {}});
|
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$']);
|
showSongDataServiceSpy = {
|
||||||
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$']);
|
add: vi.fn(),
|
||||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
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(),
|
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.add.mockResolvedValue('show-song-2');
|
||||||
showSongDataServiceSpy.read$.and.returnValue(of(showSong));
|
showSongDataServiceSpy.read$.mockReturnValue(of(showSong));
|
||||||
showSongDataServiceSpy.list$.and.returnValue(of([showSong]));
|
showSongDataServiceSpy.list$.mockReturnValue(of([showSong]));
|
||||||
showSongDataServiceSpy.delete.and.resolveTo();
|
showSongDataServiceSpy.delete.mockResolvedValue(undefined);
|
||||||
showSongDataServiceSpy.update$.and.resolveTo();
|
showSongDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
songDataServiceSpy.read$.and.returnValue(of(song));
|
songDataServiceSpy.read$.mockReturnValue(of(song));
|
||||||
userServiceSpy.incSongCount.and.resolveTo();
|
userServiceSpy.incSongCount.mockResolvedValue(undefined);
|
||||||
userServiceSpy.decSongCount.and.resolveTo();
|
userServiceSpy.decSongCount.mockResolvedValue(undefined);
|
||||||
showServiceSpy.read$.and.returnValue(of(show));
|
showServiceSpy.read$.mockReturnValue(of(show));
|
||||||
showServiceSpy.update$.and.resolveTo();
|
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -58,7 +87,7 @@ describe('ShowSongService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a show song from the song and current user', async () => {
|
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(userServiceSpy.incSongCount).toHaveBeenCalledWith('song-1');
|
||||||
expect(showSongDataServiceSpy.add).toHaveBeenCalledWith('show-1', {
|
expect(showSongDataServiceSpy.add).toHaveBeenCalledWith('show-1', {
|
||||||
@@ -72,9 +101,9 @@ describe('ShowSongService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when the song is missing', async () => {
|
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(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||||
@@ -83,19 +112,19 @@ describe('ShowSongService', () => {
|
|||||||
it('should return null when the current user is missing', async () => {
|
it('should return null when the current user is missing', async () => {
|
||||||
user$.next(null);
|
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(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate reads to the data service', async () => {
|
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');
|
expect(showSongDataServiceSpy.read$).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate list access to the data service', async () => {
|
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');
|
expect(showSongDataServiceSpy.list$).toHaveBeenCalledWith('show-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,12 +132,12 @@ describe('ShowSongService', () => {
|
|||||||
await service.delete$('show-1', 'show-song-1', 0);
|
await service.delete$('show-1', 'show-song-1', 0);
|
||||||
|
|
||||||
expect(showSongDataServiceSpy.delete).toHaveBeenCalledWith('show-1', 'show-song-1');
|
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');
|
expect(userServiceSpy.decSongCount).toHaveBeenCalledWith('song-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop delete when the show is missing', async () => {
|
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);
|
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 () => {
|
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);
|
await service.delete$('show-1', 'show-song-1', 0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {ShowDataService} from './show-data.service';
|
import {ShowDataService} from './show-data.service';
|
||||||
import {ShowService} from './show.service';
|
import {ShowService} from './show.service';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
|
||||||
describe('ShowService', () => {
|
describe('ShowService', () => {
|
||||||
let service: 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 user$: BehaviorSubject<unknown>;
|
||||||
let shows$: BehaviorSubject<unknown[]>;
|
let shows$: BehaviorSubject<unknown[]>;
|
||||||
const shows = [
|
const shows = [
|
||||||
@@ -18,13 +25,17 @@ describe('ShowService', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||||
shows$ = new BehaviorSubject<unknown[]>(shows as unknown[]);
|
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$'],
|
list$: shows$.asObservable() as unknown as ShowDataService['list$'],
|
||||||
});
|
};
|
||||||
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
showDataServiceSpy.read$.mockReturnValue(of(shows[0]));
|
||||||
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
showDataServiceSpy.listPublicSince$.mockReturnValue(of([shows[1]]));
|
||||||
showDataServiceSpy.update.and.resolveTo();
|
showDataServiceSpy.update.mockResolvedValue(undefined);
|
||||||
showDataServiceSpy.add.and.resolveTo('new-show-id');
|
showDataServiceSpy.add.mockResolvedValue('new-show-id');
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -63,12 +74,12 @@ describe('ShowService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate public listing to the data service', async () => {
|
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);
|
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate reads to the data service', async () => {
|
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');
|
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +90,7 @@ describe('ShowService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when creating a show without showType', async () => {
|
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();
|
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -87,7 +98,7 @@ describe('ShowService', () => {
|
|||||||
it('should return null when no user is available for show creation', async () => {
|
it('should return null when no user is available for show creation', async () => {
|
||||||
user$.next(null);
|
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();
|
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {ShowComponent} from './show.component';
|
import {ShowComponent} from './show.component';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {ShowService} from '../services/show.service';
|
import {ShowService} from '../services/show.service';
|
||||||
@@ -13,23 +14,39 @@ import {GuestShowService} from '../../guest/guest-show.service';
|
|||||||
describe('ShowComponent', () => {
|
describe('ShowComponent', () => {
|
||||||
let component: ShowComponent;
|
let component: ShowComponent;
|
||||||
let fixture: ComponentFixture<ShowComponent>;
|
let fixture: ComponentFixture<ShowComponent>;
|
||||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
let showServiceSpy: {
|
||||||
let showSongServiceSpy: jasmine.SpyObj<ShowSongService>;
|
update$: ReturnType<typeof vi.fn>;
|
||||||
let dialogSpy: jasmine.SpyObj<MatDialog>;
|
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 user$: BehaviorSubject<unknown>;
|
||||||
let userId$: BehaviorSubject<string | null>;
|
let userId$: BehaviorSubject<string | null>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['update$', 'read$']);
|
showServiceSpy = {
|
||||||
showSongServiceSpy = jasmine.createSpyObj<ShowSongService>('ShowSongService', ['list$', 'list']);
|
update$: vi.fn(),
|
||||||
dialogSpy = jasmine.createSpyObj<MatDialog>('MatDialog', ['open']);
|
read$: vi.fn(),
|
||||||
|
};
|
||||||
|
showSongServiceSpy = {
|
||||||
|
list$: vi.fn(),
|
||||||
|
list: vi.fn(),
|
||||||
|
};
|
||||||
|
dialogSpy = {
|
||||||
|
open: vi.fn(),
|
||||||
|
};
|
||||||
user$ = new BehaviorSubject<unknown>({id: 'user-1', role: ['leader']});
|
user$ = new BehaviorSubject<unknown>({id: 'user-1', role: ['leader']});
|
||||||
userId$ = new BehaviorSubject<string | null>('user-1');
|
userId$ = new BehaviorSubject<string | null>('user-1');
|
||||||
|
|
||||||
showServiceSpy.read$.and.returnValue(of(null));
|
showServiceSpy.read$.mockReturnValue(of(null));
|
||||||
showServiceSpy.update$.and.resolveTo();
|
showServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
showSongServiceSpy.list$.and.returnValue(of([]));
|
showSongServiceSpy.list$.mockReturnValue(of([]));
|
||||||
showSongServiceSpy.list.and.resolveTo([]);
|
showSongServiceSpy.list.mockResolvedValue([]);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ShowComponent],
|
imports: [ShowComponent],
|
||||||
@@ -38,8 +55,8 @@ describe('ShowComponent', () => {
|
|||||||
{provide: ShowService, useValue: showServiceSpy},
|
{provide: ShowService, useValue: showServiceSpy},
|
||||||
{provide: SongService, useValue: {list$: () => of([])}},
|
{provide: SongService, useValue: {list$: () => of([])}},
|
||||||
{provide: ShowSongService, useValue: showSongServiceSpy},
|
{provide: ShowSongService, useValue: showSongServiceSpy},
|
||||||
{provide: DocxService, useValue: {create: jasmine.createSpy('create').and.resolveTo()}},
|
{provide: DocxService, useValue: {create: vi.fn().mockResolvedValue(undefined)}},
|
||||||
{provide: Router, useValue: {navigateByUrl: jasmine.createSpy('navigateByUrl')}},
|
{provide: Router, useValue: {navigateByUrl: vi.fn()}},
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@@ -49,7 +66,7 @@ describe('ShowComponent', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{provide: MatDialog, useValue: dialogSpy},
|
{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();
|
}).compileComponents();
|
||||||
|
|
||||||
@@ -74,7 +91,7 @@ describe('ShowComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set pending for public shows with reportable CCLI songs', async () => {
|
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);
|
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 () => {
|
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);
|
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-3', songId: 'song-2', title: 'Beta', legalOwner: 'other', legalOwnerId: '456'},
|
||||||
{id: 'show-song-4', songId: 'song-3', title: 'Gamma', legalOwner: 'CCLI', legalOwnerId: '789'},
|
{id: 'show-song-4', songId: 'song-3', title: 'Gamma', legalOwner: 'CCLI', legalOwnerId: '789'},
|
||||||
] as never;
|
] 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);
|
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',
|
width: '640px',
|
||||||
data: {
|
data: {
|
||||||
songs: [
|
songs: [
|
||||||
|
|||||||
@@ -5,30 +5,29 @@ import {FileDataService} from './file-data.service';
|
|||||||
|
|
||||||
describe('FileDataService', () => {
|
describe('FileDataService', () => {
|
||||||
let service: FileDataService;
|
let service: FileDataService;
|
||||||
let filesCollectionValueChangesSpy: jasmine.Spy;
|
let filesCollectionValueChangesSpy: ReturnType<typeof vi.fn<() => unknown>>;
|
||||||
let filesCollectionAddSpy: jasmine.Spy;
|
let filesCollectionAddSpy: ReturnType<typeof vi.fn<() => Promise<{id: string}>>>;
|
||||||
let songDocCollectionSpy: jasmine.Spy;
|
let songDocCollectionSpy: ReturnType<typeof vi.fn<() => {add: typeof filesCollectionAddSpy; valueChanges: typeof filesCollectionValueChangesSpy}>>;
|
||||||
let songDocSpy: jasmine.Spy;
|
let songDocSpy: ReturnType<typeof vi.fn<(path: string) => unknown>>;
|
||||||
let fileDeleteSpy: jasmine.Spy;
|
let fileDeleteSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {doc: typeof songDocSpy};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
filesCollectionValueChangesSpy = jasmine.createSpy('valueChanges').and.returnValue(of([{id: 'file-1', name: 'plan.pdf'}]));
|
filesCollectionValueChangesSpy = vi.fn().mockReturnValue(of([{id: 'file-1', name: 'plan.pdf'}]));
|
||||||
filesCollectionAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'file-2'});
|
filesCollectionAddSpy = vi.fn().mockResolvedValue({id: 'file-2'});
|
||||||
songDocCollectionSpy = jasmine.createSpy('collection').and.returnValue({
|
songDocCollectionSpy = vi.fn().mockReturnValue({
|
||||||
add: filesCollectionAddSpy,
|
add: filesCollectionAddSpy,
|
||||||
valueChanges: filesCollectionValueChangesSpy,
|
valueChanges: filesCollectionValueChangesSpy,
|
||||||
});
|
});
|
||||||
songDocSpy = jasmine.createSpy('songDoc').and.callFake((path: string) => {
|
songDocSpy = vi.fn().mockImplementation((path: string) => {
|
||||||
if (path.includes('/files/')) {
|
if (path.includes('/files/')) {
|
||||||
return {delete: fileDeleteSpy};
|
return {delete: fileDeleteSpy};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {collection: songDocCollectionSpy};
|
return {collection: songDocCollectionSpy};
|
||||||
});
|
});
|
||||||
fileDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
fileDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
dbServiceSpy = {doc: songDocSpy};
|
||||||
dbServiceSpy.doc.and.callFake(songDocSpy);
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
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 () => {
|
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()};
|
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(songDocSpy).toHaveBeenCalledWith('songs/song-1');
|
||||||
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||||
|
|||||||
@@ -5,18 +5,16 @@ import {FileService} from './file.service';
|
|||||||
|
|
||||||
describe('FileService', () => {
|
describe('FileService', () => {
|
||||||
let service: FileService;
|
let service: FileService;
|
||||||
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
|
let fileDataServiceSpy: {delete: ReturnType<typeof vi.fn>};
|
||||||
type FileServiceInternals = FileService & {
|
|
||||||
resolveDownloadUrl: (path: string) => Promise<string>;
|
|
||||||
deleteFromStorage: (path: string) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['delete']);
|
fileDataServiceSpy = {
|
||||||
fileDataServiceSpy.delete.and.resolveTo();
|
delete: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
|
FileService,
|
||||||
{provide: Storage, useValue: {app: 'test-storage'}},
|
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||||
{provide: FileDataService, useValue: fileDataServiceSpy},
|
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||||
],
|
],
|
||||||
@@ -25,20 +23,26 @@ describe('FileService', () => {
|
|||||||
service = TestBed.inject(FileService);
|
service = TestBed.inject(FileService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve download urls via AngularFire storage helpers', async () => {
|
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');
|
expect(resolveSpy).toHaveBeenCalledWith('songs/song-1/file.pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete the file from storage and metadata from firestore', async () => {
|
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');
|
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', () => {
|
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('E', ['F'])).toBe(true);
|
||||||
void expect(hasAvailableKeyForRoot('H', ['C'])).toBe(true);
|
void expect(hasAvailableKeyForRoot('H', ['C'])).toBe(true);
|
||||||
void expect(hasAvailableKeyForRoot('D', ['F', 'a'])).toBe(false);
|
void expect(hasAvailableKeyForRoot('D', ['F', 'a'])).toBe(false);
|
||||||
|
|||||||
@@ -7,30 +7,36 @@ import {SongDataService} from './song-data.service';
|
|||||||
describe('SongDataService', () => {
|
describe('SongDataService', () => {
|
||||||
let service: SongDataService;
|
let service: SongDataService;
|
||||||
let songs$: Subject<Array<{id: string; title: string}>>;
|
let songs$: Subject<Array<{id: string; title: string}>>;
|
||||||
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
let docUpdateSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||||
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
let docDeleteSpy: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||||
let docSpy: jasmine.Spy;
|
let docSpy: ReturnType<typeof vi.fn<(path: string) => {update: typeof docUpdateSpy; delete: typeof docDeleteSpy}>>;
|
||||||
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
let colAddSpy: ReturnType<typeof vi.fn<() => Promise<{id: string}>>>;
|
||||||
let colSpy: jasmine.Spy;
|
let colSpy: ReturnType<typeof vi.fn<(path: string) => {add: typeof colAddSpy}>>;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {
|
||||||
|
col$: ReturnType<typeof vi.fn>;
|
||||||
|
doc$: ReturnType<typeof vi.fn>;
|
||||||
|
doc: typeof docSpy;
|
||||||
|
col: typeof colSpy;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
songs$ = new Subject<Array<{id: string; title: string}>>();
|
songs$ = new Subject<Array<{id: string; title: string}>>();
|
||||||
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
docUpdateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
docDeleteSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
docSpy = jasmine.createSpy('doc').and.callFake(() => ({
|
docSpy = vi.fn().mockImplementation(() => ({
|
||||||
update: docUpdateSpy,
|
update: docUpdateSpy,
|
||||||
delete: docDeleteSpy,
|
delete: docDeleteSpy,
|
||||||
}));
|
}));
|
||||||
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'song-3'});
|
colAddSpy = vi.fn().mockResolvedValue({id: 'song-3'});
|
||||||
colSpy = jasmine.createSpy('col').and.returnValue({
|
colSpy = vi.fn().mockReturnValue({
|
||||||
add: colAddSpy,
|
add: colAddSpy,
|
||||||
});
|
});
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
dbServiceSpy = {
|
||||||
dbServiceSpy.col$.and.returnValue(songs$.asObservable());
|
col$: vi.fn().mockReturnValue(songs$.asObservable()),
|
||||||
dbServiceSpy.doc$.and.returnValue(songs$.asObservable() as never);
|
doc$: vi.fn().mockReturnValue(songs$.asObservable() as never),
|
||||||
dbServiceSpy.doc.and.callFake(docSpy);
|
doc: docSpy,
|
||||||
dbServiceSpy.col.and.callFake(colSpy);
|
col: colSpy,
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
@@ -83,15 +89,15 @@ describe('SongDataService', () => {
|
|||||||
await service.update$('song-8', {title: 'Updated'});
|
await service.update$('song-8', {title: 'Updated'});
|
||||||
|
|
||||||
expect(docSpy).toHaveBeenCalledWith('songs/song-8');
|
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'});
|
expect(updatePayload).toEqual({title: 'Updated'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a song to the songs collection and return the new id', async () => {
|
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');
|
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'});
|
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', () => {
|
describe('SongListResolver', () => {
|
||||||
let resolver: SongListResolver;
|
let resolver: SongListResolver;
|
||||||
let songServiceSpy: jasmine.SpyObj<SongService>;
|
let songServiceSpy: {listLoaded$: ReturnType<typeof vi.fn>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['listLoaded$']);
|
songServiceSpy = {
|
||||||
songServiceSpy.listLoaded$.and.returnValue(of([{id: 'song-1', title: 'Amazing Grace'}]) as never);
|
listLoaded$: vi.fn().mockReturnValue(of([{id: 'song-1', title: 'Amazing Grace'}]) as never),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: SongService, useValue: songServiceSpy}],
|
providers: [{provide: SongService, useValue: songServiceSpy}],
|
||||||
@@ -23,7 +24,7 @@ describe('SongListResolver', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the first emitted song list from the service', async () => {
|
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();
|
expect(songServiceSpy.listLoaded$).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import {Timestamp} from '@angular/fire/firestore';
|
|||||||
|
|
||||||
describe('SongService', () => {
|
describe('SongService', () => {
|
||||||
let service: SongService;
|
let service: SongService;
|
||||||
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
let songDataServiceSpy: {
|
||||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
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 = {
|
const song = {
|
||||||
id: 'song-1',
|
id: 'song-1',
|
||||||
title: 'Amazing Grace',
|
title: 'Amazing Grace',
|
||||||
@@ -16,16 +22,22 @@ describe('SongService', () => {
|
|||||||
} as never;
|
} as never;
|
||||||
|
|
||||||
beforeEach(async () => {
|
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]),
|
list$: of([song]),
|
||||||
});
|
};
|
||||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['currentUser']);
|
userServiceSpy = {
|
||||||
|
currentUser: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
songDataServiceSpy.read$.and.returnValue(of(song));
|
songDataServiceSpy.read$.mockReturnValue(of(song));
|
||||||
songDataServiceSpy.update$.and.resolveTo();
|
songDataServiceSpy.update$.mockResolvedValue(undefined);
|
||||||
songDataServiceSpy.add.and.resolveTo('song-2');
|
songDataServiceSpy.add.mockResolvedValue('song-2');
|
||||||
songDataServiceSpy.delete.and.resolveTo();
|
songDataServiceSpy.delete.mockResolvedValue(undefined);
|
||||||
userServiceSpy.currentUser.and.resolveTo({name: 'Benjamin'} as never);
|
userServiceSpy.currentUser.mockResolvedValue({name: 'Benjamin'} as never);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -37,33 +49,37 @@ describe('SongService', () => {
|
|||||||
service = TestBed.inject(SongService);
|
service = TestBed.inject(SongService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list songs from the data service', async () => {
|
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 () => {
|
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');
|
expect(songDataServiceSpy.read$).toHaveBeenCalledWith('song-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append an edit with the current user when updating a song', async () => {
|
it('should append an edit with the current user when updating a song', async () => {
|
||||||
const timestamp = {seconds: 1} as never;
|
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'});
|
await service.update$('song-1', {title: 'Updated'});
|
||||||
|
|
||||||
expect(songDataServiceSpy.update$).toHaveBeenCalled();
|
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.title).toBe('Updated');
|
||||||
expect(payload.edits).toEqual([{username: 'Benjamin', timestamp}]);
|
expect(payload.edits).toEqual([{username: 'Benjamin', timestamp}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update when the song does not exist', async () => {
|
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'});
|
await service.update$('missing-song', {title: 'Updated'});
|
||||||
|
|
||||||
@@ -71,7 +87,7 @@ describe('SongService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not update when no current user is available', async () => {
|
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'});
|
await service.update$('song-1', {title: 'Updated'});
|
||||||
|
|
||||||
@@ -79,7 +95,7 @@ describe('SongService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create new songs with the expected defaults', async () => {
|
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({
|
expect(songDataServiceSpy.add).toHaveBeenCalledWith({
|
||||||
number: 42,
|
number: 42,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {ChordAddDescriptor} from './chord';
|
|||||||
|
|
||||||
describe('TextRenderingService', () => {
|
describe('TextRenderingService', () => {
|
||||||
const descriptor = (raw: string, partial: Partial<ChordAddDescriptor>) =>
|
const descriptor = (raw: string, partial: Partial<ChordAddDescriptor>) =>
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
raw,
|
raw,
|
||||||
quality: null,
|
quality: null,
|
||||||
extensions: [],
|
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].text).toBe('c c# db c7 cmaj7 c/e');
|
||||||
|
|
||||||
void expect(sections[2].lines[0].chords).toEqual([
|
void expect(sections[2].lines[0].chords).toEqual([
|
||||||
jasmine.objectContaining({chord: 'c', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
expect.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}),
|
expect.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}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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: 3, position: 22, add: null, slashChord: 'e', addDescriptor: null}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,9 +228,9 @@ Text`;
|
|||||||
const sections = service.parse(text, null);
|
const sections = service.parse(text, null);
|
||||||
|
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
void expect(sections[0].lines[0].chords).toEqual([
|
||||||
jasmine.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
expect.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}),
|
expect.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: '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].type).toBe(LineType.chord);
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
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']})}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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: 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].type).toBe(LineType.chord);
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
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'})}),
|
expect.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'})}),
|
expect.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'})}),
|
expect.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: '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);
|
const sections = service.parse(text, null);
|
||||||
|
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
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']})}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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']})}),
|
expect.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: '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);
|
const sections = service.parse(text, null);
|
||||||
|
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
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'})}),
|
expect.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}),
|
expect.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: '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].text).toBe('Cmaj7(add9)');
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
void expect(sections[0].lines[0].chords).toEqual([
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
chord: 'C',
|
chord: 'C',
|
||||||
length: 11,
|
length: 11,
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -362,13 +362,13 @@ Text`;
|
|||||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
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].text).toBe('C F G e C (F G)');
|
||||||
void expect(sections[0].lines[0].chords).toEqual([
|
void expect(sections[0].lines[0].chords).toEqual([
|
||||||
jasmine.objectContaining({chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null}),
|
expect.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}),
|
expect.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}),
|
expect.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}),
|
expect.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}),
|
expect.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: '('}),
|
expect.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: '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', () => {
|
it('should call the transpose service when a transpose mode is provided', () => {
|
||||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
const transposeService = TestBed.inject(TransposeService);
|
const transposeService = TestBed.inject(TransposeService);
|
||||||
const transposeSpy = spyOn(transposeService, 'transpose').and.callThrough();
|
const transposeSpy = vi.spyOn(transposeService, 'transpose');
|
||||||
const renderSpy = spyOn(transposeService, 'renderChords').and.callThrough();
|
const renderSpy = vi.spyOn(transposeService, 'renderChords');
|
||||||
const text = `Strophe
|
const text = `Strophe
|
||||||
C D E
|
C D E
|
||||||
Text`;
|
Text`;
|
||||||
@@ -404,8 +404,8 @@ Text`;
|
|||||||
it('should use renderChords when no transpose mode is provided', () => {
|
it('should use renderChords when no transpose mode is provided', () => {
|
||||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
const transposeService = TestBed.inject(TransposeService);
|
const transposeService = TestBed.inject(TransposeService);
|
||||||
const transposeSpy = spyOn(transposeService, 'transpose').and.callThrough();
|
const transposeSpy = vi.spyOn(transposeService, 'transpose');
|
||||||
const renderSpy = spyOn(transposeService, 'renderChords').and.callThrough();
|
const renderSpy = vi.spyOn(transposeService, 'renderChords');
|
||||||
const text = `Strophe
|
const text = `Strophe
|
||||||
C D E
|
C D E
|
||||||
Text`;
|
Text`;
|
||||||
@@ -434,9 +434,9 @@ Am Dm7 cdur C
|
|||||||
Text`;
|
Text`;
|
||||||
|
|
||||||
void expect(service.validateChordNotation(text)).toEqual([
|
void expect(service.validateChordNotation(text)).toEqual([
|
||||||
jasmine.objectContaining({lineNumber: 2, token: 'Am', suggestion: 'a', reason: 'minor_format'}),
|
expect.objectContaining({lineNumber: 2, token: 'Am', suggestion: 'a', reason: 'minor_format'}),
|
||||||
jasmine.objectContaining({lineNumber: 2, token: 'Dm7', suggestion: 'd7', reason: 'minor_format'}),
|
expect.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: 'cdur', suggestion: 'C', reason: 'major_format'}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -447,8 +447,8 @@ Am/C# Dm7/F#
|
|||||||
Text`;
|
Text`;
|
||||||
|
|
||||||
void expect(service.validateChordNotation(text)).toEqual([
|
void expect(service.validateChordNotation(text)).toEqual([
|
||||||
jasmine.objectContaining({lineNumber: 2, token: 'Am/C#', suggestion: 'a/C#', reason: 'minor_format'}),
|
expect.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: '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].type).toBe(LineType.chord);
|
||||||
void expect(sections[0].lines[0].text).toBe('C Es G');
|
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', () => {
|
it('should flag unknown tokens on mostly chord lines', () => {
|
||||||
@@ -471,7 +471,7 @@ Text`;
|
|||||||
C Foo G a
|
C Foo G a
|
||||||
Text`;
|
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', () => {
|
it('should reject tabs on chord lines', () => {
|
||||||
@@ -480,7 +480,7 @@ Text`;
|
|||||||
|
|
||||||
void expect(service.validateChordNotation(text)).toEqual(
|
void expect(service.validateChordNotation(text)).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
lineNumber: 2,
|
lineNumber: 2,
|
||||||
token: '\t',
|
token: '\t',
|
||||||
reason: 'tab_character',
|
reason: 'tab_character',
|
||||||
|
|||||||
@@ -80,6 +80,6 @@ describe('TransposeService', () => {
|
|||||||
const rendered = service.renderChords(line);
|
const rendered = service.renderChords(line);
|
||||||
|
|
||||||
void expect(rendered.text.length).toBe(121);
|
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', () => {
|
describe('UploadService', () => {
|
||||||
let service: UploadService;
|
let service: UploadService;
|
||||||
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
|
let fileDataServiceSpy: {set: ReturnType<typeof vi.fn>};
|
||||||
type UploadTaskLike = {
|
type UploadTaskLike = {
|
||||||
on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => void;
|
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 () => {
|
beforeEach(async () => {
|
||||||
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['set']);
|
fileDataServiceSpy = {
|
||||||
fileDataServiceSpy.set.and.resolveTo('file-1');
|
set: vi.fn().mockResolvedValue('file-1'),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
|
UploadService,
|
||||||
{provide: Storage, useValue: {app: 'test-storage'}},
|
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||||
{provide: FileDataService, useValue: fileDataServiceSpy},
|
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||||
],
|
],
|
||||||
@@ -28,6 +27,10 @@ describe('UploadService', () => {
|
|||||||
service = TestBed.inject(UploadService);
|
service = TestBed.inject(UploadService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -39,7 +42,8 @@ describe('UploadService', () => {
|
|||||||
success();
|
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'}));
|
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
|
||||||
|
|
||||||
service.pushUpload('song-1', upload);
|
service.pushUpload('song-1', upload);
|
||||||
@@ -50,10 +54,10 @@ describe('UploadService', () => {
|
|||||||
expect(upload.path).toBe('/attachments/song-1');
|
expect(upload.path).toBe('/attachments/song-1');
|
||||||
expect(fileDataServiceSpy.set).toHaveBeenCalledWith(
|
expect(fileDataServiceSpy.set).toHaveBeenCalledWith(
|
||||||
'song-1',
|
'song-1',
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'test.pdf',
|
name: 'test.pdf',
|
||||||
path: '/attachments/song-1',
|
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', () => {
|
it('should allow navigation when there is no nested editSongComponent', () => {
|
||||||
const result = guard.canDeactivate({editSongComponent: null} as never, {} as never, {} as never, {} as never);
|
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 () => {
|
it('should delegate to askForSave on the nested editSongComponent', async () => {
|
||||||
const nextState = {url: '/songs'} as never;
|
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);
|
const result = await guard.canDeactivate({editSongComponent: {askForSave}} as never, {} as never, {} as never, nextState);
|
||||||
|
|
||||||
expect(askForSave).toHaveBeenCalledWith(nextState);
|
expect(askForSave).toHaveBeenCalledWith(nextState);
|
||||||
expect(result).toBeTrue();
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,20 +18,27 @@ describe('SongComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
|
const songServiceSpy = {
|
||||||
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
|
read$: vi.fn().mockReturnValue(of({id: '4711', title: 'Test Song', number: '1', text: '', showType: '', flags: ''} as never)),
|
||||||
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
};
|
||||||
|
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}}),
|
user$: of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'onlyFirst', songUsage: {'4711': 2}}),
|
||||||
userId$: of('user-1'),
|
userId$: of('user-1'),
|
||||||
});
|
loggedIn$: vi.fn().mockReturnValue(of(true)),
|
||||||
const showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['list$', 'update$']);
|
getUserbyId$: vi.fn().mockReturnValue(of({name: 'Benjamin'})),
|
||||||
const showSongServiceSpy = jasmine.createSpyObj<ShowSongService>('ShowSongService', ['new$']);
|
};
|
||||||
|
const showServiceSpy = {
|
||||||
songServiceSpy.read$.and.returnValue(of({id: '4711', title: 'Test Song', number: '1', text: '', showType: '', flags: ''} as never));
|
list$: vi.fn().mockReturnValue(of([])),
|
||||||
fileDataServiceSpy.read$.and.returnValue(of([]));
|
update$: vi.fn(),
|
||||||
showServiceSpy.list$.and.returnValue(of([]));
|
};
|
||||||
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
|
const showSongServiceSpy = {
|
||||||
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
|
new$: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SongComponent],
|
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 {TestBed} from '@angular/core/testing';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {DbService} from './db.service';
|
import {DbService} from './db.service';
|
||||||
import {ConfigService} from './config.service';
|
import {ConfigService} from './config.service';
|
||||||
|
|
||||||
describe('ConfigService', () => {
|
describe('ConfigService', () => {
|
||||||
let service: ConfigService;
|
let service: ConfigService;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {doc$: ReturnType<typeof vi.fn>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$']);
|
dbServiceSpy = {doc$: vi.fn()};
|
||||||
dbServiceSpy.doc$.and.returnValue(of({copyright: 'CCLI'}) as never);
|
dbServiceSpy.doc$.mockReturnValue(of({copyright: 'CCLI'}) as never);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
@@ -28,10 +29,10 @@ describe('ConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the shared config stream via get$', async () => {
|
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 () => {
|
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 {TestBed} from '@angular/core/testing';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {DbService} from './db.service';
|
import {DbService} from './db.service';
|
||||||
import {GlobalSettingsService} from './global-settings.service';
|
import {GlobalSettingsService} from './global-settings.service';
|
||||||
|
|
||||||
describe('GlobalSettingsService', () => {
|
describe('GlobalSettingsService', () => {
|
||||||
let service: GlobalSettingsService;
|
let service: GlobalSettingsService;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {doc$: ReturnType<typeof vi.fn>; doc: ReturnType<typeof vi.fn>};
|
||||||
let updateSpy: jasmine.Spy;
|
let updateSpy: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
updateSpy = jasmine.createSpy('update').and.resolveTo();
|
updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$', 'doc']);
|
dbServiceSpy = {doc$: vi.fn(), doc: vi.fn()};
|
||||||
dbServiceSpy.doc$.and.returnValue(of({churchName: 'ICF'}) as never);
|
dbServiceSpy.doc$.mockReturnValue(of({churchName: 'ICF'}) as never);
|
||||||
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
@@ -31,7 +32,7 @@ describe('GlobalSettingsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the shared settings stream via the getter', async () => {
|
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 () => {
|
it('should update the static global settings document', async () => {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {UserService} from '../user.service';
|
import {UserService} from '../user.service';
|
||||||
import {UserNameComponent} from './user-name.component';
|
import {UserNameComponent} from './user-name.component';
|
||||||
|
|
||||||
describe('UserNameComponent', () => {
|
describe('UserNameComponent', () => {
|
||||||
let component: UserNameComponent;
|
let component: UserNameComponent;
|
||||||
let fixture: ComponentFixture<UserNameComponent>;
|
let fixture: ComponentFixture<UserNameComponent>;
|
||||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
let userServiceSpy: {getUserbyId$: ReturnType<typeof vi.fn>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
|
userServiceSpy = {getUserbyId$: vi.fn()};
|
||||||
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
|
userServiceSpy.getUserbyId$.mockReturnValue(of({name: 'Benjamin'} as never));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserNameComponent],
|
imports: [UserNameComponent],
|
||||||
@@ -32,14 +33,14 @@ describe('UserNameComponent', () => {
|
|||||||
component.userId = 'user-1';
|
component.userId = 'user-1';
|
||||||
|
|
||||||
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('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 () => {
|
it('should map missing users to null names', async () => {
|
||||||
userServiceSpy.getUserbyId$.and.returnValue(of(null));
|
userServiceSpy.getUserbyId$.mockReturnValue(of(null));
|
||||||
|
|
||||||
component.userId = 'missing-user';
|
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 {Router} from '@angular/router';
|
||||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
import {Auth} from '@angular/fire/auth';
|
import {Auth} from '@angular/fire/auth';
|
||||||
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {DbService} from '../db.service';
|
import {DbService} from '../db.service';
|
||||||
import {UserSessionService} from './user-session.service';
|
import {UserSessionService} from './user-session.service';
|
||||||
|
|
||||||
describe('UserSessionService', () => {
|
describe('UserSessionService', () => {
|
||||||
let service: UserSessionService;
|
let service: UserSessionService;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {col$: ReturnType<typeof vi.fn>; doc$: ReturnType<typeof vi.fn>; doc: ReturnType<typeof vi.fn>};
|
||||||
let routerSpy: jasmine.SpyObj<Router>;
|
let routerSpy: {navigateByUrl: ReturnType<typeof vi.fn>};
|
||||||
let authStateSubject: BehaviorSubject<unknown>;
|
let authStateSubject: BehaviorSubject<unknown>;
|
||||||
let createAuthStateSpy: jasmine.Spy;
|
let createAuthStateSpy: ReturnType<typeof vi.fn>;
|
||||||
let runInFirebaseContextSpy: jasmine.Spy;
|
let runInFirebaseContextSpy: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authStateSubject = new BehaviorSubject<unknown>(null);
|
authStateSubject = new BehaviorSubject<unknown>(null);
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc']);
|
dbServiceSpy = {col$: vi.fn(), doc$: vi.fn(), doc: vi.fn()};
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
routerSpy = {navigateByUrl: vi.fn()};
|
||||||
dbServiceSpy.col$.and.returnValue(of([{id: 'user-1'}]) as never);
|
dbServiceSpy.col$.mockReturnValue(of([{id: 'user-1'}]) as never);
|
||||||
dbServiceSpy.doc$.and.callFake((path: string) => {
|
dbServiceSpy.doc$.mockImplementation((path: string) => {
|
||||||
if (path === 'users/user-1') {
|
if (path === 'users/user-1') {
|
||||||
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters', songUsage: {}}) as never;
|
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;
|
return of(null) as never;
|
||||||
});
|
});
|
||||||
dbServiceSpy.doc.and.returnValue({
|
dbServiceSpy.doc.mockReturnValue({
|
||||||
update: jasmine.createSpy('update').and.resolveTo(),
|
update: vi.fn().mockResolvedValue(undefined),
|
||||||
set: jasmine.createSpy('set').and.resolveTo(),
|
set: vi.fn().mockResolvedValue(undefined),
|
||||||
} as never);
|
} as never);
|
||||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
routerSpy.navigateByUrl.mockResolvedValue(true);
|
||||||
|
|
||||||
createAuthStateSpy = spyOn(UserSessionService.prototype as UserSessionService & {createAuthState$: () => unknown}, 'createAuthState$').and.returnValue(
|
createAuthStateSpy = vi
|
||||||
authStateSubject.asObservable() as never
|
.spyOn(UserSessionService.prototype as unknown as {createAuthState$: () => unknown}, 'createAuthState$')
|
||||||
);
|
.mockReturnValue(authStateSubject.asObservable() as never) as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -47,7 +48,13 @@ describe('UserSessionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service = TestBed.inject(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', () => {
|
it('should be created', () => {
|
||||||
@@ -58,14 +65,14 @@ describe('UserSessionService', () => {
|
|||||||
it('should derive userId$ and loggedIn$ from authState', async () => {
|
it('should derive userId$ and loggedIn$ from authState', async () => {
|
||||||
authStateSubject.next({uid: 'user-1'});
|
authStateSubject.next({uid: 'user-1'});
|
||||||
|
|
||||||
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
await expect(firstValueFrom(service.userId$)).resolves.toEqual('user-1');
|
||||||
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
await expect(firstValueFrom(service.loggedIn$())).resolves.toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the current user document from auth state', async () => {
|
it('should resolve the current user document from auth state', async () => {
|
||||||
authStateSubject.next({uid: 'user-1'});
|
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 () => {
|
it('should cache user lookups by id', async () => {
|
||||||
@@ -73,30 +80,30 @@ describe('UserSessionService', () => {
|
|||||||
const second$ = service.getUserbyId$('user-2');
|
const second$ = service.getUserbyId$('user-2');
|
||||||
|
|
||||||
expect(first$).toBe(second$);
|
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 () => {
|
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') {
|
if (path === 'users/user-1') {
|
||||||
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters'}) as never;
|
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters'}) as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
return of(null) as never;
|
return of(null) as never;
|
||||||
});
|
});
|
||||||
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
const updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||||
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
|
runInFirebaseContextSpy.mockResolvedValue({user: {uid: 'user-1'}});
|
||||||
authStateSubject.next({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(runInFirebaseContextSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(updateSpy).toHaveBeenCalledWith({songUsage: {}});
|
expect(updateSpy).toHaveBeenCalledWith({songUsage: {}});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should wait for auth state propagation before resolving login', async () => {
|
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;
|
let resolved = false;
|
||||||
const loginPromise = service.login('mail', 'secret').then(result => {
|
const loginPromise = service.login('mail', 'secret').then(result => {
|
||||||
@@ -105,15 +112,15 @@ describe('UserSessionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(resolved).toBeFalse();
|
expect(resolved).toBe(false);
|
||||||
|
|
||||||
authStateSubject.next({uid: 'user-1'});
|
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 () => {
|
it('should delegate logout and password reset to AngularFire auth APIs', async () => {
|
||||||
runInFirebaseContextSpy.and.resolveTo();
|
runInFirebaseContextSpy.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await service.logout();
|
await service.logout();
|
||||||
await service.changePassword('mail@example.com');
|
await service.changePassword('mail@example.com');
|
||||||
@@ -122,16 +129,16 @@ describe('UserSessionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new user document and navigate afterwards', async () => {
|
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') {
|
if (path === 'users/user-3') {
|
||||||
return of({id: 'user-3', name: 'New User', role: 'user', chordMode: 'onlyFirst', songUsage: {}}) as never;
|
return of({id: 'user-3', name: 'New User', role: 'user', chordMode: 'onlyFirst', songUsage: {}}) as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
return of(null) as never;
|
return of(null) as never;
|
||||||
});
|
});
|
||||||
const setSpy = jasmine.createSpy('set').and.resolveTo();
|
const setSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
dbServiceSpy.doc.and.returnValue({set: setSpy} as never);
|
dbServiceSpy.doc.mockReturnValue({set: setSpy} as never);
|
||||||
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-3'}});
|
runInFirebaseContextSpy.mockResolvedValue({user: {uid: 'user-3'}});
|
||||||
|
|
||||||
await service.createNewUser('mail@example.com', 'New User', 'secret');
|
await service.createNewUser('mail@example.com', 'New User', 'secret');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {DbService} from '../db.service';
|
import {DbService} from '../db.service';
|
||||||
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
||||||
import {ShowSongDataService} from '../../modules/shows/services/show-song-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', () => {
|
describe('UserSongUsageService', () => {
|
||||||
let service: UserSongUsageService;
|
let service: UserSongUsageService;
|
||||||
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
let dbServiceSpy: {doc: ReturnType<typeof vi.fn>};
|
||||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
let sessionSpy: {update$: ReturnType<typeof vi.fn>; user$: unknown; users$: unknown};
|
||||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
let showDataServiceSpy: {listRaw$: ReturnType<typeof vi.fn>};
|
||||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
let showSongDataServiceSpy: {list$: ReturnType<typeof vi.fn>};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
dbServiceSpy = {doc: vi.fn()};
|
||||||
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
sessionSpy = {
|
||||||
|
update$: vi.fn(),
|
||||||
user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never,
|
user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never,
|
||||||
users$: of([{id: 'user-1'}, {id: 'user-2'}]) as never,
|
users$: of([{id: 'user-1'}, {id: 'user-2'}]) as never,
|
||||||
});
|
};
|
||||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$']);
|
showDataServiceSpy = {listRaw$: vi.fn()};
|
||||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
showSongDataServiceSpy = {list$: vi.fn()};
|
||||||
|
|
||||||
sessionSpy.update$.and.resolveTo();
|
sessionSpy.update$.mockResolvedValue(undefined);
|
||||||
showDataServiceSpy.listRaw$.and.returnValue(
|
showDataServiceSpy.listRaw$.mockReturnValue(
|
||||||
of([
|
of([
|
||||||
{id: 'show-1', owner: 'user-1', archived: false},
|
{id: 'show-1', owner: 'user-1', archived: false},
|
||||||
{id: 'show-2', owner: 'user-2', archived: true},
|
{id: 'show-2', owner: 'user-2', archived: true},
|
||||||
]) as never
|
]) 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
|
(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({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -51,19 +53,19 @@ describe('UserSongUsageService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should increment and decrement song usage for the current user', async () => {
|
it('should increment and decrement song usage for the current user', async () => {
|
||||||
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
const updateSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
dbServiceSpy.doc.mockReturnValue({update: updateSpy} as never);
|
||||||
|
|
||||||
await service.incSongCount('song-1');
|
await service.incSongCount('song-1');
|
||||||
await service.decSongCount('song-2');
|
await service.decSongCount('song-2');
|
||||||
|
|
||||||
expect(dbServiceSpy.doc).toHaveBeenCalledWith('users/user-1');
|
expect(dbServiceSpy.doc).toHaveBeenCalledWith('users/user-1');
|
||||||
expect(updateSpy.calls.argsFor(0)[0]).toEqual({'songUsage.song-1': jasmine.anything()});
|
expect(updateSpy.mock.calls[0]?.[0]).toEqual({'songUsage.song-1': expect.anything()});
|
||||||
expect(updateSpy.calls.argsFor(1)[0]).toEqual({'songUsage.song-2': jasmine.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 () => {
|
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,
|
usersProcessed: 2,
|
||||||
showsProcessed: 2,
|
showsProcessed: 2,
|
||||||
showSongsProcessed: 3,
|
showSongsProcessed: 3,
|
||||||
@@ -76,6 +78,6 @@ describe('UserSongUsageService', () => {
|
|||||||
it('should reject song usage rebuilds for non-admin users', async () => {
|
it('should reject song usage rebuilds for non-admin users', async () => {
|
||||||
Object.defineProperty(sessionSpy, 'user$', {value: of({id: 'user-1', role: 'leader'})});
|
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 {TestBed} from '@angular/core/testing';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||||
import {UserService} from './user.service';
|
import {UserService} from './user.service';
|
||||||
import {UserSessionService} from './user-session.service';
|
import {UserSessionService} from './user-session.service';
|
||||||
import {UserSongUsageService} from './user-song-usage.service';
|
import {UserSongUsageService} from './user-song-usage.service';
|
||||||
import {ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
let sessionSpy: {
|
||||||
let songUsageSpy: jasmine.SpyObj<UserSongUsageService>;
|
currentUser: ReturnType<typeof vi.fn>;
|
||||||
let showSongIndexSpy: jasmine.SpyObj<ShowSongIndexService>;
|
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 () => {
|
beforeEach(async () => {
|
||||||
sessionSpy = jasmine.createSpyObj<UserSessionService>(
|
sessionSpy = {
|
||||||
'UserSessionService',
|
currentUser: vi.fn(),
|
||||||
['currentUser', 'getUserbyId', 'getUserbyId$', 'login', 'loggedIn$', 'list$', 'logout', 'update$', 'changePassword', 'createNewUser'],
|
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,
|
users$: of([{id: 'user-1'}]) as never,
|
||||||
userId$: of('user-1'),
|
userId$: of('user-1'),
|
||||||
user$: of({id: 'user-1'}) as never,
|
user$: of({id: 'user-1'}) as never,
|
||||||
}
|
};
|
||||||
);
|
songUsageSpy = {incSongCount: vi.fn(), decSongCount: vi.fn()};
|
||||||
songUsageSpy = jasmine.createSpyObj<UserSongUsageService>('UserSongUsageService', ['incSongCount', 'decSongCount', 'rebuildSongUsage']);
|
|
||||||
showSongIndexSpy = jasmine.createSpyObj<ShowSongIndexService>('ShowSongIndexService', ['rebuildShowSongIds']);
|
|
||||||
|
|
||||||
sessionSpy.currentUser.and.resolveTo({id: 'user-1'} as never);
|
sessionSpy.currentUser.mockResolvedValue({id: 'user-1'} as never);
|
||||||
sessionSpy.getUserbyId.and.resolveTo({id: 'user-2'} as never);
|
sessionSpy.getUserbyId.mockResolvedValue({id: 'user-2'} as never);
|
||||||
sessionSpy.getUserbyId$.and.returnValue(of({id: 'user-2'}) as never);
|
sessionSpy.getUserbyId$.mockReturnValue(of({id: 'user-2'}) as never);
|
||||||
sessionSpy.login.and.resolveTo('user-1');
|
sessionSpy.login.mockResolvedValue('user-1');
|
||||||
sessionSpy.loggedIn$.and.returnValue(of(true));
|
sessionSpy.loggedIn$.mockReturnValue(of(true));
|
||||||
sessionSpy.list$.and.returnValue(of([{id: 'user-1'}]) as never);
|
sessionSpy.list$.mockReturnValue(of([{id: 'user-1'}]) as never);
|
||||||
sessionSpy.logout.and.resolveTo();
|
sessionSpy.logout.mockResolvedValue(undefined);
|
||||||
sessionSpy.update$.and.resolveTo();
|
sessionSpy.update$.mockResolvedValue(undefined);
|
||||||
sessionSpy.changePassword.and.resolveTo();
|
sessionSpy.changePassword.mockResolvedValue(undefined);
|
||||||
sessionSpy.createNewUser.and.resolveTo();
|
sessionSpy.createNewUser.mockResolvedValue(undefined);
|
||||||
songUsageSpy.incSongCount.and.resolveTo();
|
songUsageSpy.incSongCount.mockResolvedValue(undefined);
|
||||||
songUsageSpy.decSongCount.and.resolveTo();
|
songUsageSpy.decSongCount.mockResolvedValue(undefined);
|
||||||
songUsageSpy.rebuildSongUsage.and.resolveTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
|
||||||
showSongIndexSpy.rebuildShowSongIds.and.resolveTo({showsProcessed: 2, showSongsProcessed: 3});
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: UserSessionService, useValue: sessionSpy},
|
{provide: UserSessionService, useValue: sessionSpy},
|
||||||
{provide: UserSongUsageService, useValue: songUsageSpy},
|
{provide: UserSongUsageService, useValue: songUsageSpy},
|
||||||
{provide: ShowSongIndexService, useValue: showSongIndexSpy},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,15 +70,15 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the session streams directly', async () => {
|
it('should expose the session streams directly', async () => {
|
||||||
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
await expect(firstValueFrom(service.userId$)).resolves.toEqual('user-1');
|
||||||
await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo({id: 'user-1'} as never);
|
await expect(firstValueFrom(service.user$)).resolves.toEqual({id: 'user-1'} as never);
|
||||||
await expectAsync(firstValueFrom(service.users$)).toBeResolvedTo([{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 () => {
|
it('should delegate session operations to UserSessionService', async () => {
|
||||||
await expectAsync(service.currentUser()).toBeResolvedTo({id: 'user-1'} as never);
|
await expect(service.currentUser()).resolves.toEqual({id: 'user-1'} as never);
|
||||||
await expectAsync(service.getUserbyId('user-2')).toBeResolvedTo({id: 'user-2'} as never);
|
await expect(service.getUserbyId('user-2')).resolves.toEqual({id: 'user-2'} as never);
|
||||||
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
|
await expect(service.login('mail', 'secret')).resolves.toEqual('user-1');
|
||||||
await service.logout();
|
await service.logout();
|
||||||
await service.update$('user-1', {name: 'Benjamin'} as never);
|
await service.update$('user-1', {name: 'Benjamin'} as never);
|
||||||
await service.changePassword('mail');
|
await service.changePassword('mail');
|
||||||
@@ -79,9 +94,9 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate user lookup and loggedIn/list streams to UserSessionService', async () => {
|
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 expect(firstValueFrom(service.getUserbyId$('user-2'))).resolves.toEqual({id: 'user-2'} as never);
|
||||||
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
await expect(firstValueFrom(service.loggedIn$())).resolves.toEqual(true);
|
||||||
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([{id: 'user-1'}] as never);
|
await expect(firstValueFrom(service.list$())).resolves.toEqual([{id: 'user-1'}] as never);
|
||||||
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
||||||
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
||||||
expect(sessionSpy.list$).toHaveBeenCalled();
|
expect(sessionSpy.list$).toHaveBeenCalled();
|
||||||
@@ -90,12 +105,8 @@ describe('UserService', () => {
|
|||||||
it('should delegate song usage operations to UserSongUsageService', async () => {
|
it('should delegate song usage operations to UserSongUsageService', async () => {
|
||||||
await service.incSongCount('song-1');
|
await service.incSongCount('song-1');
|
||||||
await service.decSongCount('song-2');
|
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.incSongCount).toHaveBeenCalledWith('song-1');
|
||||||
expect(songUsageSpy.decSongCount).toHaveBeenCalledWith('song-2');
|
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 {Injectable, inject} from '@angular/core';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {User} from './user';
|
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 {UserSessionService} from './user-session.service';
|
||||||
import {MigrationProgress, ShowSongIndexMigrationResult, ShowSongIndexService} from '../../modules/shows/services/show-song-index.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -11,7 +10,6 @@ import {MigrationProgress, ShowSongIndexMigrationResult, ShowSongIndexService} f
|
|||||||
export class UserService {
|
export class UserService {
|
||||||
private session = inject(UserSessionService);
|
private session = inject(UserSessionService);
|
||||||
private songUsage = inject(UserSongUsageService);
|
private songUsage = inject(UserSongUsageService);
|
||||||
private showSongIndex = inject(ShowSongIndexService);
|
|
||||||
|
|
||||||
public users$ = this.session.users$;
|
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 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 incSongCount = (songId: string): Promise<void | null> => this.songUsage.incSongCount(songId);
|
||||||
public decSongCount = (songId: string): Promise<void | null> => this.songUsage.decSongCount(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);
|
fixture = TestBed.createComponent(PageFrameComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
fixture.componentRef.setInput('title', 'Test');
|
||||||
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,12 +23,12 @@ describe('SidebarComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle and close the sidebar', () => {
|
it('should toggle and close the sidebar', () => {
|
||||||
expect(component.collapsed).toBeTrue();
|
expect(component.collapsed).toBe(true);
|
||||||
|
|
||||||
component.toggle();
|
component.toggle();
|
||||||
expect(component.collapsed).toBeFalse();
|
expect(component.collapsed).toBe(false);
|
||||||
|
|
||||||
component.close();
|
component.close();
|
||||||
expect(component.collapsed).toBeTrue();
|
expect(component.collapsed).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
|
import {vi} from 'vitest';
|
||||||
import {UserService} from '../../services/user/user.service';
|
import {UserService} from '../../services/user/user.service';
|
||||||
import {RoleGuard} from './role.guard';
|
import {RoleGuard} from './role.guard';
|
||||||
|
|
||||||
describe('RoleGuard', () => {
|
describe('RoleGuard', () => {
|
||||||
let guard: RoleGuard;
|
let guard: RoleGuard;
|
||||||
let routerSpy: jasmine.SpyObj<Router>;
|
let createUrlTreeSpy: ReturnType<typeof vi.fn>;
|
||||||
|
let routerSpy: Pick<Router, 'createUrlTree'>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn().mockImplementation(commands => ({commands}) as never);
|
||||||
routerSpy.createUrlTree.and.callFake(commands => ({commands}) as never);
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -31,12 +33,13 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should deny access when there is no current user', async () => {
|
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 () => {
|
it('should allow admins regardless of requiredRoles', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn();
|
||||||
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy},
|
||||||
@@ -45,12 +48,13 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(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 () => {
|
it('should allow users with a matching required role', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn();
|
||||||
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy},
|
||||||
@@ -59,13 +63,13 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(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 () => {
|
it('should redirect users without the required role to their role default route', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn().mockReturnValue({redirect: ['presentation']} as never);
|
||||||
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy},
|
||||||
@@ -75,14 +79,14 @@ describe('RoleGuard', () => {
|
|||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never));
|
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);
|
expect(result).toEqual({redirect: ['presentation']} as never);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect members to shows instead of new-user', async () => {
|
it('should redirect members to shows instead of new-user', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn().mockReturnValue({redirect: ['shows']} as never);
|
||||||
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy},
|
||||||
@@ -92,14 +96,14 @@ describe('RoleGuard', () => {
|
|||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['user']}} as never));
|
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);
|
expect(result).toEqual({redirect: ['shows']} as never);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should choose a matching default route from all assigned roles', async () => {
|
it('should choose a matching default route from all assigned roles', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
createUrlTreeSpy = vi.fn().mockImplementation(commands => ({redirect: commands}) as never);
|
||||||
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
|
routerSpy = {createUrlTree: createUrlTreeSpy as Router['createUrlTree']};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: routerSpy},
|
{provide: Router, useValue: routerSpy},
|
||||||
@@ -109,7 +113,7 @@ describe('RoleGuard', () => {
|
|||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['presenter']}} as never));
|
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);
|
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 {initializeApp} from 'firebase/app';
|
||||||
import {getAuth} from 'firebase/auth';
|
import {getAuth} from 'firebase/auth';
|
||||||
import {initializeFirestore, persistentLocalCache, persistentMultipleTabManager} from 'firebase/firestore';
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
wgeneratorAdmin?: {
|
wgeneratorAdmin?: {
|
||||||
|
downloadSongs(): Promise<unknown>;
|
||||||
rebuildSongUsage(): Promise<unknown>;
|
rebuildSongUsage(): Promise<unknown>;
|
||||||
rebuildShowSongIds(): Promise<unknown>;
|
rebuildShowSongIds(): Promise<unknown>;
|
||||||
};
|
};
|
||||||
@@ -50,12 +51,18 @@ bootstrapApplication(AppComponent, {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
.then(appRef => {
|
.then(appRef => {
|
||||||
const userService = appRef.injector.get(UserService);
|
const adminService = appRef.injector.get(AdminService);
|
||||||
window.wgeneratorAdmin = {
|
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 () => {
|
rebuildShowSongIds: async () => {
|
||||||
console.info('[wgeneratorAdmin] rebuildShowSongIds started');
|
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`, {
|
console.info(`[wgeneratorAdmin] rebuildShowSongIds ${progress.processed}/${progress.total} shows processed`, {
|
||||||
showId: progress.showId,
|
showId: progress.showId,
|
||||||
showSongsProcessed: progress.showSongsProcessed,
|
showSongsProcessed: progress.showSongsProcessed,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import {expect, vi} from 'vitest';
|
|
||||||
|
|
||||||
import 'zone.js/testing';
|
import 'zone.js/testing';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
||||||
@@ -25,20 +23,6 @@ type DocumentStub = {
|
|||||||
collection: () => CollectionStub;
|
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 routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||||
const queryParams$ = 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);
|
return originalConfigureTestingModule(mergedModuleDef);
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule = configureTestingModule;
|
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