MOBILE-3320 tests: Add format-text directive tests

main
Noel De Martin 2020-10-26 13:20:00 +01:00
parent ca95f53134
commit 504bfe4c08
9 changed files with 243 additions and 58 deletions

View File

@ -112,6 +112,7 @@ const appConfig = {
},
],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/quotes': [
@ -197,7 +198,6 @@ const appConfig = {
'no-irregular-whitespace': 'error',
'no-multiple-empty-lines': 'error',
'no-new-wrappers': 'error',
'no-redeclare': 'error',
'no-sequences': 'error',
'no-trailing-spaces': 'error',
'no-underscore-dangle': 'error',

6
package-lock.json generated
View File

@ -3065,6 +3065,12 @@
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz",
"integrity": "sha512-ddDIRTO1ajtbxaNo2o7fPJggpN54PZf1ZUJKOjto2ENMJE/9GKUvaw3ZRuQzlS/p0E+PnIcssxfoqYJ4yiXSBw=="
},
"@types/faker": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.1.3.tgz",
"integrity": "sha512-7YTyCRoujZWYaCpDLslQJ8QzaFWFLZZ3mZ7Vfr/jJHascRmSd05pYteyt2FK4btF2vXyGq0obuoyLpcF99OvaA==",
"dev": true
},
"@types/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",

View File

@ -123,6 +123,7 @@
"@angular/compiler-cli": "~10.0.0",
"@angular/language-service": "~10.0.0",
"@ionic/angular-toolkit": "^2.3.0",
"@types/faker": "^5.1.3",
"@types/node": "^12.12.64",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",

View File

