Skip to content

Commit 6fa8df9

Browse files
feat: testing directives (#47)
BREAKING CHANGE: - It isn't possible to define your component as a component template anymore, it's obligated now to use the component's class. BEFORE: ```ts const component = render(`<person-details></person-details>`) ``` AFTER: ```ts const component = render(PersonDetails) ``` - It isn't possible anymore to define a wrapper while testing a component, it's only possible to do this while testing a directive. ```ts const component = render(PersonDetails, { wrapper: PersonWrapper }) ```
1 parent 656aa69 commit 6fa8df9

12 files changed

+288
-162
lines changed

jest.base.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
transform: {
1414
'^.+\\.(ts|js|html)$': 'ts-jest',
1515
},
16+
transformIgnorePatterns: ['node_modules/(?!@ngrx)'],
1617
snapshotSerializers: [
1718
'jest-preset-angular/AngularSnapshotSerializer.js',
1819
'jest-preset-angular/HTMLCommentSerializer.js',

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

+49-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { Type } from '@angular/core';
1+
import { Type, DebugElement } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
44
import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom';
55
import { UserEvents } from './user-events';
66

77
export type RenderResultQueries<Q extends Queries = typeof queries> = { [P in keyof Q]: BoundFunction<Q[P]> };
88

9-
export interface RenderResult extends RenderResultQueries, FireObject, UserEvents {
9+
export interface RenderResult<ComponentType, WrapperType = ComponentType>
10+
extends RenderResultQueries,
11+
FireObject,
12+
UserEvents {
1013
/**
1114
* @description
1215
* The containing DOM node of your rendered Angular Component.
@@ -31,11 +34,19 @@ export interface RenderResult extends RenderResultQueries, FireObject, UserEvent
3134
detectChanges: () => void;
3235
/**
3336
* @description
34-
* The Angular `ComponentFixture` of the component.
37+
* The Angular `ComponentFixture` of the component or the wrapper.
38+
* If a template is provided, it will be the fixture of the wrapper.
3539
*
3640
* For more info see https://angular.io/api/core/testing/ComponentFixture
3741
*/
38-
fixture: ComponentFixture<any>;
42+
fixture: ComponentFixture<WrapperType>;
43+
/**
44+
* @description
45+
* The Angular `DebugElement` of the component.
46+
*
47+
* For more info see https://angular.io/api/core/DebugElement
48+
*/
49+
debugElement: DebugElement;
3950
/**
4051
* @description
4152
* Navigates to the href of the element or to the path.
@@ -44,7 +55,7 @@ export interface RenderResult extends RenderResultQueries, FireObject, UserEvent
4455
navigate: (elementOrPath: Element | string, basePath?: string) => Promise<boolean>;
4556
}
4657

47-
export interface RenderOptions<C, Q extends Queries = typeof queries> {
58+
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
4859
/**
4960
* @description
5061
* Will call detectChanges when the component is compiled
@@ -146,7 +157,7 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
146157
* }
147158
* })
148159
*/
149-
componentProperties?: Partial<C>;
160+
componentProperties?: Partial<ComponentType>;
150161
/**
151162
* @description
152163
* A collection of providers to inject dependencies of the component.
@@ -180,19 +191,6 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
180191
* })
181192
*/
182193
queries?: Q;
183-
/**
184-
* @description
185-
* An Angular component to wrap the component in.
186-
*
187-
* @default
188-
* `WrapperComponent`, an empty component that strips the `ng-version` attribute
189-
*
190-
* @example
191-
* const component = await render(AppComponent, {
192-
* wrapper: CustomWrapperComponent
193-
* })
194-
*/
195-
wrapper?: Type<any>;
196194
/**
197195
* @description
198196
* Exclude the component to be automatically be added as a declaration.
@@ -208,6 +206,7 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
208206
* })
209207
*/
210208
excludeComponentDeclaration?: boolean;
209+
211210
/**
212211
* @description
213212
* The route configuration to set up the router service via `RouterTestingModule.withRoutes`.
@@ -231,3 +230,34 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
231230
*/
232231
routes?: Routes;
233232
}
233+
234+
export interface RenderDirectiveOptions<DirectiveType, WrapperType, Q extends Queries = typeof queries>
235+
extends RenderComponentOptions<DirectiveType, Q> {
236+
/**
237+
* @description
238+
* The template to render the directive.
239+
* This template will override the template from the WrapperComponent.
240+
*
241+
* @example
242+
* const component = await render(SpoilerDirective, {
243+
* template: `<div spoiler message='SPOILER'></div>`
244+
* })
245+
*/
246+
template: string;
247+
/**
248+
* @description
249+
* An Angular component to wrap the component in.
250+
* The template will be overridden with the `template` option.
251+
*
252+
* @default
253+
* `WrapperComponent`, an empty component that strips the `ng-version` attribute
254+
*
255+
* @example
256+
* const component = await render(SpoilerDirective, {
257+
* template: `<div spoiler message='SPOILER'></div>`
258+
* wrapper: CustomWrapperComponent
259+
* })
260+
*/
261+
wrapper?: Type<WrapperType>;
262+
componentProperties?: Partial<any>;
263+
}

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

+61-90
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
1-
import { Component, DebugElement, ElementRef, OnInit, Type, NgZone } from '@angular/core';
1+
import { Component, ElementRef, OnInit, Type, NgZone } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
55
import { Router } from '@angular/router';
66
import { RouterTestingModule } from '@angular/router/testing';
77
import { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom';
8-
import { RenderOptions, RenderResult } from './models';
8+
import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models';
99
import { createSelectOptions, createType } from './user-events';
1010

1111
@Component({ selector: 'wrapper-component', template: '' })
1212
class WrapperComponent implements OnInit {
13-
constructor(private elemtRef: ElementRef) {}
13+
constructor(private elementRef: ElementRef) {}
1414

1515
ngOnInit() {
16-
this.elemtRef.nativeElement.removeAttribute('ng-version');
16+
this.elementRef.nativeElement.removeAttribute('ng-version');
1717
}
1818
}
1919

20-
export async function render<T>(template: string, renderOptions: RenderOptions<T>): Promise<RenderResult>;
21-
export async function render<T>(component: Type<T>, renderOptions?: RenderOptions<T>): Promise<RenderResult>;
22-
export async function render<T>(
23-
templateOrComponent: string | Type<T>,
24-
renderOptions: RenderOptions<T> = {},
25-
): Promise<RenderResult> {
20+
export async function render<ComponentType>(
21+
component: Type<ComponentType>,
22+
renderOptions?: RenderComponentOptions<ComponentType>,
23+
): Promise<RenderResult<ComponentType, ComponentType>>;
24+
export async function render<DirectiveType, WrapperType = WrapperComponent>(
25+
component: Type<DirectiveType>,
26+
renderOptions?: RenderDirectiveOptions<DirectiveType, WrapperType>,
27+
): Promise<RenderResult<DirectiveType, WrapperType>>;
28+
29+
export async function render<SutType, WrapperType = SutType>(
30+
sut: Type<SutType>,
31+
renderOptions: RenderComponentOptions<SutType> | RenderDirectiveOptions<SutType, WrapperType> = {},
32+
): Promise<RenderResult<SutType>> {
2633
const {
2734
detectChanges = true,
2835
declarations = [],
2936
imports = [],
3037
providers = [],
3138
schemas = [],
3239
queries,
40+
template,
3341
wrapper = WrapperComponent,
3442
componentProperties = {},
3543
componentProviders = [],
3644
excludeComponentDeclaration = false,
37-
routes,
38-
} = renderOptions;
39-
40-
const isTemplate = typeof templateOrComponent === 'string';
41-
const componentDeclarations = declareComponents({
42-
templateOrComponent,
43-
wrapper,
44-
isTemplate,
45-
excludeComponentDeclaration,
46-
});
45+
routes
46+
} = renderOptions as RenderDirectiveOptions<SutType, WrapperType>;
4747

4848
TestBed.configureTestingModule({
49-
declarations: [...declarations, ...componentDeclarations],
50-
imports: addAutoImports({ imports, routes }),
49+
declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }),
50+
imports: addAutoImports({imports, routes}),
5151
providers: [...providers],
5252
schemas: [...schemas],
5353
});
@@ -61,9 +61,8 @@ export async function render<T>(
6161
});
6262
}
6363

64-
const fixture = isTemplate
65-
? createWrapperComponentFixture(templateOrComponent as string, { wrapper, componentProperties })
66-
: createComponentFixture(templateOrComponent as Type<T>, { componentProperties });
64+
const fixture = createComponentFixture(sut, { template, wrapper });
65+
setComponentProperties(fixture, { componentProperties });
6766

6867
await TestBed.compileComponents();
6968

@@ -93,12 +92,16 @@ export async function render<T>(
9392

9493
const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href');
9594

96-
await zone.run(() => router.navigate([basePath + href]));
95+
let result;
96+
await zone.run(() => result = router.navigate([basePath + href]));
9797
fixture.detectChanges();
98+
return result;
9899
}
100+
const debugElement = fixture.debugElement.query(By.directive(sut));
99101

100102
return {
101103
fixture,
104+
debugElement,
102105
container: fixture.nativeElement,
103106
debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)),
104107
detectChanges: () => fixture.detectChanges(),
@@ -107,86 +110,54 @@ export async function render<T>(
107110
type: createType(eventsWithDetectChanges),
108111
selectOptions: createSelectOptions(eventsWithDetectChanges),
109112
navigate,
110-
} as any;
113+
};
111114
}
112115

