import {expect, vi} from 'vitest'; import 'zone.js/testing'; import {TestBed} from '@angular/core/testing'; import {provideNoopAnimations} from '@angular/platform-browser/animations'; import {ActivatedRoute, provideRouter} from '@angular/router'; import {BehaviorSubject, Observable, of} from 'rxjs'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {provideNativeDateAdapter} from '@angular/material/core'; import {Auth} from '@angular/fire/auth'; import {Firestore} from '@angular/fire/firestore'; import {Storage} from '@angular/fire/storage'; import {DbService} from './app/services/db.service'; type TestingModuleDefinition = Parameters[0]; type TestingProviderList = NonNullable['providers']>; type CollectionStub = { valueChanges: () => Observable; add: () => Promise<{id: string}>; }; type DocumentStub = { set: () => Promise; update: () => Promise; delete: () => Promise; collection: () => CollectionStub; }; type MockFunction = ReturnType & { 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>({}); const queryParams$ = new BehaviorSubject>({}); const defaultTestingProviders: TestingProviderList = [ provideNoopAnimations(), provideNativeDateAdapter(), provideRouter([]), {provide: Auth, useValue: {}}, {provide: Firestore, useValue: {}}, {provide: Storage, useValue: {}}, { provide: ActivatedRoute, useValue: { snapshot: {params: {}, queryParams: {}, data: {}}, params: routeParams$.asObservable(), queryParams: queryParams$.asObservable(), data: of({}), fragment: of(null), }, }, {provide: MAT_DIALOG_DATA, useValue: {}}, {provide: MatDialogRef, useValue: {close: () => void 0}}, { provide: DbService, useValue: { col$: () => of([]), doc$: () => of(null), col: (): CollectionStub => ({ valueChanges: () => of([]), add: () => Promise.resolve({id: 'test-id'}), }), doc: (): DocumentStub => ({ set: () => Promise.resolve(), update: () => Promise.resolve(), delete: () => Promise.resolve(), collection: (): CollectionStub => ({ valueChanges: () => of([]), add: () => Promise.resolve({id: 'test-id'}), }), }), }, }, ]; const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed); const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => { const extraProviders: TestingProviderList = moduleDef?.providers ?? []; const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {}; mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders); return originalConfigureTestingModule(mergedModuleDef); }; TestBed.configureTestingModule = configureTestingModule; function decorateMock>(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(baseName: string, methodNames: string[] | Record, propertyValues?: Record): T { const result: Record = {}; 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(object: T, methodName: K): MockFunction { const spy = vi.spyOn(object as Record unknown>, methodName as PropertyKey); return decorateMock(spy as unknown as ReturnType); } function expectAsyncCompat(value: Promise) { 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, }, });