MOBILE-3320 tests: Add format-text directive tests
parent
ca95f53134
commit
504bfe4c08
|
@ -112,6 +112,7 @@ const appConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-redeclare': 'error',
|
||||||
'@typescript-eslint/no-this-alias': 'error',
|
'@typescript-eslint/no-this-alias': 'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'error',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
'@typescript-eslint/quotes': [
|
'@typescript-eslint/quotes': [
|
||||||
|
@ -197,7 +198,6 @@ const appConfig = {
|
||||||
'no-irregular-whitespace': 'error',
|
'no-irregular-whitespace': 'error',
|
||||||
'no-multiple-empty-lines': 'error',
|
'no-multiple-empty-lines': 'error',
|
||||||
'no-new-wrappers': 'error',
|
'no-new-wrappers': 'error',
|
||||||
'no-redeclare': 'error',
|
|
||||||
'no-sequences': 'error',
|
'no-sequences': 'error',
|
||||||
'no-trailing-spaces': 'error',
|
'no-trailing-spaces': 'error',
|
||||||
'no-underscore-dangle': 'error',
|
'no-underscore-dangle': 'error',
|
||||||
|
|
|
@ -3065,6 +3065,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz",
|
||||||
"integrity": "sha512-ddDIRTO1ajtbxaNo2o7fPJggpN54PZf1ZUJKOjto2ENMJE/9GKUvaw3ZRuQzlS/p0E+PnIcssxfoqYJ4yiXSBw=="
|
"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": {
|
"@types/glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||||
|
|
|
@ -123,6 +123,7 @@
|
||||||
"@angular/compiler-cli": "~10.0.0",
|
"@angular/compiler-cli": "~10.0.0",
|
||||||
"@angular/language-service": "~10.0.0",
|
"@angular/language-service": "~10.0.0",
|
||||||
"@ionic/angular-toolkit": "^2.3.0",
|
"@ionic/angular-toolkit": "^2.3.0",
|
||||||
|
"@types/faker": "^5.1.3",
|
||||||
"@types/node": "^12.12.64",
|
"@types/node": "^12.12.64",
|
||||||
"@typescript-eslint/eslint-plugin": "4.3.0",
|
"@typescript-eslint/eslint-plugin": "4.3.0",
|
||||||
"@typescript-eslint/parser": "4.3.0",
|
"@typescript-eslint/parser": "4.3.0",
|
||||||
|
|
|
@ -12,33 +12,35 @@
|
||||||
// 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 { createComponent, createMock, prepareComponentTest } from '@/tests/utils';
|
|
||||||
|
|
||||||
import { AppComponent } from '@app/app.component';
|
import { AppComponent } from '@app/app.component';
|
||||||
import { CoreLangProvider } from '@services/lang';
|
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreLangProvider } from '@services/lang';
|
||||||
|
|
||||||
|
import { mock, renderComponent, RenderConfig } from '@/tests/utils';
|
||||||
|
|
||||||
describe('App component', () => {
|
describe('App component', () => {
|
||||||
|
|
||||||
let langProvider: CoreLangProvider;
|
let langProvider: CoreLangProvider;
|
||||||
|
let config: Partial<RenderConfig>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
langProvider = createMock<CoreLangProvider>(['clearCustomStrings']);
|
langProvider = mock<CoreLangProvider>(['clearCustomStrings']);
|
||||||
|
config = {
|
||||||
prepareComponentTest(AppComponent, [
|
providers: [
|
||||||
{ provide: CoreLangProvider, useValue: langProvider },
|
{ provide: CoreLangProvider, useValue: langProvider },
|
||||||
]);
|
],
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render', () => {
|
it('should render', async () => {
|
||||||
const fixture = createComponent(AppComponent);
|
const fixture = await renderComponent(AppComponent, config);
|
||||||
|
|
||||||
expect(fixture.debugElement.componentInstance).toBeTruthy();
|
expect(fixture.debugElement.componentInstance).toBeTruthy();
|
||||||
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
|
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears custom strings on logout', async () => {
|
it('clears custom strings on logout', async () => {
|
||||||
const fixture = createComponent(AppComponent);
|
const fixture = await renderComponent(AppComponent, config);
|
||||||
|
|
||||||
fixture.componentInstance.ngOnInit();
|
fixture.componentInstance.ngOnInit();
|
||||||
CoreEvents.trigger(CoreEvents.LOGOUT);
|
CoreEvents.trigger(CoreEvents.LOGOUT);
|
||||||
|
|
|
@ -12,39 +12,47 @@
|
||||||
// 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 { NavController } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreApp } from '@/app/services/app';
|
||||||
import { CoreInit } from '@services/init';
|
import { CoreInit } from '@services/init';
|
||||||
import { CoreLoginInitPage } from '@core/login/pages/init/init.page';
|
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', () => {
|
describe('CoreLogin Init Page', () => {
|
||||||
|
|
||||||
let mocks: PageTestMocks;
|
let navController: NavController;
|
||||||
|
let config: Partial<RenderConfig>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
const initPromise = Promise.resolve();
|
mockSingleton(SplashScreen, ['hide']);
|
||||||
|
mockSingleton(CoreInit, { ready: () => Promise.resolve() });
|
||||||
|
mockSingleton(CoreApp, { getRedirect: () => ({}) });
|
||||||
|
|
||||||
mockSingleton(CoreInit, [], { ready: () => initPromise });
|
navController = mock<NavController>(['navigateRoot']);
|
||||||
mockSingleton(CoreApp, [], { getRedirect: () => ({}) });
|
config = {
|
||||||
|
providers: [
|
||||||
mocks = await preparePageTest(CoreLoginInitPage);
|
{ provide: NavController, useValue: navController },
|
||||||
|
],
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render', () => {
|
it('should render', async () => {
|
||||||
const fixture = createComponent(CoreLoginInitPage);
|
const fixture = await renderComponent(CoreLoginInitPage, config);
|
||||||
|
|
||||||
expect(fixture.debugElement.componentInstance).toBeTruthy();
|
expect(fixture.debugElement.componentInstance).toBeTruthy();
|
||||||
expect(fixture.nativeElement.querySelector('ion-spinner')).toBeTruthy();
|
expect(fixture.nativeElement.querySelector('ion-spinner')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to sites page after loading', async () => {
|
it('navigates to sites page after loading', async () => {
|
||||||
const fixture = createComponent(CoreLoginInitPage);
|
const fixture = await renderComponent(CoreLoginInitPage, config);
|
||||||
|
|
||||||
fixture.componentInstance.ngOnInit();
|
fixture.componentInstance.ngOnInit();
|
||||||
await CoreInit.instance.ready();
|
await CoreInit.instance.ready();
|
||||||
|
|
||||||
expect(mocks.navController.navigateRoot).toHaveBeenCalledWith('/login/sites');
|
expect(navController.navigateRoot).toHaveBeenCalledWith('/login/sites');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
});
|
|
@ -12,65 +12,119 @@
|
||||||
// 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 { 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
import { CoreSingletonClass } from '@app/classes/singletons-factory';
|
import { CoreSingletonClass } from '@app/classes/singletons-factory';
|
||||||
|
|
||||||
export interface ComponentTestMocks {
|
abstract class WrapperComponent<U> {
|
||||||
//
|
|
||||||
|
child!: U;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PageTestMocks extends ComponentTestMocks {
|
export interface RenderConfig {
|
||||||
navController: NavController;
|
declarations: unknown[];
|
||||||
|
providers: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMock<T>(methods: string[] = [], properties: Record<string, unknown> = {}): T {
|
export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>;
|
||||||
const mockObject = properties;
|
|
||||||
|
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) {
|
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(
|
export function mockSingleton(
|
||||||
singletonClass: CoreSingletonClass<unknown>,
|
singletonClass: CoreSingletonClass<unknown>,
|
||||||
methods: string[] = [],
|
methods: string[],
|
||||||
properties: Record<string, unknown> = {},
|
instance?: Record<string, unknown>,
|
||||||
|
): void;
|
||||||
|
export function mockSingleton(
|
||||||
|
singletonClass: CoreSingletonClass<unknown>,
|
||||||
|
methodsOrInstance: string[] | Record<string, unknown> = [],
|
||||||
|
instance: Record<string, unknown> = {},
|
||||||
): void {
|
): 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, '"')}"`)
|
||||||
|
.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({
|
TestBed.configureTestingModule({
|
||||||
declarations: [component],
|
declarations: config.declarations,
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
providers,
|
providers: config.providers,
|
||||||
});
|
});
|
||||||
|
|
||||||
await TestBed.compileComponents();
|
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> {
|
function createWrapperComponent<U>(template: string, componentClass: Type<U>): Type<WrapperComponent<U>> {
|
||||||
const mocks = {
|
@Component({ template })
|
||||||
navController: createMock<NavController>(['navigateRoot']),
|
class HostComponent extends WrapperComponent<U> {
|
||||||
};
|
|
||||||
|
|
||||||
const componentTestMocks = await prepareComponentTest(component, [
|
@ViewChild(componentClass) child!: U;
|
||||||
...providers,
|
|
||||||
{ provide: NavController, useValue: mocks.navController },
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
}
|
||||||
...componentTestMocks,
|
|
||||||
...mocks,
|
return HostComponent;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createComponent<T>(component: Type<T>): ComponentFixture<T> {
|
|
||||||
return TestBed.createComponent(component);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"cordova-plugin-inappbrowser",
|
"cordova-plugin-inappbrowser",
|
||||||
"cordova",
|
"cordova",
|
||||||
"dom-mediacapture-record",
|
"dom-mediacapture-record",
|
||||||
|
"faker",
|
||||||
"jest",
|
"jest",
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"cordova-plugin-inappbrowser",
|
"cordova-plugin-inappbrowser",
|
||||||
"cordova",
|
"cordova",
|
||||||
"dom-mediacapture-record",
|
"dom-mediacapture-record",
|
||||||
|
"faker",
|
||||||
"jest",
|
"jest",
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue