From 504bfe4c085cbd4341b97d7a7ac5cf97a585e7b4 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 26 Oct 2020 13:20:00 +0100 Subject: [PATCH] MOBILE-3320 tests: Add format-text directive tests --- .eslintrc.js | 2 +- package-lock.json | 6 + package.json | 1 + src/app/app.component.test.ts | 24 ++-- src/app/core/login/tests/init.page.test.ts | 34 ++++-- src/app/directives/tests/format-text.test.ts | 112 +++++++++++++++++ src/tests/utils.ts | 120 ++++++++++++++----- tsconfig.json | 1 + tsconfig.test.json | 1 + 9 files changed, 243 insertions(+), 58 deletions(-) create mode 100644 src/app/directives/tests/format-text.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 19d6f58e5..4066ad7f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/package-lock.json b/package-lock.json index 738a386c8..5834af075 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 94694b8ac..a77f4a743 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/app.component.test.ts b/src/app/app.component.test.ts index 619c8faf9..e2c1c1958 100644 --- a/src/app/app.component.test.ts +++ b/src/app/app.component.test.ts @@ -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; beforeEach(() => { - langProvider = createMock(['clearCustomStrings']); - - prepareComponentTest(AppComponent, [ - { provide: CoreLangProvider, useValue: langProvider }, - ]); + langProvider = mock(['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); diff --git a/src/app/core/login/tests/init.page.test.ts b/src/app/core/login/tests/init.page.test.ts index f5ddfce3c..f66a1ff8b 100644 --- a/src/app/core/login/tests/init.page.test.ts +++ b/src/app/core/login/tests/init.page.test.ts @@ -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; - 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(['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'); }); }); diff --git a/src/app/directives/tests/format-text.test.ts b/src/app/directives/tests/format-text.test.ts new file mode 100644 index 000000000..3f7d858e9 --- /dev/null +++ b/src/app/directives/tests/format-text.test.ts @@ -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; + + beforeEach(() => { + mockSingleton(Platform, { ready: () => Promise.resolve() }); + mockSingleton(CoreConfig, { get: (_, defaultValue) => defaultValue }); + + CoreDomUtils.setInstance(new CoreDomUtilsProvider(mock())); + CoreUrlUtils.setInstance(new CoreUrlUtilsProvider()); + CoreUtils.setInstance(new CoreUtilsProvider(mock())); + + 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({ + 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 = () => 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: '', 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'); + +}); diff --git a/src/tests/utils.ts b/src/tests/utils.ts index 9adc3bdd6..58d93164a 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -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 { + + child!: U; + }; -export interface PageTestMocks extends ComponentTestMocks { - navController: NavController; +export interface RenderConfig { + declarations: unknown[]; + providers: unknown[]; } -export function createMock(methods: string[] = [], properties: Record = {}): T { - const mockObject = properties; +export type WrapperComponentFixture = ComponentFixture>; + +export function mock(instance?: Record): T; +export function mock(methods: string[], instance?: Record): T; +export function mock( + methodsOrInstance: string[] | Record = [], + instance: Record = {}, +): 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, instance?: Record): void; export function mockSingleton( singletonClass: CoreSingletonClass, - methods: string[] = [], - properties: Record = {}, + methods: string[], + instance?: Record, +): void; +export function mockSingleton( + singletonClass: CoreSingletonClass, + methodsOrInstance: string[] | Record = [], + instance: Record = {}, ): 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(component: Type, providers: unknown[] = []): Promise { +export async function renderComponent(component: Type, config: Partial = {}): Promise> { + return renderAngularComponent(component, { + declarations: [], + providers: [], + ...config, + }); +} + +export async function renderWrapperComponent( + component: Type, + tag: string, + inputs: Record = {}, + config: Partial = {}, +): Promise> { + 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}>`, component), + { + declarations: [], + providers: [], + ...config, + }, + ); +} + +async function renderAngularComponent(component: Type, config: RenderConfig): Promise> { + 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(component: Type, providers: unknown[] = []): Promise { - const mocks = { - navController: createMock(['navigateRoot']), - }; +function createWrapperComponent(template: string, componentClass: Type): Type> { + @Component({ template }) + class HostComponent extends WrapperComponent { - const componentTestMocks = await prepareComponentTest(component, [ - ...providers, - { provide: NavController, useValue: mocks.navController }, - ]); + @ViewChild(componentClass) child!: U; - return { - ...componentTestMocks, - ...mocks, - }; -} - -export function createComponent(component: Type): ComponentFixture { - return TestBed.createComponent(component); + } + + return HostComponent; } diff --git a/tsconfig.json b/tsconfig.json index 5c880d8f6..b2401e638 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "cordova-plugin-inappbrowser", "cordova", "dom-mediacapture-record", + "faker", "jest", "node" ], diff --git a/tsconfig.test.json b/tsconfig.test.json index 25bf4d29a..06e4ff33d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,6 +10,7 @@ "cordova-plugin-inappbrowser", "cordova", "dom-mediacapture-record", + "faker", "jest", "node" ],