Vmeda.Online/src/testing/utils.ts

250 lines
7.8 KiB
TypeScript
Raw Normal View History

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { AbstractType, Component, CUSTOM_ELEMENTS_SCHEMA, Type, ViewChild } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable, Subject } from 'rxjs';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreSingletonProxy, Network, Platform } from '@singletons';
import { CoreTextUtilsProvider } from '@services/utils/text';
import { TranslatePipeStub } from './stubs/pipes/translate';
import { CoreExternalContentDirectiveStub } from './stubs/directives/core-external-content';
abstract class WrapperComponent<U> {
child!: U;
2020-10-07 09:13:54 +00:00
};
type ServiceInjectionToken = AbstractType<unknown> | Type<unknown> | string;
let testBedInitialized = false;
const textUtils = new CoreTextUtilsProvider();
const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, Record<string, unknown>][] = [
[Platform, mock({ is: () => false, ready: () => Promise.resolve(), resume: new Subject<void>() })],
[Network, { onChange: () => new Observable() }],
];
async function renderAngularComponent<T>(component: Type<T>, config: RenderConfig): Promise<ComponentFixture<T>> {
config.declarations.push(component);
TestBed.configureTestingModule({
declarations: [
...getDefaultDeclarations(),
...config.declarations,
],
providers: [
...getDefaultProviders(),
...config.providers,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [BrowserModule],
});
testBedInitialized = true;
await TestBed.compileComponents();
const fixture = TestBed.createComponent(component);
fixture.autoDetectChanges(true);
await fixture.whenRenderingDone();
await fixture.whenStable();
return fixture;
}
function createWrapperComponent<U>(template: string, componentClass: Type<U>): Type<WrapperComponent<U>> {
@Component({ template })
class HostComponent extends WrapperComponent<U> {
@ViewChild(componentClass) child!: U;
}
return HostComponent;
}
function getDefaultDeclarations(): unknown[] {
return [
TranslatePipeStub,
CoreExternalContentDirectiveStub,
];
}
function getDefaultProviders(): unknown[] {
const serviceProviders = DEFAULT_SERVICE_SINGLETON_MOCKS.map(
([singleton, mockInstance]) => ({
provide: singleton.injectionToken,
useValue: mockInstance,
}),
);
return [
...serviceProviders,
{ provide: CORE_SITE_SCHEMAS, multiple: true, useValue: [] },
];
}
function resolveServiceInstanceFromTestBed(injectionToken: Exclude<ServiceInjectionToken, string>): Record<string, unknown> | null {
if (!testBedInitialized) {
return null;
}
return TestBed.inject(injectionToken) as Record<string, unknown> | null;
}
function createNewServiceInstance(injectionToken: Exclude<ServiceInjectionToken, string>): Record<string, unknown> | null {
try {
const constructor = injectionToken as { new (): Record<string, unknown> };
return new constructor();
} catch (e) {
return null;
}
}
export interface RenderConfig {
declarations: unknown[];
providers: unknown[];
2020-10-07 09:13:54 +00:00
}
export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>;
/**
* Mock a certain class, converting its methods to Mock functions and overriding the specified properties and methods.
*
* @param instance Instance to mock.
* @param overrides Object with the properties or methods to override, or a list of methods to override with an empty function.
* @return Mock instance.
*/
export function mock<T>(instance?: T, overrides?: string[] | Record<string, unknown>): T;
export function mock<T>(instance?: Partial<T>, overrides?: string[] | Record<string, unknown>): Partial<T>;
export function mock<T>(
instance: T | Partial<T> = {},
overrides: string[] | Record<string, unknown> = {},
): T | Partial<T> {
// If overrides is an object, apply them to the instance.
if (!Array.isArray(overrides)) {
Object.assign(instance, overrides);
}
2020-10-07 09:13:54 +00:00
// Convert instance functions to jest functions.
for (const property of Object.getOwnPropertyNames(instance)) {
const value = instance[property];
if (typeof value !== 'function') {
continue;
}
instance[property] = jest.fn((...args) => value.call(instance, ...args));
}
// If overrides is a list of methods, add them now.
if (Array.isArray(overrides)) {
for (const method of overrides) {
instance[method] = jest.fn();
}
2020-10-07 09:13:54 +00:00
}
return instance;
2020-10-07 09:13:54 +00:00
}
2021-03-02 09:51:23 +00:00
export function mockSingleton<T>(singletonClass: CoreSingletonProxy<T>, instance: T): T;
export function mockSingleton<T>(singletonClass: CoreSingletonProxy<unknown>, instance?: Record<string, unknown>): T;
export function mockSingleton<T>(
2021-03-02 09:51:23 +00:00
singletonClass: CoreSingletonProxy<unknown>,
methods: string[],
instance?: Record<string, unknown>,
): T;
export function mockSingleton<T>(
2021-03-02 09:51:23 +00:00
singleton: CoreSingletonProxy<T>,
methodsOrProperties: string[] | Record<string, unknown> = [],
properties: Record<string, unknown> = {},
): T {
properties = Array.isArray(methodsOrProperties) ? properties : methodsOrProperties;
const methods = Array.isArray(methodsOrProperties) ? methodsOrProperties : [];
const instance = getServiceInstance(singleton.injectionToken) as T;
const mockInstance = mock(instance, methods);
Object.assign(mockInstance, properties);
2021-03-02 09:51:23 +00:00
singleton.setInstance(mockInstance);
return mockInstance;
}
export function resetTestingEnvironment(): void {
testBedInitialized = false;
for (const [singleton, mockInstance] of DEFAULT_SERVICE_SINGLETON_MOCKS) {
mockSingleton(singleton, mockInstance);
}
}
export function getServiceInstance(injectionToken: ServiceInjectionToken): Record<string, unknown> {
if (typeof injectionToken === 'string') {
return {};
}
return resolveServiceInstanceFromTestBed(injectionToken)
?? createNewServiceInstance(injectionToken)
?? {};
}
export async function renderComponent<T>(component: Type<T>, config: Partial<RenderConfig> = {}): Promise<ComponentFixture<T>> {
return renderAngularComponent(component, {
declarations: [],
providers: [],
...config,
});
}
export async function renderTemplate<T>(
component: Type<T>,
template: string,
config: Partial<RenderConfig> = {},
): Promise<WrapperComponentFixture<T>> {
config.declarations = config.declarations ?? [];
config.declarations.push(component);
return renderAngularComponent(
createWrapperComponent(template, component),
{
declarations: [],
providers: [],
...config,
},
);
2020-10-07 09:13:54 +00:00
}
export async function renderWrapperComponent<T>(
component: Type<T>,
tag: string,
inputs: Record<string, unknown> = {},
config: Partial<RenderConfig> = {},
): Promise<WrapperComponentFixture<T>> {
const inputAttributes = Object
.entries(inputs)
.map(([name, value]) => `[${name}]="${textUtils.escapeHTML(JSON.stringify(value)).replace(/\//g, '\\/')}"`)
.join(' ');
return renderTemplate(component, `<${tag} ${inputAttributes}></${tag}>`, config);
}