feat: init
This commit is contained in:
12
src/testing/create-retriable-stream.helper.ts
Normal file
12
src/testing/create-retriable-stream.helper.ts
Normal 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
7
src/testing/index.ts
Normal 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
14
src/testing/init-test.ts
Normal 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
48
src/testing/mock.ts
Normal 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
12
src/testing/router.ts
Normal 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
53
src/testing/spy.ts
Normal 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
90
src/testing/testbed.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user