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/test.ts b/projects/setupJest.ts similarity index 100% rename from test.ts rename to projects/setupJest.ts 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 3e109f0b..0511f6a9 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,8 @@ export interface RenderDirectiveOptions; componentProperties?: Partial; } + +export interface 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 541bfc59..3c504c56 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -18,18 +18,11 @@ import { configure as dtlConfigure, } from '@testing-library/dom'; 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, @@ -59,9 +52,20 @@ export async function render( removeAngularAttributes = false, } = renderOptions as RenderDirectiveOptions; + 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, routes }), + imports: addAutoImports({ imports: imports.concat(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, @@ -425,7 +430,6 @@ const userEvent = { */ export { buildQueries, - configure, getByLabelText, getAllByLabelText, queryByLabelText, 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 a28e7cf6..b8c6b7bc 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -40,20 +40,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 +72,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 }; 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/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/src/setupJest.ts b/src/setupJest.ts new file mode 100644 index 00000000..13a4e108 --- /dev/null +++ b/src/setupJest.ts @@ -0,0 +1,8 @@ +import 'jest-preset-angular'; +import '@testing-library/jest-dom'; +import { configure } from '@testing-library/angular'; +import { ReactiveFormsModule } from '@angular/forms'; + +configure({ + defaultImports: [ReactiveFormsModule], +});