Skip to content

Commit 14d36f1

Browse files
committed
feat: testing directives
1 parent 0f4022e commit 14d36f1

File tree

11 files changed

+285
-160
lines changed

11 files changed

+285
-160
lines changed

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

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { Type } from '@angular/core';
1+
import { Type, DebugElement } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
33
import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom';
44
import { UserEvents } from './user-events';
55

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

8-
export interface RenderResult extends RenderResultQueries, FireObject, UserEvents {
8+
export interface RenderResult<ComponentType, WrapperType = ComponentType>
9+
extends RenderResultQueries,
10+
FireObject,
11+
UserEvents {
912
/**
1013
* @description
1114
* The containing DOM node of your rendered Angular Component.
@@ -30,14 +33,22 @@ export interface RenderResult extends RenderResultQueries, FireObject, UserEvent
3033
detectChanges: () => void;
3134
/**
3235
* @description
33-
* The Angular `ComponentFixture` of the component.
36+
* The Angular `ComponentFixture` of the component or the wrapper.
37+
* If a template is provided, it will be the fixture of the wrapper.
3438
*
3539
* For more info see https://angular.io/api/core/testing/ComponentFixture
3640
*/
37-
fixture: ComponentFixture<any>;
41+
fixture: ComponentFixture<WrapperType>;
42+
/**
43+
* @description
44+
* The Angular `DebugElement` of the component.
45+
*
46+
* For more info see https://angular.io/api/core/DebugElement
47+
*/
48+
debugElement: DebugElement;
3849
}
3950

40-
export interface RenderOptions<C, Q extends Queries = typeof queries> {
51+
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
4152
/**
4253
* @description
4354
* Will call detectChanges when the component is compiled
@@ -139,7 +150,7 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
139150
* }
140151
* })
141152
*/
142-
componentProperties?: Partial<C>;
153+
componentProperties?: Partial<ComponentType>;
143154
/**
144155
* @description
145156
* A collection of providers to inject dependencies of the component.
@@ -175,30 +186,48 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
175186
queries?: Q;
176187
/**
177188
* @description
178-
* An Angular component to wrap the component in.
189+
* Exclude the component to be automatically be added as a declaration.
190+
* This is needed when the component is declared in an imported module.
179191
*
180192
* @default
181-
* `WrapperComponent`, an empty component that strips the `ng-version` attribute
193+
* false
182194
*
183195
* @example
184196
* const component = await render(AppComponent, {
185-
* wrapper: CustomWrapperComponent
197+
* imports: [AppModule], // a module that includes AppComponent
198+
* excludeComponentDeclaration: true
186199
* })
187200
*/
188-
wrapper?: Type<any>;
201+
excludeComponentDeclaration?: boolean;
202+
}
203+
204+
export interface RenderDirectiveOptions<DirectiveType, WrapperType, Q extends Queries = typeof queries>
205+
extends RenderComponentOptions<DirectiveType, Q> {
189206
/**
190207
* @description
191-
* Exclude the component to be automatically be added as a declaration.
192-
* This is needed when the component is declared in an imported module.
208+
* The template to render the directive.
209+
* This template will override the template from the WrapperComponent.
210+
*
211+
* @example
212+
* const component = await render(SpoilerDirective, {
213+
* template: `<div spoiler message='SPOILER'></div>`
214+
* })
215+
*/
216+
template: string;
217+
/**
218+
* @description
219+
* An Angular component to wrap the component in.
220+
* The template will be overridden with the `template` option.
193221
*
194222
* @default
195-
* false
223+
* `WrapperComponent`, an empty component that strips the `ng-version` attribute
196224
*
197225
* @example
198-
* const component = await render(AppComponent, {
199-
* imports: [AppModule], // a module that includes AppComponent
200-
* excludeComponentDeclaration: true
226+
* const component = await render(SpoilerDirective, {
227+
* template: `<div spoiler message='SPOILER'></div>`
228+
* wrapper: CustomWrapperComponent
201229
* })
202230
*/
203-
excludeComponentDeclaration?: boolean;
231+
wrapper?: Type<WrapperType>;
232+
componentProperties?: Partial<any>;
204233
}
Lines changed: 62 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,49 @@
1-
import { Component, DebugElement, ElementRef, OnInit, Type } from '@angular/core';
1+
import { Component, ElementRef, OnInit, Type } 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 { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom';
6-
import { RenderOptions, RenderResult } from './models';
6+
import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models';
77
import { createSelectOptions, createType } from './user-events';
88

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

1313
ngOnInit() {
14-
this.elemtRef.nativeElement.removeAttribute('ng-version');
14+
this.elementRef.nativeElement.removeAttribute('ng-version');
1515
}
1616
}
1717

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

4545
TestBed.configureTestingModule({
46-
declarations: [...declarations, ...componentDeclarations],
46+
declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }),
4747
imports: addAutoImports(imports),
4848
providers: [...providers],
4949
schemas: [...schemas],
@@ -58,9 +58,8 @@ export async function render<T>(
5858
});
5959
}
6060

