Skip to content

Commit 5d2a25b

Browse files
authored
Enhance API for testing Inputs and Outputs (#30)
1 parent 9b9a5bd commit 5d2a25b

18 files changed

+453
-159
lines changed

docs/docs/api-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const dependencyService = getMock(MyServiceDependency);
3939
when(dependencyService.greet('John')).return('Hello John').once();
4040
```
4141

42-
### `renderComponent<T>(component: Type<T>, module: Type<any> | ModuleWithProviders<any>): Promise<Rendering<T, never>>`
42+
### `renderComponent<T, TBindings>(component: Type<T>, module: Type<any> | ModuleWithProviders<any>, options: RenderSettings<TBindings>): Promise<Rendering<T, never>>`
4343

4444
Shallow-renders a component, meaning that its children components are not rendered themselves, and any constructor dependency is mocked.
4545

docs/docs/best-practices.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ describe('SomeComponent', () => {
100100

101101
let rendering: Rendering<SomeComponent, unknown>;
102102

103-
beforeEach(async () => {
104-
rendering = await renderComponent(SomeComponent, AppModule);
105-
});
103+
beforeEach(fakeAsync(() => {
104+
rendering = renderComponent(SomeComponent, AppModule);
105+
}));
106106

107107
it('can click a button', () => {
108108
// Bad: This test is not readable
@@ -117,9 +117,9 @@ describe('SomeComponent', () => {
117117

118118
let page: Page;
119119

120-
beforeEach(async () => {
121-
page = new Page(await renderComponent(SomeComponent, AppModule));
122-
});
120+
beforeEach(fakeAsync(() => {
121+
page = new Page(renderComponent(SomeComponent, AppModule));
122+
}));
123123

124124
it('can click a button', () => {
125125
// Bad: This test is not readable

docs/docs/component-bindings.md

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,123 @@
11
---
22
id: component-bindings
3-
title: Testing Component Bindings
4-
sidebar_label: Testing Component Bindings
3+
title: Testing @Input and @Ouput
4+
sidebar_label: '@Input and @Output'
55
---
66

7-
TODO: Write proper documentation.
8-
- Use `getShallow` instead of `renderComponent` and bind values
9-
- Use page object `setBoundValues` in a fakeAsync context.
7+
In the previous section, we have seen how to mock services and child components in a component test.
8+
In this section we will cover how to inject data into the inputs of our components, and how to assert events emitted on its outputs.
109

10+
# Binding input data
11+
12+
Consider the component below:
13+
14+
```ts
15+
@Component({
16+
selector: 'greetings-component',
17+
templateUrl: `
18+
<span test-id="message">Hello {{name}}</span>
19+
`
20+
})
21+
export class GreetingsComponent {
22+
@Input()
23+
userName: string;
24+
}
25+
```
26+
27+
We would like to verify that the label gets updated with the user name.
28+
29+
As always, we create a page object to model the interactions with our component:
30+
31+
```ts
32+
class Page<T> extends BasePage<GreetingsComponent, T> {
33+
34+
get messageText(): string {
35+
return (this.rendering.find('[test-id=message]').nativeElement as HTMLElement).innerText
36+
}
37+
}
38+
```
39+
40+
Then, we use `setInputs` on the page object to update the input bindings and trigger a change detection cycle:
41+
42+
```ts
43+
it('greets the user', fakeAsync(() => {
44+
// Render the component with initial input values
45+
const page = new Page(renderComponent(GreetingsComponent, GreetingsModule, {
46+
inputs: {
47+
userName: 'John'
48+
}
49+
}));
50+
expect(page.messageText).toBe('Hello John');
51+
52+
// Set input values and test again
53+
page.setInputs({
54+
userName: 'Atul'
55+
});
56+
expect(page.messageText).toBe('Hello Atul');
57+
}));
58+
```
59+
60+
Setting input values after the component has been initialized can be useful when you want to test `onChanges` logic.
61+
62+
:::caution
63+
64+
You can only use `setInputs` for inputs that you have initialized in `renderComponent`. This is what the template type parameter `T` in `Page<T>` is for.
65+
66+
:::
67+
68+
# Test output events
69+
70+
Let's consider a keypad component which lets users type in a numeric code:
71+
72+
```ts
73+
@Component({
74+
selector: 'keypad-component',
75+
templateUrl: `
76+
<button *ngFor="number of numbers" [test-id]="number" (click)="onClick(number)></button>
77+
`
78+
})
79+
export class KeypadComponent {
80+
81+
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
82+
83+
code = '';
84+
85+
@Output()
86+
codeChange = new EventEmitter<string>();
87+
88+
onClick(n: number) {
89+
this.code += String(n);
90+
this.codeChange.emit(this.code);
91+
}
92+
}
93+
```
94+
95+
We would like to test the emitted output.
96+
97+
98+
```ts
99+
class Page<T> extends BasePage<KeypadComponent, T> {
100+
101+
getButton(n: number): HTMLButtonElement {
102+
return this.rendering.find(`[test-id=${n}]`).nativeElement;
103+
}
104+
}
105+
```
106+
107+
The page object exposes outputs utilities on `page.outputs`. Use `capture()` to create an array which receives all future events emitted on this output:
108+
109+
```ts
110+
it('lets the user type a combination', fakeAsync(() => {
111+
const page = new Page(renderComponent(KeypadComponent, KeypadModule));
112+
113+
const emittedEvents = page.outputs.codeChage.capture();
114+
115+
page.getButton(1).click();
116+
page.getButton(3).click();
117+
page.getButton(3).click();
118+
page.getButton(7).click();
119+
120+
expect(emittedEvents).toEqual(['1', '13', '133', '1337'])
121+
}));
122+
```
11123

12-
See example in [fancy-button.component.spec.ts](https://github.com/hmil/ng-vacuum/blob/080e8803df257338497dd7b07f663ce502b03aac/test/ng7/src/app/fancy-button.component.spec.ts)

docs/docs/component-test.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class MyComponent {
5151
}
5252
```
5353

54-
The sample component uses the global `console` object, injected using an [`InjectionToken`](https://angular.io/api/core/InjectionToken), to give this tutorial a bit more uumpf.
54+
The sample component uses the global `console` object, injected using an [`InjectionToken`](https://angular.io/api/core/InjectionToken), to give this tutorial a bit more depth.
5555

5656

5757
## Scaffolding
@@ -62,14 +62,15 @@ NgVcuum provides a utility class to simplify the setup of the page object
6262

6363
```ts
6464
import { BasePage, renderComponent } from 'ng-vacuum';
65+
import { fakeAsync } from '@angular/core/testing';
6566

6667
describe('MyComponent', () => {
6768

6869
let page: Page;
6970

70-
beforeEach(async () => {
71-
page = new Page(await renderComponent(MyComponent, AppModule));
72-
});
71+
beforeEach(fakeAsync(() => {
72+
page = new Page(renderComponent(MyComponent, AppModule));
73+
}));
7374
});
7475

7576
class Page extends BasePage<MyComponent> { }
@@ -137,10 +138,10 @@ We need to specify the behavior of `AuthService` _before_ rendering the componen
137138
Let's do this. We move the mock behavior to the `beforeEach` function:
138139

139140
```ts
140-
beforeEach(async () => {
141+
beforeEach(fakeAsync(() => {
141142
when(getMock(AuthService).authenticated).return(false); // Add here
142-
page = new Page(await renderComponent(AppComponent, AppModule));
143-
});
143+
page = new Page(renderComponent(AppComponent, AppModule));
144+
}));
144145

145146
it('lets user log in when not authenticated', fakeAsync(() => {
146147
// Remove from here
@@ -182,11 +183,11 @@ describe('AppComponent', () => {
182183

183184
let isAuthenticated: boolean;
184185

185-
beforeEach(async () => {
186+
beforeEach(fakeAsync(() => {
186187
isAuthenticated = false;
187188
when(getMock(AuthService).isAuthenticated()).useGetter(() => isAuthenticated);
188-
page = new Page(await renderComponent(AppComponent, AppModule));
189-
});
189+
page = new Page(renderComponent(AppComponent, AppModule));
190+
}));
190191

191192
// snip
192193
```
@@ -225,12 +226,12 @@ describe('AppComponent', () => {
225226
let page: Page;
226227
let isAuthenticated: boolean;
227228

228-
beforeEach(async () => {
229+
beforeEach(fakeAsync(() => {
229230
isAuthenticated = false;
230231
// Mock data required by the template
231232
when(getMock(AuthService).isAuthenticated()).useGetter(() => isAuthenticated);
232-
page = new Page(await renderComponent(AppComponent, AppModule));
233-
});
233+
page = new Page(renderComponent(AppComponent, AppModule));
234+
}));
234235

235236
it('lets user log in when not authenticated', fakeAsync(() => {
236237
// Ensure the tempalte is fully rendered

docs/sidebars.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
someSidebar: {
3-
"Getting Started": ['setup', 'service-test', 'component-test', 'component-bindings'],
4-
"Guides": ['mocking-techniques', 'best-practices', 'injection-tokens', 'ng-model'],
3+
"Getting Started": ['setup', 'service-test', 'component-test'],
4+
"Guides": ['component-bindings', 'mocking-techniques', 'best-practices', 'injection-tokens', 'ng-model'],
55
"Reference": ['api-reference'],
66
},
77
};

0 commit comments

Comments
 (0)