feat: init

This commit is contained in:
2025-01-30 20:02:28 +08:00
parent da0d9855da
commit 1785df25e2
125 changed files with 8601 additions and 4725 deletions

View File

@@ -0,0 +1,12 @@
import { type Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
// Create retriable observable stream to test retry / retryWhen. Credits to:
// https://stackoverflow.com/questions/51399819/how-to-create-a-mock-observable-to-test-http-rxjs-retry-retrywhen-in-angular
export const createRetriableStream = (...resp$: any): Observable<any> => {
const fetchData: jasmine.Spy = jasmine.createSpy('fetchData');
fetchData.mockReturnValues(...resp$);
return of(null).pipe(switchMap((_) => fetchData()));
};

7
src/testing/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export { TestBed } from './testbed';
export {
createSpyObj,
mockImplementationWhenArgsEqual,
} from './spy';
export { createRetriableStream } from './create-retriable-stream.helper';
export { MockRouter, mockRouterProvider } from './router';

14
src/testing/init-test.ts Normal file
View File

@@ -0,0 +1,14 @@
import { getTestBed } from '@/testing/testbed';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{
teardown: { destroyAfterEach: false },
}
);

48
src/testing/mock.ts Normal file
View File

@@ -0,0 +1,48 @@
import type { Provider } from 'injection-js';
export function mockClass<T>(obj: new (...args: any[]) => T): any {
const keys = Object.getOwnPropertyNames(obj.prototype);
const allMethods = keys.filter((key) => {
try {
return typeof obj.prototype[key] === 'function';
} catch {
return false;
}
});
const allProperties = keys.filter((x) => !allMethods.includes(x));
const mockedClass = class T {};
for (const method of allMethods) {
(mockedClass.prototype as any)[method] = (): void => {
return;
};
}
for (const method of allProperties) {
Object.defineProperty(mockedClass.prototype, method, {
get() {
return '';
},
configurable: true,
});
}
return mockedClass;
}
export function mockProvider<T>(obj: new (...args: any[]) => T): Provider {
return {
provide: obj,
useClass: mockClass(obj),
};
}
export function mockAbstractProvider<T, M extends T>(
type: abstract new (...args: any[]) => T,
mockType: new (...args: any[]) => M
): Provider {
const mock = mockClass(mockType);
return { provide: type, useClass: mock };
}

12
src/testing/router.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { Provider } from 'injection-js';
import { AbstractRouter } from 'oidc-client-rx';
// TODO
export class MockRouter extends AbstractRouter {}
export function mockRouterProvider(): Provider {
return {
useClass: MockRouter,
provide: AbstractRouter, // This is the token that will be injected into components
};
}

53
src/testing/spy.ts Normal file
View File

@@ -0,0 +1,53 @@
import { isEqual } from 'lodash-es';
import { type MockInstance, vi } from 'vitest';
export function createSpyObj<T>(baseName: string, methods: (keyof T)[]): T {
const SpyClass = new Function(
`return class Spy${baseName} {
constructor() {}
}`
) as new () => T;
const spyObj = new SpyClass();
for (const method of methods) {
Object.defineProperty(spyObj, method, {
value: vi.fn(),
writable: true,
});
}
return spyObj;
}
export function mockImplementationWhenArgsEqual<M extends MockInstance<any>>(
mockInstance: M,
whenArgs: Parameters<M extends MockInstance<infer T> ? T : never>,
implementation: Exclude<ReturnType<M['getMockImplementation']>, undefined>
): M {
const spyImpl = mockInstance.getMockImplementation()!;
return mockInstance.mockImplementation((...args) => {
if (isEqual(args, whenArgs)) {
return implementation(...args);
}
return spyImpl?.(...args);
});
}
export function mockImplementationWhenArgs<M extends MockInstance<any>>(
mockInstance: M,
whenArgs: (
...args: Parameters<M extends MockInstance<infer T> ? T : never>
) => boolean,
implementation: Exclude<ReturnType<M['getMockImplementation']>, undefined>
): M {
const spyImpl = mockInstance.getMockImplementation()!;
return mockInstance.mockImplementation((...args) => {
if (isEqual(args, whenArgs)) {
return implementation(...args);
}
return spyImpl?.(...args);
});
}

90
src/testing/testbed.ts Normal file
View File

@@ -0,0 +1,90 @@
import {
type InjectionToken,
type Injector,
type Provider,
ReflectiveInjector,
type Type,
} from 'injection-js';
import { setCurrentInjector } from 'injection-js/lib/injector_compatibility';
export interface TestModuleMetadata {
providers?: Provider[];
imports?: ((parentInjector: Injector) => Injector)[];
}
export class TestBed {
private injector: ReflectiveInjector;
private providers: Provider[] = [];
private imports: Injector[] = [];
constructor(metadata: TestModuleMetadata = {}) {
const providers = metadata.providers ?? [];
const imports = metadata.imports ?? [];
this.injector = ReflectiveInjector.resolveAndCreate(providers);
this.imports = imports.map((importFn) => importFn(this.injector));
}
static #instance?: TestBed;
static configureTestingModule(metadata: TestModuleMetadata = {}) {
const newTestBed = new TestBed(metadata);
TestBed.#instance = newTestBed;
return newTestBed;
}
/**
* 在 TestBed 的注入上下文中运行函数
*/
static runInInjectionContext<T>(fn: () => T): T {
const injector = TestBed.#instance?.injector;
if (!injector) {
throw new Error(
'TestBed is not configured. Call configureTestingModule first.'
);
}
// 保存当前的注入器
const previousInjector = setCurrentInjector(injector);
try {
// 在注入上下文中执行函数
return fn();
} finally {
// 恢复之前的注入器
setCurrentInjector(previousInjector);
}
}
compileComponents(): Promise<any> {
return Promise.resolve();
}
static get instance(): TestBed {
if (!TestBed.#instance) {
throw new Error('TestBest.configureTestingModule should be called first');
}
return TestBed.#instance;
}
static get<T>(token: Type<T> | InjectionToken<T>): T;
static get(token: any): any;
static get(token: unknown): any {
const g = TestBed.instance;
return g.injector.get(token);
}
static inject<T>(token: Type<T> | InjectionToken<T>): T;
static inject(token: any): any;
static inject(token: unknown): any {
return TestBed.get(token as any);
}
static [Symbol.dispose]() {
TestBed.#instance = undefined;
}
}
export function getTestBed() {
return TestBed.instance;
}