@ -12,33 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { createComponent, createMock, prepareComponentTest } from '@/tests/utils';
import { AppComponent } from '@app/app.component';
import { CoreLangProvider } from '@services/lang';
import { CoreEvents } from '@singletons/events';
import { CoreLangProvider } from '@services/lang';
import { mock, renderComponent, RenderConfig } from '@/tests/utils';
describe('App component', () => {
let langProvider: CoreLangProvider;
let config: Partial<RenderConfig>;
beforeEach(() => {
langProvider = createMock<CoreLangProvider>(['clearCustomStrings']);
prepareComponentTest(AppComponent, [
langProvider = mock<CoreLangProvider>(['clearCustomStrings']);
config = {
providers: [
{ provide: CoreLangProvider, useValue: langProvider },
]);
],
};
});
it('should render', () => {
const fixture = createComponent(AppComponent);
it('should render', async () => {
const fixture = await renderComponent(AppComponent, config);
expect(fixture.debugElement.componentInstance).toBeTruthy();
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
});
it('clears custom strings on logout', async () => {
const fixture = createComponent(AppComponent);
const fixture = await renderComponent(AppComponent, config);
fixture.componentInstance.ngOnInit();
CoreEvents.trigger(CoreEvents.LOGOUT);

View File

@ -12,39 +12,47 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NavController } from '@ionic/angular';
import { CoreApp } from '@/app/services/app';
import { CoreInit } from '@services/init';
import { CoreLoginInitPage } from '@core/login/pages/init/init.page';
import { CoreApp } from '@/app/services/app';
import { SplashScreen } from '@/app/singletons/core.singletons';
import { createComponent, preparePageTest, PageTestMocks, mockSingleton } from '@/tests/utils';
import { mock, mockSingleton, renderComponent, RenderConfig } from '@/tests/utils';
describe('CoreLogin Init Page', () => {
let mocks: PageTestMocks;
let navController: NavController;
let config: Partial<RenderConfig>;
beforeEach(async () => {
const initPromise = Promise.resolve();
beforeEach(() => {
mockSingleton(SplashScreen, ['hide']);
mockSingleton(CoreInit, { ready: () => Promise.resolve() });
mockSingleton(CoreApp, { getRedirect: () => ({}) });
mockSingleton(CoreInit, [], { ready: () => initPromise });
mockSingleton(CoreApp, [], { getRedirect: () => ({}) });
mocks = await preparePageTest(CoreLoginInitPage);
navController = mock<NavController>(['navigateRoot']);
config = {
providers: [
{ provide: NavController, useValue: navController },
],
};
});
it('should render', () => {
const fixture = createComponent(CoreLoginInitPage);
it('should render', async () => {
const fixture = await renderComponent(CoreLoginInitPage, config);
expect(fixture.debugElement.componentInstance).toBeTruthy();
expect(fixture.nativeElement.querySelector('ion-spinner')).toBeTruthy();
});
it('navigates to sites page after loading', async () => {
const fixture = createComponent(CoreLoginInitPage);
const fixture = await renderComponent(CoreLoginInitPage, config);
fixture.componentInstance.ngOnInit();
await CoreInit.instance.ready();
expect(mocks.navController.navigateRoot).toHaveBeenCalledWith('/login/sites');
expect(navController.navigateRoot).toHaveBeenCalledWith('/login/sites');
});
});

View File

@ -0,0 +1,112 @@
// (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 { DomSanitizer } from '@angular/platform-browser';
import { IonContent, NavController } from '@ionic/angular';
import { NgZone } from '@angular/core';
import Faker from 'faker';
import { CoreConfig } from '@services/config';
import { CoreDomUtils, CoreDomUtilsProvider } from '@services/utils/dom';
import { CoreFilepool } from '@services/filepool';
import { CoreFormatTextDirective } from '@directives/format-text';
import { CoreSite } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreUrlUtils, CoreUrlUtilsProvider } from '@services/utils/url';
import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils';
import { Platform } from '@singletons/core.singletons';
import { mock, mockSingleton, RenderConfig, renderWrapperComponent } from '@/tests/utils';
describe('CoreFormatTextDirective', () => {
let config: Partial<RenderConfig>;
beforeEach(() => {
mockSingleton(Platform, { ready: () => Promise.resolve() });
mockSingleton(CoreConfig, { get: (_, defaultValue) => defaultValue });
CoreDomUtils.setInstance(new CoreDomUtilsProvider(mock<DomSanitizer>()));
CoreUrlUtils.setInstance(new CoreUrlUtilsProvider());
CoreUtils.setInstance(new CoreUtilsProvider(mock<NgZone>()));
config = {
providers: [
{ provide: NavController, useValue: null },
{ provide: IonContent, useValue: null },
],
};
});
it('should render', async () => {
// Arrange
const sentence = Faker.lorem.sentence();
mockSingleton(CoreSites, { getSite: () => Promise.reject() });
// Act
const fixture = await renderWrapperComponent(
CoreFormatTextDirective,
'core-format-text',
{ text: sentence },
config,
);
// Assert
const text = fixture.nativeElement.querySelector('core-format-text');
expect(text).not.toBeNull();
expect(text.innerHTML).toEqual(sentence);
});
it('should use external-content directive on images', async () => {
// Arrange
const site = mock<CoreSite>({
getId: () => '42',
canDownloadFiles: () => true,
isVersionGreaterEqualThan: () => true,
});
// @todo this is done because we cannot mock image being loaded, we should find an alternative...
CoreUtils.instance.timeoutPromise = <T>() => Promise.resolve(null as unknown as T);
mockSingleton(CoreFilepool, { getSrcByUrl: jest.fn(() => Promise.resolve('file://local-path')) });
mockSingleton(CoreSites, {
getSite: jest.fn(() => Promise.resolve(site)),
getCurrentSite: () => Promise.resolve(site),
});
// Act
const fixture = await renderWrapperComponent(
CoreFormatTextDirective,
'core-format-text',
{ text: '<img src="https://image-url">', siteId: site.getId() },
config,
);
// Assert
const image = fixture.nativeElement.querySelector('img');
expect(image).not.toBeNull();
expect(image.src).toEqual('file://local-path/');
expect(CoreSites.instance.getSite).toHaveBeenCalledWith(site.getId());
expect(CoreFilepool.instance.getSrcByUrl).toHaveBeenCalledTimes(1);
});
it.todo('should format text');
it.todo('should get filters from server and format text');
it.todo('should use link directive on anchors');
});

View File

@ -12,65 +12,119 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CUSTOM_ELEMENTS_SCHEMA, Type } from '@angular/core';
import { Component, CUSTOM_ELEMENTS_SCHEMA, Type, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavController } from '@ionic/angular';
import { CoreSingletonClass } from '@app/classes/singletons-factory';
export interface ComponentTestMocks {
//
abstract class WrapperComponent<U> {
child!: U;
};
export interface PageTestMocks extends ComponentTestMocks {
navController: NavController;
export interface RenderConfig {
declarations: unknown[];
providers: unknown[];
}
export function createMock<T>(methods: string[] = [], properties: Record<string, unknown> = {}): T {
const mockObject = properties;
export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<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>(
methodsOrInstance: string[] | Record<string, unknown> = [],
instance: Record<string, unknown> = {},
): T {
instance = Array.isArray(methodsOrInstance) ? instance : methodsOrInstance;
const methods = Array.isArray(methodsOrInstance) ? methodsOrInstance : [];
for (const method of methods) {
mockObject[method] = jest.fn();
instance[method] = jest.fn();
}
return mockObject as T;
return instance as T;
}
export function mockSingleton(singletonClass: CoreSingletonClass<unknown>, instance?: Record<string, unknown>): void;
export function mockSingleton(
singletonClass: CoreSingletonClass<unknown>,
methods: string[] = [],
properties: Record<string, unknown> = {},
methods: string[],
instance?: Record<string, unknown>,
): void;
export function mockSingleton(
singletonClass: CoreSingletonClass<unknown>,
methodsOrInstance: string[] | Record<string, unknown> = [],
instance: Record<string, unknown> = {},
): void {
singletonClass.setInstance(createMock(methods, properties));
instance = Array.isArray(methodsOrInstance) ? instance : methodsOrInstance;
const methods = Array.isArray(methodsOrInstance) ? methodsOrInstance : [];
singletonClass.setInstance(mock(methods, instance));
}
export async function prepareComponentTest<T>(component: Type<T>, providers: unknown[] = []): Promise<ComponentTestMocks> {
export async function renderComponent<T>(component: Type<T>, config: Partial<RenderConfig> = {}): Promise<ComponentFixture<T>> {
return renderAngularComponent(component, {
declarations: [],
providers: [],
...config,
});
}
export async function renderWrapperComponent<T>(
component: Type<T>,
tag: string,
inputs: Record<string, { toString() }> = {},
config: Partial<RenderConfig> = {},
): Promise<WrapperComponentFixture<T>> {
const inputAttributes = Object
.entries(inputs)
.map(([name, value]) => `${name}="${value.toString().replace(/"/g, '&quot;')}"`)
.join(' ');
config.declarations = config.declarations ?? [];
config.declarations.push(component);
return renderAngularComponent(
createWrapperComponent(`<${tag} ${inputAttributes}></${tag}>`, component),
{
declarations: [],
providers: [],
...config,
},
);
}
async function renderAngularComponent<T>(component: Type<T>, config: RenderConfig): Promise<ComponentFixture<T>> {
config.declarations.push(component);
TestBed.configureTestingModule({
declarations: [component],
declarations: config.declarations,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers,
providers: config.providers,
});
await TestBed.compileComponents();
return {};
const fixture = TestBed.createComponent(component);
fixture.autoDetectChanges(true);
await fixture.whenRenderingDone();
await fixture.whenStable();
return fixture;
}
export async function preparePageTest<T>(component: Type<T>, providers: unknown[] = []): Promise<PageTestMocks> {
const mocks = {
navController: createMock<NavController>(['navigateRoot']),
};
function createWrapperComponent<U>(template: string, componentClass: Type<U>): Type<WrapperComponent<U>> {
@Component({ template })
class HostComponent extends WrapperComponent<U> {
const componentTestMocks = await prepareComponentTest(component, [
...providers,
{ provide: NavController, useValue: mocks.navController },
]);
@ViewChild(componentClass) child!: U;
return {
...componentTestMocks,
...mocks,
};
}
export function createComponent<T>(component: Type<T>): ComponentFixture<T> {
return TestBed.createComponent(component);
}
return HostComponent;
}

View File

@ -24,6 +24,7 @@
"cordova-plugin-inappbrowser",
"cordova",
"dom-mediacapture-record",
"faker",
"jest",
"node"
],

View File

@ -10,6 +10,7 @@
"cordova-plugin-inappbrowser",
"cordova",
"dom-mediacapture-record",
"faker",
"jest",
"node"
],