From 16b64478a5cecb05ce8b5cd690e5311867c855dc Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:05:08 +0200 Subject: [PATCH 1/3] feat: add imports option to configure --- projects/testing-library/src/lib/models.ts | 12 ++- .../src/lib/testing-library.ts | 44 +++++---- projects/testing-library/tests/render.spec.ts | 96 +++++++++++++------ src/app/examples/03-forms.spec.ts | 5 +- src/app/examples/03-forms.ts | 2 +- .../examples/04-forms-with-material.spec.ts | 3 +- src/app/examples/04-forms-with-material.ts | 2 +- test.ts | 8 ++ 8 files changed, 116 insertions(+), 56 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 3e109f0b..21a6b318 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -1,7 +1,15 @@ import { Type, DebugElement } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { Routes } from '@angular/router'; -import { BoundFunction, FireObject, Queries, queries, waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; +import { + BoundFunction, + FireObject, + Queries, + queries, + waitFor, + waitForElementToBeRemoved, + Config as dtlConfig, +} from '@testing-library/dom'; import { UserEvents } from './user-events'; import { OptionsReceived } from 'pretty-format'; @@ -304,3 +312,5 @@ export interface RenderDirectiveOptions; componentProperties?: Partial; } + +export type Config = dtlConfig & { defaultImports: any[] }; diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 541bfc59..24efceb0 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -16,8 +16,10 @@ import { queries as dtlQueries, waitForOptions as dtlWaitForOptions, configure as dtlConfigure, + getConfig as dtlGetConfig, + Config as dtlConfig, } from '@testing-library/dom'; -import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models'; +import { RenderComponentOptions, RenderDirectiveOptions, RenderResult, Config } from './models'; import { createSelectOptions, createType, tab } from './user-events'; const mountedFixtures = new Set>(); @@ -59,9 +61,11 @@ export async function render( removeAngularAttributes = false, } = renderOptions as RenderDirectiveOptions; + const config = dtlGetConfig(); + TestBed.configureTestingModule({ declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }), - imports: addAutoImports({ imports, routes }), + imports: addAutoImports({ imports: imports.concat((config as Config).defaultImports || []), routes }), providers: [...providers], schemas: [...schemas], }); @@ -102,7 +106,7 @@ export async function render( // Call ngOnChanges on initial render if (hasOnChangesHook(fixture.componentInstance)) { const changes = getChangesObj(null, fixture.componentInstance); - fixture.componentInstance.ngOnChanges(changes) + fixture.componentInstance.ngOnChanges(changes); } if (detectChangesOnRender) { @@ -224,20 +228,21 @@ function setComponentProperties( } function hasOnChangesHook(componentInstance: SutType): componentInstance is SutType & OnChanges { - return 'ngOnChanges' in componentInstance - && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'; -}; + return ( + 'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function' + ); +} -function getChangesObj( - oldProps: Partial | null, - newProps: Partial -) { +function getChangesObj(oldProps: Partial | null, newProps: Partial) { const isFirstChange = oldProps === null; - return Object.keys(newProps).reduce((changes, key) => ({ - ...changes, - [key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange) - }), {}); -}; + return Object.keys(newProps).reduce( + (changes, key) => ({ + ...changes, + [key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange), + }), + {}, + ); +} function addAutoDeclarations( component: Type, @@ -418,6 +423,12 @@ const userEvent = { tab: tab, }; +function configure(config: Partial) { + dtlConfigure({ + defaultImports: config.defaultImports, + } as Partial); +} + /** * Manually export otherwise we get the following error while running Jest tests * TypeError: Cannot set property fireEvent of [object Object] which has only a getter @@ -425,7 +436,6 @@ const userEvent = { */ export { buildQueries, - configure, getByLabelText, getAllByLabelText, queryByLabelText, @@ -491,4 +501,4 @@ export { within, } from '@testing-library/dom'; -export { fireEvent, screen, userEvent, waitFor, waitForElementToBeRemoved }; +export { configure, fireEvent, screen, userEvent, waitFor, waitForElementToBeRemoved }; diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index a28e7cf6..074599bc 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -1,7 +1,8 @@ import { Component, NgModule, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestBed } from '@angular/core/testing'; -import { render } from '../src/public_api'; +import { render, configure } from '../src/public_api'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; @Component({ selector: 'fixture', @@ -40,20 +41,20 @@ describe('removeAngularAttributes', () => { }); }); -@NgModule({ - declarations: [FixtureComponent], -}) -export class FixtureModule {} -describe('excludeComponentDeclaration', () => { - test('will throw if component is declared in an import', async () => { - await render(FixtureComponent, { - imports: [FixtureModule], - excludeComponentDeclaration: true, +describe('animationModule', () => { + @NgModule({ + declarations: [FixtureComponent], + }) + class FixtureModule {} + describe('excludeComponentDeclaration', () => { + test('will throw if component is declared in an import', async () => { + await render(FixtureComponent, { + imports: [FixtureModule], + excludeComponentDeclaration: true, + }); }); }); -}); -describe('animationModule', () => { test('adds NoopAnimationsModule by default', async () => { await render(FixtureComponent); const noopAnimationsModule = TestBed.inject(NoopAnimationsModule); @@ -72,28 +73,29 @@ describe('animationModule', () => { }); }); -@Component({ - selector: 'fixture', - template: ` {{ name }} `, -}) -class FixtureWithNgOnChangesComponent implements OnInit, OnChanges { - @Input() name = 'Sarah'; - @Input() nameInitialized?: (name: string) => void; - @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; - - ngOnInit() { - if (this.nameInitialized) { - this.nameInitialized(this.name); +describe('Angular component life-cycle hooks', () => { + @Component({ + selector: 'fixture', + template: ` {{ name }} `, + }) + class FixtureWithNgOnChangesComponent implements OnInit, OnChanges { + @Input() name = 'Sarah'; + @Input() nameInitialized?: (name: string) => void; + @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; + + ngOnInit() { + if (this.nameInitialized) { + this.nameInitialized(this.name); + } } - } - ngOnChanges(changes: SimpleChanges) { - if (changes.name && this.nameChanged) { - this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + ngOnChanges(changes: SimpleChanges) { + if (changes.name && this.nameChanged) { + this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + } } } -} -describe('Angular component life-cycle hooks', () => { + test('will call ngOnInit on initial render', async () => { const nameInitialized = jest.fn(); const componentProperties = { nameInitialized }; @@ -115,3 +117,37 @@ describe('Angular component life-cycle hooks', () => { expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); }); }); + +describe('configure: default imports', () => { + @Component({ + selector: 'app-fixture', + template: ` +
+
+ + +
+
+ `, + }) + class FormsComponent { + form = this.formBuilder.group({ + name: [''], + }); + + constructor(private formBuilder: FormBuilder) {} + } + + beforeEach(() => { + configure({ + defaultImports: [ReactiveFormsModule], + }); + }); + + test('adds default imports to the testbed', async () => { + await render(FormsComponent); + + const reactive = TestBed.inject(ReactiveFormsModule); + expect(reactive).not.toBeNull(); + }); +}); diff --git a/src/app/examples/03-forms.spec.ts b/src/app/examples/03-forms.spec.ts index e634f4c4..6be38a45 100644 --- a/src/app/examples/03-forms.spec.ts +++ b/src/app/examples/03-forms.spec.ts @@ -1,13 +1,10 @@ -import { ReactiveFormsModule } from '@angular/forms'; import { render, screen, fireEvent } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { FormsComponent } from './03-forms'; test('is possible to fill in a form and verify error messages (with the help of jest-dom https://testing-library.com/docs/ecosystem-jest-dom)', async () => { - await render(FormsComponent, { - imports: [ReactiveFormsModule], - }); + await render(FormsComponent); const nameControl = screen.getByRole('textbox', { name: /name/i }); const scoreControl = screen.getByRole('spinbutton', { name: /score/i }); diff --git a/src/app/examples/03-forms.ts b/src/app/examples/03-forms.ts index e881addb..03fa74f3 100644 --- a/src/app/examples/03-forms.ts +++ b/src/app/examples/03-forms.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '@angular/forms'; +import { FormBuilder, Validators, ValidationErrors } from '@angular/forms'; @Component({ selector: 'app-fixture', diff --git a/src/app/examples/04-forms-with-material.spec.ts b/src/app/examples/04-forms-with-material.spec.ts index cb038498..37bf17ab 100644 --- a/src/app/examples/04-forms-with-material.spec.ts +++ b/src/app/examples/04-forms-with-material.spec.ts @@ -1,4 +1,3 @@ -import { ReactiveFormsModule } from '@angular/forms'; import { render, screen } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -7,7 +6,7 @@ import { MaterialFormsComponent } from './04-forms-with-material'; test('is possible to fill in a form and verify error messages (with the help of jest-dom https://testing-library.com/docs/ecosystem-jest-dom)', async () => { const { fixture } = await render(MaterialFormsComponent, { - imports: [ReactiveFormsModule, MaterialModule], + imports: [MaterialModule], }); const nameControl = screen.getByLabelText(/name/i); diff --git a/src/app/examples/04-forms-with-material.ts b/src/app/examples/04-forms-with-material.ts index ea3c618a..e9a026ce 100644 --- a/src/app/examples/04-forms-with-material.ts +++ b/src/app/examples/04-forms-with-material.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '@angular/forms'; +import { FormBuilder, Validators, ValidationErrors } from '@angular/forms'; @Component({ selector: 'app-fixture', diff --git a/test.ts b/test.ts index 9020de51..95625a5a 100644 --- a/test.ts +++ b/test.ts @@ -1,2 +1,10 @@ import 'jest-preset-angular'; import '@testing-library/jest-dom'; +import { configure } from '@testing-library/angular'; +import { ReactiveFormsModule } from '@angular/forms'; + +beforeEach(() => { + configure({ + defaultImports: [ReactiveFormsModule], + }); +}); From a290e9c8841348bff5289b619ede0eed241d726f Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Wed, 24 Jun 2020 19:01:02 +0200 Subject: [PATCH 2/3] refactor: move things around --- jest.base.config.js | 2 +- projects/jest.lib.config.js | 5 ++ projects/setupJest.ts | 2 + projects/testing-library/src/lib/config.ts | 24 +++++++++ projects/testing-library/src/lib/models.ts | 2 +- .../src/lib/testing-library.ts | 34 +++++-------- projects/testing-library/src/public_api.ts | 1 + projects/testing-library/tests/config.spec.ts | 51 +++++++++++++++++++ projects/testing-library/tests/render.spec.ts | 37 +------------- src/jest.app.config.js | 6 +++ test.ts => src/setupJest.ts | 6 +-- 11 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 projects/setupJest.ts create mode 100644 projects/testing-library/src/lib/config.ts create mode 100644 projects/testing-library/tests/config.spec.ts rename test.ts => src/setupJest.ts (68%) diff --git a/jest.base.config.js b/jest.base.config.js index d360fe95..2ad734c0 100644 --- a/jest.base.config.js +++ b/jest.base.config.js @@ -1,8 +1,8 @@ module.exports = { preset: 'jest-preset-angular', rootDir: '../', - setupFilesAfterEnv: ['/test.ts'], transformIgnorePatterns: ['node_modules/(?!@ngrx)'], + snapshotSerializers: [ 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 'jest-preset-angular/build/AngularSnapshotSerializer.js', diff --git a/projects/jest.lib.config.js b/projects/jest.lib.config.js index eed3cf00..c74b4758 100644 --- a/projects/jest.lib.config.js +++ b/projects/jest.lib.config.js @@ -3,4 +3,9 @@ const baseConfig = require('../jest.base.config'); module.exports = { ...baseConfig, roots: ['/projects'], + setupFilesAfterEnv: ['/projects/setupJest.ts'], + displayName: { + name: 'LIB', + color: 'magenta', + }, }; diff --git a/projects/setupJest.ts b/projects/setupJest.ts new file mode 100644 index 00000000..9020de51 --- /dev/null +++ b/projects/setupJest.ts @@ -0,0 +1,2 @@ +import 'jest-preset-angular'; +import '@testing-library/jest-dom'; diff --git a/projects/testing-library/src/lib/config.ts b/projects/testing-library/src/lib/config.ts new file mode 100644 index 00000000..1ac96455 --- /dev/null +++ b/projects/testing-library/src/lib/config.ts @@ -0,0 +1,24 @@ +import { Config } from './models'; + +let config: Config = { + defaultImports: [], + dom: {}, +}; + +export function configure(newConfig: Partial | ((config: Partial) => Partial)) { + if (typeof newConfig === 'function') { + // Pass the existing config out to the provided function + // and accept a delta in return + newConfig = newConfig(config); + } + + // Merge the incoming config delta + config = { + ...config, + ...newConfig, + }; +} + +export function getConfig() { + return config; +} diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 21a6b318..6bd56951 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -313,4 +313,4 @@ export interface RenderDirectiveOptions; } -export type Config = dtlConfig & { defaultImports: any[] }; +export type Config = { defaultImports: any[]; dom: Partial }; diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 24efceb0..3c504c56 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -16,22 +16,13 @@ import { queries as dtlQueries, waitForOptions as dtlWaitForOptions, configure as dtlConfigure, - getConfig as dtlGetConfig, - Config as dtlConfig, } from '@testing-library/dom'; -import { RenderComponentOptions, RenderDirectiveOptions, RenderResult, Config } from './models'; +import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models'; +import { getConfig } from './config'; import { createSelectOptions, createType, tab } from './user-events'; const mountedFixtures = new Set>(); -dtlConfigure({ - eventWrapper: (cb) => { - const result = cb(); - detectChangesForMountedFixtures(); - return result; - }, -}); - export async function render( component: Type, renderOptions?: RenderComponentOptions, @@ -61,11 +52,20 @@ export async function render( removeAngularAttributes = false, } = renderOptions as RenderDirectiveOptions; - const config = dtlGetConfig(); + const config = getConfig(); + + dtlConfigure({ + eventWrapper: (cb) => { + const result = cb(); + detectChangesForMountedFixtures(); + return result; + }, + ...config.dom, + }); TestBed.configureTestingModule({ declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }), - imports: addAutoImports({ imports: imports.concat((config as Config).defaultImports || []), routes }), + imports: addAutoImports({ imports: imports.concat(config.defaultImports), routes }), providers: [...providers], schemas: [...schemas], }); @@ -423,12 +423,6 @@ const userEvent = { tab: tab, }; -function configure(config: Partial) { - dtlConfigure({ - defaultImports: config.defaultImports, - } as Partial); -} - /** * Manually export otherwise we get the following error while running Jest tests * TypeError: Cannot set property fireEvent of [object Object] which has only a getter @@ -501,4 +495,4 @@ export { within, } from '@testing-library/dom'; -export { configure, fireEvent, screen, userEvent, waitFor, waitForElementToBeRemoved }; +export { fireEvent, screen, userEvent, waitFor, waitForElementToBeRemoved }; diff --git a/projects/testing-library/src/public_api.ts b/projects/testing-library/src/public_api.ts index 629018f6..2ade91c1 100644 --- a/projects/testing-library/src/public_api.ts +++ b/projects/testing-library/src/public_api.ts @@ -4,4 +4,5 @@ export * from './lib/models'; export * from './lib/user-events'; +export * from './lib/config'; export * from './lib/testing-library'; diff --git a/projects/testing-library/tests/config.spec.ts b/projects/testing-library/tests/config.spec.ts new file mode 100644 index 00000000..06a79915 --- /dev/null +++ b/projects/testing-library/tests/config.spec.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { render, configure } from '../src/public_api'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; + +@Component({ + selector: 'app-fixture', + template: ` +
+
+ + +
+
+ `, +}) +class FormsComponent { + form = this.formBuilder.group({ + name: [''], + }); + + constructor(private formBuilder: FormBuilder) {} +} + +let originalConfig; +beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure((existingConfig) => { + originalConfig = existingConfig; + // Don't change the existing config + return {}; + }); +}); + +afterEach(() => { + configure(originalConfig); +}); + +beforeEach(() => { + configure({ + defaultImports: [ReactiveFormsModule], + }); +}); + +test('adds default imports to the testbed', async () => { + await render(FormsComponent); + + const reactive = TestBed.inject(ReactiveFormsModule); + expect(reactive).not.toBeNull(); +}); diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 074599bc..b8c6b7bc 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -1,8 +1,7 @@ import { Component, NgModule, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestBed } from '@angular/core/testing'; -import { render, configure } from '../src/public_api'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { render } from '../src/public_api'; @Component({ selector: 'fixture', @@ -117,37 +116,3 @@ describe('Angular component life-cycle hooks', () => { expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); }); }); - -describe('configure: default imports', () => { - @Component({ - selector: 'app-fixture', - template: ` -
-
- - -
-
- `, - }) - class FormsComponent { - form = this.formBuilder.group({ - name: [''], - }); - - constructor(private formBuilder: FormBuilder) {} - } - - beforeEach(() => { - configure({ - defaultImports: [ReactiveFormsModule], - }); - }); - - test('adds default imports to the testbed', async () => { - await render(FormsComponent); - - const reactive = TestBed.inject(ReactiveFormsModule); - expect(reactive).not.toBeNull(); - }); -}); diff --git a/src/jest.app.config.js b/src/jest.app.config.js index ebcc0a7d..31f1d766 100644 --- a/src/jest.app.config.js +++ b/src/jest.app.config.js @@ -2,6 +2,12 @@ const baseConfig = require('../jest.base.config'); module.exports = { ...baseConfig, + roots: ['/src'], modulePaths: ['/dist'], + setupFilesAfterEnv: ['/src/setupJest.ts'], + displayName: { + name: 'EXAMPLE', + color: 'blue', + }, }; diff --git a/test.ts b/src/setupJest.ts similarity index 68% rename from test.ts rename to src/setupJest.ts index 95625a5a..13a4e108 100644 --- a/test.ts +++ b/src/setupJest.ts @@ -3,8 +3,6 @@ import '@testing-library/jest-dom'; import { configure } from '@testing-library/angular'; import { ReactiveFormsModule } from '@angular/forms'; -beforeEach(() => { - configure({ - defaultImports: [ReactiveFormsModule], - }); +configure({ + defaultImports: [ReactiveFormsModule], }); From bcd5bd033d68b7e18b91b31b517ae738d14c5919 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:13:56 +0200 Subject: [PATCH 3/3] style: fix linting --- projects/testing-library/src/lib/models.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 6bd56951..0511f6a9 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -313,4 +313,7 @@ export interface RenderDirectiveOptions; } -export type Config = { defaultImports: any[]; dom: Partial }; +export interface Config { + defaultImports: any[]; + dom: Partial; +}