113-
/**
114-
* Creates the wrapper component and sets its the template to the to-be-tested component
115-
*/
116-
function createWrapperComponentFixture<T>(
117-
template: string,
118-
{
119-
wrapper,
120-
componentProperties,
121-
}: {
122-
wrapper: RenderOptions<T>['wrapper'];
123-
componentProperties: RenderOptions<T>['componentProperties'];
124-
},
125-
): ComponentFixture<any> {
126-
TestBed.overrideComponent(wrapper, {
127-
set: {
128-
template: template,
129-
},
130-
});
131-
132-
const fixture = TestBed.createComponent(wrapper);
133-
// get the component selector, e.g. <foo color="green"> and <foo> results in foo
134-
const componentSelector = template.match(/\<(.*?)\ /) || template.match(/\<(.*?)\>/);
135-
if (!componentSelector) {
136-
throw Error(`Template ${template} is not valid.`);
116+
function createComponentFixture<SutType>(
117+
component: Type<SutType>,
118+
{ template, wrapper }: Pick<RenderDirectiveOptions<SutType, any>, 'template' | 'wrapper'>,
119+
): ComponentFixture<SutType> {
120+
if (template) {
121+
TestBed.overrideTemplate(wrapper, template);
122+
return TestBed.createComponent(wrapper);
137123
}
138-
139-
const sut = fixture.debugElement.query(By.css(componentSelector[1]));
140-
setComponentProperties(sut, { componentProperties });
141-
return fixture;
124+
return TestBed.createComponent(component);
142125
}
143126

144-
/**
145-
* Creates the components and sets its properties
146-
*/
147-
function createComponentFixture<T>(
148-
component: Type<T>,
149-
{
150-
componentProperties = {},
151-
}: {
152-
componentProperties: RenderOptions<T>['componentProperties'];
153-
},
154-
): ComponentFixture<T> {
155-
const fixture = TestBed.createComponent(component);
156-
setComponentProperties(fixture, { componentProperties });
157-
return fixture;
158-
}
159-
160-
/**
161-
* Set the component properties
162-
*/
163-
function setComponentProperties<T>(
164-
fixture: ComponentFixture<T> | DebugElement,
165-
{
166-
componentProperties = {},
167-
}: {
168-
componentProperties: RenderOptions<T>['componentProperties'];
169-
},
127+
function setComponentProperties<SutType>(
128+
fixture: ComponentFixture<SutType>,
129+
{ componentProperties = {} }: Pick<RenderDirectiveOptions<SutType, any>, 'componentProperties'>,
170130
) {
171131
for (const key of Object.keys(componentProperties)) {
172132
fixture.componentInstance[key] = componentProperties[key];
173133
}
174134
return fixture;
175135
}
176136

177-
function declareComponents({ isTemplate, wrapper, excludeComponentDeclaration, templateOrComponent }) {
178-
if (isTemplate) {
179-
return [wrapper];
180-
}
137+
function addAutoDeclarations<SutType>(
138+
component: Type<SutType>,
139+
{
140+
declarations,
141+
excludeComponentDeclaration,
142+
template,
143+
wrapper,
144+
}: Pick<
145+
RenderDirectiveOptions<SutType, any>,
146+
'declarations' | 'excludeComponentDeclaration' | 'template' | 'wrapper'
147+
>,
148+
) {
149+
const wrappers = () => {
150+
return template ? [wrapper] : [];
151+
};
181152

182-
if (excludeComponentDeclaration) {
183-
return [];
184-
}
153+
const components = () => {
154+
return excludeComponentDeclaration ? [] : [component];
155+
};
185156

186-
return [templateOrComponent];
157+
return [...declarations, ...wrappers(), ...components()];
187158
}
188159

189-
function addAutoImports({ imports, routes }: Pick<RenderOptions<any>, 'imports' | 'routes'>) {
160+
function addAutoImports({ imports, routes }: Pick<RenderComponentOptions<any>, 'imports' | 'routes'>) {
190161
const animations = () => {
191162
const animationIsDefined =
192163
imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1;

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ class FixtureComponent {}
1212

1313
test('debug', async () => {
1414
jest.spyOn(console, 'log').mockImplementation(() => {});
15-
const { debug } = await render('<fixture></fixture>', {
16-
declarations: [FixtureComponent],
17-
});
15+
const { debug } = await render(FixtureComponent);
1816

1917
debug();
2018

@@ -24,9 +22,7 @@ test('debug', async () => {
2422

2523
test('debug allows to be called with an element', async () => {
2624
jest.spyOn(console, 'log').mockImplementation(() => {});
27-
const { debug, getByTestId } = await render('<fixture></fixture>', {
28-
declarations: [FixtureComponent],
29-
});
25+
const { debug, getByTestId } = await render(FixtureComponent);
3026
const btn = getByTestId('btn');
3127

3228
debug(btn);

0 commit comments

Comments
 (0)