61-
const fixture = isTemplate
62-
? createWrapperComponentFixture(templateOrComponent as string, { wrapper, componentProperties })
63-
: createComponentFixture(templateOrComponent as Type<T>, { componentProperties });
61+
const fixture = createComponentFixture(sut, { template, wrapper });
62+
setComponentProperties(fixture, { componentProperties });
6463

6564
await TestBed.compileComponents();
6665

@@ -80,98 +79,71 @@ export async function render<T>(
8079
{} as FireFunction & FireObject,
8180
);
8281

82+
const debugElement = fixture.debugElement.query(By.directive(sut));
83+
8384
return {
8485
fixture,
86+
debugElement,
8587
container: fixture.nativeElement,
8688
debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)),
8789
detectChanges: () => fixture.detectChanges(),
8890
...getQueriesForElement(fixture.nativeElement, queries),
8991
...eventsWithDetectChanges,
9092
type: createType(eventsWithDetectChanges),
9193
selectOptions: createSelectOptions(eventsWithDetectChanges),
92-
} as any;
94+
};
9395
}
9496

95-
/**
96-
* Creates the wrapper component and sets its the template to the to-be-tested component
97-
*/
98-
function createWrapperComponentFixture<T>(
99-
template: string,
100-
{
101-
wrapper,
102-
componentProperties,
103-
}: {
104-
wrapper: RenderOptions<T>['wrapper'];
105-
componentProperties: RenderOptions<T>['componentProperties'];
106-
},
107-
): ComponentFixture<any> {
108-
TestBed.overrideComponent(wrapper, {
109-
set: {
110-
template: template,
111-
},
112-
});
113-
114-
const fixture = TestBed.createComponent(wrapper);
115-
// get the component selector, e.g. <foo color="green"> and <foo> results in foo
116-
const componentSelector = template.match(/\<(.*?)\ /) || template.match(/\<(.*?)\>/);
117-
if (!componentSelector) {
118-
throw Error(`Template ${template} is not valid.`);
97+
function createComponentFixture<SutType>(
98+
component: Type<SutType>,
99+
{ template, wrapper }: Pick<RenderDirectiveOptions<SutType, any>, 'template' | 'wrapper'>,
100+
): ComponentFixture<SutType> {
101+
if (template) {
102+
TestBed.overrideTemplate(wrapper, template);
103+
return TestBed.createComponent(wrapper);
119104
}
120-
121-
const sut = fixture.debugElement.query(By.css(componentSelector[1]));
122-
setComponentProperties(sut, { componentProperties });
123-
return fixture;
124-
}
125-
126-
/**
127-
* Creates the components and sets its properties
128-
*/
129-
function createComponentFixture<T>(
130-
component: Type<T>,
131-
{
132-
componentProperties = {},
133-
}: {
134-
componentProperties: RenderOptions<T>['componentProperties'];
135-
},
136-
): ComponentFixture<T> {
137-
const fixture = TestBed.createComponent(component);
138-
setComponentProperties(fixture, { componentProperties });
139-
return fixture;
105+
return TestBed.createComponent(component);
140106
}
141107

142-
/**
143-
* Set the component properties
144-
*/
145-
function setComponentProperties<T>(
146-
fixture: ComponentFixture<T> | DebugElement,
147-
{
148-
componentProperties = {},
149-
}: {
150-
componentProperties: RenderOptions<T>['componentProperties'];
151-
},
108+
function setComponentProperties<SutType>(
109+
fixture: ComponentFixture<SutType>,
110+
{ componentProperties = {} }: Pick<RenderDirectiveOptions<SutType, any>, 'componentProperties'>,
152111
) {
153112
for (const key of Object.keys(componentProperties)) {
154113
fixture.componentInstance[key] = componentProperties[key];
155114
}
156115
return fixture;
157116
}
158117

159-
function declareComponents({ isTemplate, wrapper, excludeComponentDeclaration, templateOrComponent }) {
160-
if (isTemplate) {
161-
return [wrapper];
162-
}
118+
function addAutoDeclarations<SutType>(
119+
component: Type<SutType>,
120+
{
121+
declarations,
122+
excludeComponentDeclaration,
123+
template,
124+
wrapper,
125+
}: Pick<
126+
RenderDirectiveOptions<SutType, any>,
127+
'declarations' | 'excludeComponentDeclaration' | 'template' | 'wrapper'
128+
>,
129+
) {
130+
const wrappers = () => {
131+
return template ? [wrapper] : [];
132+
};
163133

164-
if (excludeComponentDeclaration) {
165-
return [];
166-
}
134+
const components = () => {
135+
return excludeComponentDeclaration ? [] : [component];
136+
};
167137

168-
return [templateOrComponent];
138+
return [...declarations, ...wrappers(), ...components()];
169139
}
170140

171141
function addAutoImports(imports: any[]) {
172-
if (imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1) {
173-
return imports;
174-
}
142+
const animations = () => {
143+
const animationIsDefined =
144+
imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1;
145+
return animationIsDefined ? [] : [NoopAnimationsModule];
146+
};
175147

176-
return [...imports, NoopAnimationsModule];
148+
return [...imports, ...animations()];
177149
}

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

Lines changed: 2 additions & 6 deletions
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)