fix: fix some tests

This commit is contained in:
2025-02-02 00:45:46 +08:00
parent 28da493462
commit 6a03a2bd62
93 changed files with 2671 additions and 1622 deletions

View File

@@ -4,10 +4,12 @@ import { vi } from 'vitest';
// 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 = vi.fn()('fetchData');
export const createRetriableStream = (...resp$: any[]): Observable<any> => {
const fetchData = vi.fn();
fetchData.mockReturnValues(...resp$);
for (const r of resp$) {
fetchData.mockReturnValueOnce(r);
}
return of(null).pipe(switchMap((_) => fetchData()));
};

View File

@@ -1,14 +1,11 @@
import { getTestBed } from '@/testing/testbed';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { TestBed } from '@/testing/testbed';
import { DOCUMENT } from 'oidc-client-rx/dom';
import 'reflect-metadata';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
TestBed.initTestEnvironment([
{
teardown: { destroyAfterEach: false },
}
);
provide: DOCUMENT,
useValue: document,
},
]);

View File

@@ -1,6 +1,10 @@
import type { Provider } from 'injection-js';
export function mockClass<T>(obj: new (...args: any[]) => T): any {
export function mockClass<T>(
obj: new (...args: any[]) => T
): new (
...args: any[]
) => T {
const keys = Object.getOwnPropertyNames(obj.prototype);
const allMethods = keys.filter((key) => {
try {
@@ -14,9 +18,16 @@ export function mockClass<T>(obj: new (...args: any[]) => T): any {
const mockedClass = class T {};
for (const method of allMethods) {
(mockedClass.prototype as any)[method] = (): void => {
return;
};
const mockImplementation = Reflect.getMetadata(
'mock:implementation',
obj.prototype,
method
);
(mockedClass.prototype as any)[method] =
mockImplementation ??
((): any => {
return;
});
}
for (const method of allProperties) {
@@ -28,12 +39,15 @@ export function mockClass<T>(obj: new (...args: any[]) => T): any {
});
}
return mockedClass;
return mockedClass as any;
}
export function mockProvider<T>(obj: new (...args: any[]) => T): Provider {
export function mockProvider<T>(
obj: new (...args: any[]) => T,
token?: any
): Provider {
return {
provide: obj,
provide: token ?? obj,
useClass: mockClass(obj),
};
}

View File

@@ -1,23 +1,31 @@
import type { Provider } from 'injection-js';
import { JSDOM } from 'jsdom';
import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx';
export class MockRouter extends AbstractRouter {
dom = new JSDOM('', {
url: 'http://localhost',
});
navigation: Navigation = {
id: 1,
extras: {},
initialUrl: new URL('https://localhost/'),
extractedUrl: new URL('https://localhost/'),
initialUrl: this.parseUrl(this.dom.window.location.href),
extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: 'imperative',
previousNavigation: null,
};
navigateByUrl(url: string): void {
const prevNavigation = this.navigation;
this.dom.reconfigure({
url: new URL(url, this.dom.window.location.href).href,
});
this.navigation = {
id: prevNavigation.id + 1,
extras: {},
initialUrl: prevNavigation.initialUrl,
extractedUrl: new URL(url),
extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: prevNavigation.trigger,
previousNavigation: prevNavigation,
};
@@ -26,7 +34,8 @@ export class MockRouter extends AbstractRouter {
return this.navigation;
}
parseUrl(url: string): UrlTree {
return new URL(url);
const u = new URL(url, this.dom.window.location.href);
return `${u.pathname}${u.search}${u.hash}`;
}
}

View File

@@ -35,20 +35,80 @@ export function mockImplementationWhenArgsEqual<M extends MockInstance<any>>(
});
}
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()!;
type Procedure = (...args: any[]) => any;
type Methods<T> = keyof {
[K in keyof T as T[K] extends Procedure ? K : never]: T[K];
};
type Classes<T> = {
[K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never;
}[keyof T] &
(string | symbol);
export type MockInstanceWithOrigin<M extends Procedure> = MockInstance<M> & {
getOriginImplementation?: () => any;
};
export function spyOnWithOrigin<
T,
M extends Classes<Required<T>> | Methods<Required<T>>,
>(
obj: T,
methodName: M
): Required<T>[M] extends {
new (...args: infer A): infer R;
}
? MockInstanceWithOrigin<(this: R, ...args: A) => R>
: T[M] extends Procedure
? MockInstanceWithOrigin<T[M]>
: never {
let currentObj = obj;
let origin:
| (Required<T>[M] extends {
new (...args: infer A): infer R;
}
? (this: R, ...args: A) => R
: T[M] extends Procedure
? T[M]
: never)
| undefined;
while (currentObj) {
origin = currentObj[methodName] as any;
if (origin) {
break;
}
currentObj = Object.getPrototypeOf(currentObj);
}
const spy = vi.spyOn(obj, methodName as any) as Required<T>[M] extends {
new (...args: infer A): infer R;
}
? MockInstanceWithOrigin<(this: R, ...args: A) => R>
: T[M] extends Procedure
? MockInstanceWithOrigin<T[M]>
: never;
spy.getOriginImplementation = () => origin;
return spy;
}
export function mockImplementationWhenArgs<T extends Procedure = Procedure>(
mockInstance: MockInstance<T> & { getOriginImplementation?: () => T },
whenArgs: (...args: Parameters<T>) => boolean,
implementation: T
): MockInstance<T> {
const spyImpl =
mockInstance.getMockImplementation() ??
mockInstance.getOriginImplementation?.();
return mockInstance.mockImplementation((...args) => {
if (isEqual(args, whenArgs)) {
if (whenArgs(...args)) {
return implementation(...args);
}
return spyImpl?.(...args);
if (spyImpl) {
return spyImpl(...args);
}
throw new Error('Mock implementation not defined for these arguments.');
});
}
@@ -58,46 +118,37 @@ export function mockImplementationWhenArgs<M extends MockInstance<any>>(
export function spyOnProperty<T, K extends keyof T>(
obj: T,
propertyKey: K,
accessType: 'get' | 'set' = 'get',
mockImplementation?: any
accessType: 'get' | 'set' = 'get'
) {
const originalDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey);
if (!originalDescriptor) {
throw new Error(
`Property ${String(propertyKey)} does not exist on the object.`
);
const ownDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey);
let finalDescriptor: PropertyDescriptor | undefined;
let currentObj = obj;
while (currentObj) {
finalDescriptor = Object.getOwnPropertyDescriptor(currentObj, propertyKey);
if (finalDescriptor) {
break;
}
currentObj = Object.getPrototypeOf(currentObj);
}
const spy = vi.fn();
let value: T[K] | undefined;
if (accessType === 'get') {
Object.defineProperty(obj, propertyKey, {
get: mockImplementation
? () => {
value = mockImplementation();
return value;
}
: spy,
get: spy,
configurable: true,
});
} else if (accessType === 'set') {
Object.defineProperty(obj, propertyKey, {
set: mockImplementation
? (next) => {
value = next;
}
: spy,
set: spy,
configurable: true,
});
}
// 恢复原始属性
spy.mockRestore = () => {
if (originalDescriptor) {
Object.defineProperty(obj, propertyKey, originalDescriptor);
if (ownDescriptor) {
Object.defineProperty(obj, propertyKey, ownDescriptor);
} else {
delete obj[propertyKey];
}

View File

@@ -13,21 +13,33 @@ export interface TestModuleMetadata {
}
export class TestBed {
static environmentInjector?: Injector;
private injector: ReflectiveInjector;
private providers: Provider[] = [];
private imports: Injector[] = [];
constructor(metadata: TestModuleMetadata = {}) {
constructor(
metadata: TestModuleMetadata = {},
environmentInjector?: Injector
) {
const providers = metadata.providers ?? [];
const imports = metadata.imports ?? [];
this.injector = ReflectiveInjector.resolveAndCreate(providers);
this.injector = ReflectiveInjector.resolveAndCreate(
providers,
environmentInjector
);
this.imports = imports.map((importFn) => importFn(this.injector));
}
static #instance?: TestBed;
static initTestEnvironment(providers: Provider[] = []) {
TestBed.environmentInjector =
ReflectiveInjector.resolveAndCreate(providers);
}
static configureTestingModule(metadata: TestModuleMetadata = {}) {
const newTestBed = new TestBed(metadata);
const newTestBed = new TestBed(metadata, TestBed.environmentInjector);
TestBed.#instance = newTestBed;
return newTestBed;