Skip to content

Commit 76942fb

Browse files
feat: add imports option to configure (#116)
1 parent 9baca38 commit 76942fb

15 files changed

+171
-62
lines changed

jest.base.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module.exports = {
22
preset: 'jest-preset-angular',
33
rootDir: '../',
4-
setupFilesAfterEnv: ['<rootDir>/test.ts'],
54
transformIgnorePatterns: ['node_modules/(?!@ngrx)'],
5+
66
snapshotSerializers: [
77
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
88
'jest-preset-angular/build/AngularSnapshotSerializer.js',

projects/jest.lib.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ const baseConfig = require('../jest.base.config');
33
module.exports = {
44
...baseConfig,
55
roots: ['<rootDir>/projects'],
6+
setupFilesAfterEnv: ['<rootDir>/projects/setupJest.ts'],
7+
displayName: {
8+
name: 'LIB',
9+
color: 'magenta',
10+
},
611
};
File renamed without changes.
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Config } from './models';
2+
3+
let config: Config = {
4+
defaultImports: [],
5+
dom: {},
6+
};
7+
8+
export function configure(newConfig: Partial<Config> | ((config: Partial<Config>) => Partial<Config>)) {
9+
if (typeof newConfig === 'function') {
10+
// Pass the existing config out to the provided function
11+
// and accept a delta in return
12+
newConfig = newConfig(config);
13+
}
14+
15+
// Merge the incoming config delta
16+
config = {
17+
...config,
18+
...newConfig,
19+
};
20+
}
21+
22+
export function getConfig() {
23+
return config;
24+
}

projects/testing-library/src/lib/models.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { Type, DebugElement } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
4-
import { BoundFunction, FireObject, Queries, queries, waitFor, waitForElementToBeRemoved } from '@testing-library/dom';
4+
import {
5+
BoundFunction,
6+
FireObject,
7+
Queries,
8+
queries,
9+
waitFor,
10+
waitForElementToBeRemoved,
11+
Config as dtlConfig,
12+
} from '@testing-library/dom';
513
import { UserEvents } from './user-events';
614
import { OptionsReceived } from 'pretty-format';
715

@@ -304,3 +312,8 @@ export interface RenderDirectiveOptions<DirectiveType, WrapperType, Q extends Qu
304312
wrapper?: Type<WrapperType>;
305313
componentProperties?: Partial<any>;
306314
}
315+
316+
export interface Config {
317+
defaultImports: any[];
318+
dom: Partial<dtlConfig>;
319+
}

projects/testing-library/src/lib/testing-library.ts

+27-23
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,11 @@ import {
1818
configure as dtlConfigure,
1919
} from '@testing-library/dom';
2020
import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models';
21+
import { getConfig } from './config';
2122
import { createSelectOptions, createType, tab } from './user-events';
2223

2324
const mountedFixtures = new Set<ComponentFixture<any>>();
2425

25-
dtlConfigure({
26-
eventWrapper: (cb) => {
27-
const result = cb();
28-
detectChangesForMountedFixtures();
29-
return result;
30-
},
31-
});
32-
3326
export async function render<ComponentType>(
3427
component: Type<ComponentType>,
3528
renderOptions?: RenderComponentOptions<ComponentType>,
@@ -59,9 +52,20 @@ export async function render<SutType, WrapperType = SutType>(
5952
removeAngularAttributes = false,
6053
} = renderOptions as RenderDirectiveOptions<SutType, WrapperType>;
6154

55+
const config = getConfig();
56+
57+
dtlConfigure({
58+
eventWrapper: (cb) => {
59+
const result = cb();
60+
detectChangesForMountedFixtures();
61+
return result;
62+
},
63+
...config.dom,
64+
});
65+
6266
TestBed.configureTestingModule({
6367
declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }),
64-
imports: addAutoImports({ imports, routes }),
68+
imports: addAutoImports({ imports: imports.concat(config.defaultImports), routes }),
6569
providers: [...providers],
6670
schemas: [...schemas],
6771
});
@@ -102,7 +106,7 @@ export async function render<SutType, WrapperType = SutType>(
102106
// Call ngOnChanges on initial render
103107
if (hasOnChangesHook(fixture.componentInstance)) {
104108
const changes = getChangesObj(null, fixture.componentInstance);
105-
fixture.componentInstance.ngOnChanges(changes)
109+
fixture.componentInstance.ngOnChanges(changes);
106110
}
107111

108112
if (detectChangesOnRender) {
@@ -224,20 +228,21 @@ function setComponentProperties<SutType>(
224228
}
225229

226230
function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
227-
return 'ngOnChanges' in componentInstance
228-
&& typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function';
229-
};
231+
return (
232+
'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'
233+
);
234+
}
230235

231-
function getChangesObj<SutType>(
232-
oldProps: Partial<SutType> | null,
233-
newProps: Partial<SutType>
234-
) {
236+
function getChangesObj<SutType>(oldProps: Partial<SutType> | null, newProps: Partial<SutType>) {
235237
const isFirstChange = oldProps === null;
236-
return Object.keys(newProps).reduce<SimpleChanges>((changes, key) => ({
237-
...changes,
238-
[key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange)
239-
}), {});
240-
};
238+
return Object.keys(newProps).reduce<SimpleChanges>(
239+
(changes, key) => ({
240+
...changes,
241+
[key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange),
242+
}),
243+
{},
244+
);
245+
}
241246

242247
function addAutoDeclarations<SutType>(
243248
component: Type<SutType>,
@@ -425,7 +430,6 @@ const userEvent = {
425430
*/
426431
export {
427432
buildQueries,
428-
configure,
429433
getByLabelText,
430434
getAllByLabelText,
431435
queryByLabelText,

projects/testing-library/src/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
export * from './lib/models';
66
export * from './lib/user-events';
7+
export * from './lib/config';
78
export * from './lib/testing-library';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Component } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { render, configure } from '../src/public_api';
4+
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
5+
6+
@Component({
7+
selector: 'app-fixture',
8+
template: `
9+
<form [formGroup]="form" name="form">
10+
<div>
11+
<label for="name">Name</label>
12+
<input type="text" id="name" name="name" formControlName="name" />
13+
</div>
14+
</form>
15+
`,
16+
})
17+
class FormsComponent {
18+
form = this.formBuilder.group({
19+
name: [''],
20+
});
21+
22+
constructor(private formBuilder: FormBuilder) {}
23+
}
24+
25+
let originalConfig;
26+
beforeEach(() => {
27+
// Grab the existing configuration so we can restore
28+
// it at the end of the test
29+
configure((existingConfig) => {
30+
originalConfig = existingConfig;
31+
// Don't change the existing config
32+
return {};
33+
});
34+
});
35+
36+
afterEach(() => {
37+
configure(originalConfig);
38+
});
39+
40+
beforeEach(() => {
41+
configure({
42+
defaultImports: [ReactiveFormsModule],
43+
});
44+
});
45+
46+
test('adds default imports to the testbed', async () => {
47+
await render(FormsComponent);
48+
49+
const reactive = TestBed.inject(ReactiveFormsModule);
50+
expect(reactive).not.toBeNull();
51+
});

projects/testing-library/tests/render.spec.ts

+30-29
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ describe('removeAngularAttributes', () => {
4040
});
4141
});
4242

43-
@NgModule({
44-
declarations: [FixtureComponent],
45-
})
46-
export class FixtureModule {}
47-
describe('excludeComponentDeclaration', () => {
48-
test('will throw if component is declared in an import', async () => {
49-
await render(FixtureComponent, {
50-
imports: [FixtureModule],
51-
excludeComponentDeclaration: true,
43+
describe('animationModule', () => {
44+
@NgModule({
45+
declarations: [FixtureComponent],
46+
})
47+
class FixtureModule {}
48+
describe('excludeComponentDeclaration', () => {
49+
test('will throw if component is declared in an import', async () => {
50+
await render(FixtureComponent, {
51+
imports: [FixtureModule],
52+
excludeComponentDeclaration: true,
53+
});
5254
});
5355
});
54-
});
5556

56-
describe('animationModule', () => {
5757
test('adds NoopAnimationsModule by default', async () => {
5858
await render(FixtureComponent);
5959
const noopAnimationsModule = TestBed.inject(NoopAnimationsModule);
@@ -72,28 +72,29 @@ describe('animationModule', () => {
7272
});
7373
});
7474

75-
@Component({
76-
selector: 'fixture',
77-
template: ` {{ name }} `,
78-
})
79-
class FixtureWithNgOnChangesComponent implements OnInit, OnChanges {
80-
@Input() name = 'Sarah';
81-
@Input() nameInitialized?: (name: string) => void;
82-
@Input() nameChanged?: (name: string, isFirstChange: boolean) => void;
83-
84-
ngOnInit() {
85-
if (this.nameInitialized) {
86-
this.nameInitialized(this.name);
75+
describe('Angular component life-cycle hooks', () => {
76+
@Component({
77+
selector: 'fixture',
78+
template: ` {{ name }} `,
79+
})
80+
class FixtureWithNgOnChangesComponent implements OnInit, OnChanges {
81+
@Input() name = 'Sarah';
82+
@Input() nameInitialized?: (name: string) => void;
83+
@Input() nameChanged?: (name: string, isFirstChange: boolean) => void;
84+
85+
ngOnInit() {
86+
if (this.nameInitialized) {
87+
this.nameInitialized(this.name);
88+
}
8789
}
88-
}
8990

90-
ngOnChanges(changes: SimpleChanges) {
91-
if (changes.name && this.nameChanged) {
92-
this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
91+
ngOnChanges(changes: SimpleChanges) {
92+
if (changes.name && this.nameChanged) {
93+
this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
94+
}
9395
}
9496
}
95-
}
96-
describe('Angular component life-cycle hooks', () => {
97+
9798
test('will call ngOnInit on initial render', async () => {
9899
const nameInitialized = jest.fn();
99100
const componentProperties = { nameInitialized };

src/app/examples/03-forms.spec.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { ReactiveFormsModule } from '@angular/forms';
21
import { render, screen, fireEvent } from '@testing-library/angular';
32
import userEvent from '@testing-library/user-event';
43

54
import { FormsComponent } from './03-forms';
65

76
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 () => {
8-
await render(FormsComponent, {
9-
imports: [ReactiveFormsModule],
10-
});
7+
await render(FormsComponent);
118

129
const nameControl = screen.getByRole('textbox', { name: /name/i });
1310
const scoreControl = screen.getByRole('spinbutton', { name: /score/i });

src/app/examples/03-forms.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from '@angular/core';
2-
import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
2+
import { FormBuilder, Validators, ValidationErrors } from '@angular/forms';
33

44
@Component({
55
selector: 'app-fixture',

src/app/examples/04-forms-with-material.spec.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ReactiveFormsModule } from '@angular/forms';
21
import { render, screen } from '@testing-library/angular';
32
import userEvent from '@testing-library/user-event';
43

@@ -7,7 +6,7 @@ import { MaterialFormsComponent } from './04-forms-with-material';
76

87
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 () => {
98
const { fixture } = await render(MaterialFormsComponent, {
10-
imports: [ReactiveFormsModule, MaterialModule],
9+
imports: [MaterialModule],
1110
});
1211

1312
const nameControl = screen.getByLabelText(/name/i);

src/app/examples/04-forms-with-material.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from '@angular/core';
2-
import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
2+
import { FormBuilder, Validators, ValidationErrors } from '@angular/forms';
33

44
@Component({
55
selector: 'app-fixture',

src/jest.app.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ const baseConfig = require('../jest.base.config');
22

33
module.exports = {
44
...baseConfig,
5+
56
roots: ['<rootDir>/src'],
67
modulePaths: ['<rootDir>/dist'],
8+
setupFilesAfterEnv: ['<rootDir>/src/setupJest.ts'],
9+
displayName: {
10+
name: 'EXAMPLE',
11+
color: 'blue',
12+
},
713
};

src/setupJest.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import 'jest-preset-angular';
2+
import '@testing-library/jest-dom';
3+
import { configure } from '@testing-library/angular';
4+
import { ReactiveFormsModule } from '@angular/forms';
5+
6+
configure({
7+
defaultImports: [ReactiveFormsModule],
8+
});

0 commit comments

Comments
 (0)