MOBILE-3833 test: Improve testing utils
parent
f86c2af9bf
commit
b039ad6275
|
@ -12,17 +12,12 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Observable, Subject } from 'rxjs';
|
|
||||||
|
|
||||||
import { AppComponent } from '@/app/app.component';
|
import { AppComponent } from '@/app/app.component';
|
||||||
import { CoreApp } from '@services/app';
|
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreLang, CoreLangProvider } from '@services/lang';
|
import { CoreLang, CoreLangProvider } from '@services/lang';
|
||||||
import { Network, Platform, NgZone } from '@singletons';
|
|
||||||
|
|
||||||
import { mockSingleton, renderComponent } from '@/testing/utils';
|
import { mockSingleton, renderComponent } from '@/testing/utils';
|
||||||
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
||||||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
|
|
||||||
|
@ -30,12 +25,6 @@ describe('AppComponent', () => {
|
||||||
let navigator: CoreNavigatorService;
|
let navigator: CoreNavigatorService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockSingleton(CoreApp, { setStatusBarColor: jest.fn() });
|
|
||||||
mockSingleton(Network, { onChange: () => new Observable() });
|
|
||||||
mockSingleton(Platform, { ready: () => Promise.resolve(), resume: new Subject<void>() });
|
|
||||||
mockSingleton(NgZone, { run: jest.fn() });
|
|
||||||
mockSingleton(CoreSitePlugins, { hasSitePluginsLoaded: false });
|
|
||||||
|
|
||||||
navigator = mockSingleton(CoreNavigator, ['navigate']);
|
navigator = mockSingleton(CoreNavigator, ['navigate']);
|
||||||
langProvider = mockSingleton(CoreLang, ['clearCustomStrings']);
|
langProvider = mockSingleton(CoreLang, ['clearCustomStrings']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,12 +14,9 @@
|
||||||
|
|
||||||
import { NavController as NavControllerService } from '@ionic/angular';
|
import { NavController as NavControllerService } from '@ionic/angular';
|
||||||
|
|
||||||
import { mock, mockSingleton } from '@/testing/utils';
|
import { mockSingleton } from '@/testing/utils';
|
||||||
|
|
||||||
import { CoreNavigatorService } from '@services/navigator';
|
import { CoreNavigatorService } from '@services/navigator';
|
||||||
import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils';
|
|
||||||
import { CoreUrlUtils, CoreUrlUtilsProvider } from '@services/utils/url';
|
|
||||||
import { CoreTextUtils, CoreTextUtilsProvider } from '@services/utils/text';
|
|
||||||
import { NavController, Router } from '@singletons';
|
import { NavController, Router } from '@singletons';
|
||||||
import { ActivatedRoute, RouterState } from '@angular/router';
|
import { ActivatedRoute, RouterState } from '@angular/router';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -36,15 +33,12 @@ describe('CoreNavigator', () => {
|
||||||
let navControllerMock: NavControllerService;
|
let navControllerMock: NavControllerService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
router = { url: '/' };
|
|
||||||
currentMainMenuHandlers = ['home'];
|
currentMainMenuHandlers = ['home'];
|
||||||
navigator = new CoreNavigatorService();
|
navigator = new CoreNavigatorService();
|
||||||
navControllerMock = mockSingleton(NavController, ['navigateRoot', 'navigateForward']);
|
navControllerMock = mockSingleton(NavController, ['navigateRoot', 'navigateForward']);
|
||||||
|
|
||||||
mockSingleton(Router, router);
|
router = mockSingleton(Router, { url: '/' });
|
||||||
mockSingleton(CoreUtils, new CoreUtilsProvider(mock()));
|
|
||||||
mockSingleton(CoreUrlUtils, new CoreUrlUtilsProvider());
|
|
||||||
mockSingleton(CoreTextUtils, new CoreTextUtilsProvider());
|
|
||||||
mockSingleton(CoreSites, { getCurrentSiteId: () => 42, isLoggedIn: () => true });
|
mockSingleton(CoreSites, { getCurrentSiteId: () => 42, isLoggedIn: () => true });
|
||||||
mockSingleton(CoreMainMenu, { isMainMenuTab: path => Promise.resolve(currentMainMenuHandlers.includes(path)) });
|
mockSingleton(CoreMainMenu, { isMainMenuTab: path => Promise.resolve(currentMainMenuHandlers.includes(path)) });
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,8 +73,9 @@ let createSingletonMethodProxy = (instance: any, method: Function, property: str
|
||||||
*
|
*
|
||||||
* @see makeSingleton
|
* @see makeSingleton
|
||||||
*/
|
*/
|
||||||
export type CoreSingletonProxy<Service> = Service & {
|
export type CoreSingletonProxy<Service = unknown> = Service & {
|
||||||
instance: Service;
|
instance: Service;
|
||||||
|
injectionToken: Type<Service> | AbstractType<Service> | Type<unknown> | string;
|
||||||
setInstance(instance: Service): void;
|
setInstance(instance: Service): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,13 +114,14 @@ export function makeSingleton<Service extends object = object>( // eslint-disabl
|
||||||
injectionToken: Type<Service> | AbstractType<Service> | Type<unknown> | string,
|
injectionToken: Type<Service> | AbstractType<Service> | Type<unknown> | string,
|
||||||
): CoreSingletonProxy<Service> {
|
): CoreSingletonProxy<Service> {
|
||||||
const singleton = {
|
const singleton = {
|
||||||
|
injectionToken,
|
||||||
setInstance(instance: Service) {
|
setInstance(instance: Service) {
|
||||||
Object.defineProperty(singleton, 'instance', {
|
Object.defineProperty(singleton, 'instance', {
|
||||||
value: instance,
|
value: instance,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
} as { instance: Service; setInstance(instance: Service) };
|
} as Pick<CoreSingletonProxy<Service>, 'injectionToken' | 'instance' | 'setInstance'>;
|
||||||
|
|
||||||
Object.defineProperty(singleton, 'instance', {
|
Object.defineProperty(singleton, 'instance', {
|
||||||
get: () => {
|
get: () => {
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
|
|
||||||
import 'jest-preset-angular';
|
import 'jest-preset-angular';
|
||||||
|
|
||||||
import { setCreateSingletonMethodProxy } from '@singletons';
|
import { setCreateSingletonMethodProxy, setSingletonsInjector } from '@singletons';
|
||||||
|
|
||||||
|
import { resetTestingEnvironment, getServiceInstance } from './utils';
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.debug = () => {
|
console.debug = () => {
|
||||||
|
@ -35,3 +37,6 @@ setCreateSingletonMethodProxy(
|
||||||
instance[`mock_${String(property)}`] ??
|
instance[`mock_${String(property)}`] ??
|
||||||
jest.fn((...args) => method.call(instance, ...args)),
|
jest.fn((...args) => method.call(instance, ...args)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setSingletonsInjector({ get: injectionToken => getServiceInstance(injectionToken) });
|
||||||
|
beforeEach(() => resetTestingEnvironment());
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// (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 { Directive, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[core-external-content]',
|
||||||
|
})
|
||||||
|
export class CoreExternalContentDirectiveStub { // eslint-disable-line @angular-eslint/directive-class-suffix
|
||||||
|
|
||||||
|
@Input() siteId?: string;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// (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 { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'translate',
|
||||||
|
})
|
||||||
|
export class TranslatePipeStub implements PipeTransform {
|
||||||
|
|
||||||
|
transform(text: string): string {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,18 +12,108 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, Type, ViewChild } from '@angular/core';
|
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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Network } from '@ionic-native/network/ngx';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { Platform } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
import { CoreSingletonProxy } from '@singletons';
|
import { CoreSingletonProxy } from '@singletons';
|
||||||
import { CoreTextUtilsProvider } from '@services/utils/text';
|
import { CoreTextUtilsProvider } from '@services/utils/text';
|
||||||
|
|
||||||
|
import { TranslatePipeStub } from './stubs/pipes/translate';
|
||||||
|
import { CoreExternalContentDirectiveStub } from './stubs/directives/core-external-content';
|
||||||
|
|
||||||
abstract class WrapperComponent<U> {
|
abstract class WrapperComponent<U> {
|
||||||
|
|
||||||
child!: U;
|
child!: U;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ServiceInjectionToken = AbstractType<unknown> | Type<unknown> | string;
|
||||||
|
|
||||||
|
let testBedInitialized = false;
|
||||||
|
const textUtils = new CoreTextUtilsProvider();
|
||||||
|
|
||||||
|
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 platformMock = mock<Platform>({ is: () => false, ready: () => Promise.resolve(), resume: new Subject<void>() });
|
||||||
|
const networkMock = mock<Network>({ onChange: () => new Observable() });
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ provide: Platform, useValue: platformMock },
|
||||||
|
{ provide: Network, useValue: networkMock },
|
||||||
|
{ 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 {
|
export interface RenderConfig {
|
||||||
declarations: unknown[];
|
declarations: unknown[];
|
||||||
providers: unknown[];
|
providers: unknown[];
|
||||||
|
@ -31,8 +121,6 @@ export interface RenderConfig {
|
||||||
|
|
||||||
export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>;
|
export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>;
|
||||||
|
|
||||||
const textUtils = new CoreTextUtilsProvider();
|
|
||||||
|
|
||||||
export function mock<T>(instance?: Record<string, unknown>): T;
|
export function mock<T>(instance?: Record<string, unknown>): T;
|
||||||
export function mock<T>(methods: string[], instance?: Record<string, unknown>): T;
|
export function mock<T>(methods: string[], instance?: Record<string, unknown>): T;
|
||||||
export function mock<T>(
|
export function mock<T>(
|
||||||
|
@ -69,19 +157,36 @@ export function mockSingleton<T>(
|
||||||
): T;
|
): T;
|
||||||
export function mockSingleton<T>(
|
export function mockSingleton<T>(
|
||||||
singleton: CoreSingletonProxy<T>,
|
singleton: CoreSingletonProxy<T>,
|
||||||
methodsOrInstance: string[] | Record<string, unknown> = [],
|
methodsOrProperties: string[] | Record<string, unknown> = [],
|
||||||
instance: Record<string, unknown> = {},
|
properties: Record<string, unknown> = {},
|
||||||
): T {
|
): T {
|
||||||
instance = Array.isArray(methodsOrInstance) ? instance : methodsOrInstance;
|
properties = Array.isArray(methodsOrProperties) ? properties : methodsOrProperties;
|
||||||
|
|
||||||
const methods = Array.isArray(methodsOrInstance) ? methodsOrInstance : [];
|
const methods = Array.isArray(methodsOrProperties) ? methodsOrProperties : [];
|
||||||
|
const instance = getServiceInstance(singleton.injectionToken);
|
||||||
const mockInstance = mock<T>(methods, instance);
|
const mockInstance = mock<T>(methods, instance);
|
||||||
|
|
||||||
|
Object.assign(mockInstance, properties);
|
||||||
|
|
||||||
singleton.setInstance(mockInstance);
|
singleton.setInstance(mockInstance);
|
||||||
|
|
||||||
return mockInstance;
|
return mockInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetTestingEnvironment(): void {
|
||||||
|
testBedInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>> {
|
export async function renderComponent<T>(component: Type<T>, config: Partial<RenderConfig> = {}): Promise<ComponentFixture<T>> {
|
||||||
return renderAngularComponent(component, {
|
return renderAngularComponent(component, {
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
@ -121,35 +226,3 @@ export async function renderWrapperComponent<T>(
|
||||||
|
|
||||||
return renderTemplate(component, `<${tag} ${inputAttributes}></${tag}>`, config);
|
return renderTemplate(component, `<${tag} ${inputAttributes}></${tag}>`, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderAngularComponent<T>(component: Type<T>, config: RenderConfig): Promise<ComponentFixture<T>> {
|
|
||||||
config.declarations.push(component);
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: config.declarations,
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
||||||
providers: config.providers,
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue