Skip to content

Commit de77901

Browse files
authored
feat: support component's ngOnChanges lifecycle hook (#110)
Closes #56
1 parent 1cce20d commit de77901

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Type, NgZone } from '@angular/core';
1+
import { Component, Type, NgZone, SimpleChange, OnChanges, SimpleChanges } 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';
@@ -99,6 +99,12 @@ export async function render<SutType, WrapperType = SutType>(
9999
}
100100
}
101101

102+
// Call ngOnChanges on initial render
103+
if (hasOnChangesHook(fixture.componentInstance)) {
104+
const changes = getChangesObj(null, fixture.componentInstance);
105+
fixture.componentInstance.ngOnChanges(changes)
106+
}
107+
102108
if (detectChangesOnRender) {
103109
detectChanges();
104110
}
@@ -113,7 +119,14 @@ export async function render<SutType, WrapperType = SutType>(
113119
}, {} as FireFunction & FireObject);
114120

115121
const rerender = (rerenderedProperties: Partial<SutType>) => {
122+
const changes = getChangesObj(fixture.componentInstance, rerenderedProperties);
123+
116124
setComponentProperties(fixture, { componentProperties: rerenderedProperties });
125+
126+
if (hasOnChangesHook(fixture.componentInstance)) {
127+
fixture.componentInstance.ngOnChanges(changes);
128+
}
129+
117130
detectChanges();
118131
};
119132

@@ -210,6 +223,22 @@ function setComponentProperties<SutType>(
210223
return fixture;
211224
}
212225

226+
function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
227+
return 'ngOnChanges' in componentInstance
228+
&& typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function';
229+
};
230+
231+
function getChangesObj<SutType>(
232+
oldProps: Partial<SutType> | null,
233+
newProps: Partial<SutType>
234+
) {
235+
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+
};
241+
213242
function addAutoDeclarations<SutType>(
214243
component: Type<SutType>,
215244
{

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, NgModule } from '@angular/core';
1+
import { Component, NgModule, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
22
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
33
import { TestBed } from '@angular/core/testing';
44
import { render } from '../src/public_api';
@@ -71,3 +71,47 @@ describe('animationModule', () => {
7171
expect(() => TestBed.inject(NoopAnimationsModule)).toThrow();
7272
});
7373
});
74+
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);
87+
}
88+
}
89+
90+
ngOnChanges(changes: SimpleChanges) {
91+
if (changes.name && this.nameChanged) {
92+
this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
93+
}
94+
}
95+
}
96+
describe('Angular component life-cycle hooks', () => {
97+
test('will call ngOnInit on initial render', async () => {
98+
const nameInitialized = jest.fn();
99+
const componentProperties = { nameInitialized };
100+
const component = await render(FixtureWithNgOnChangesComponent, { componentProperties });
101+
102+
component.getByText('Sarah');
103+
expect(nameInitialized).toBeCalledWith('Sarah');
104+
});
105+
106+
test('will call ngOnChanges on initial render before ngOnInit', async () => {
107+
const nameInitialized = jest.fn();
108+
const nameChanged = jest.fn();
109+
const componentProperties = { nameInitialized, nameChanged };
110+
const component = await render(FixtureWithNgOnChangesComponent, { componentProperties });
111+
112+
component.getByText('Sarah');
113+
expect(nameChanged).toBeCalledWith('Sarah', true);
114+
// expect `nameChanged` to be called before `nameInitialized`
115+
expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]);
116+
});
117+
});

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

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Input } from '@angular/core';
1+
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
22
import { render } from '../src/public_api';
33

44
@Component({
@@ -20,3 +20,33 @@ test('will rerender the component with updated props', async () => {
2020

2121
component.getByText(name);
2222
});
23+
24+
@Component({
25+
selector: 'fixture-onchanges',
26+
template: ` {{ name }} `,
27+
})
28+
class FixtureWithNgOnChangesComponent implements OnChanges {
29+
@Input() name = 'Sarah';
30+
@Input() nameChanged: (name: string, isFirstChange: boolean) => void;
31+
32+
ngOnChanges(changes: SimpleChanges) {
33+
if (changes.name && this.nameChanged) {
34+
this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
35+
}
36+
}
37+
}
38+
39+
test('will call ngOnChanges on rerender', async () => {
40+
const nameChanged = jest.fn();
41+
const componentProperties = { nameChanged };
42+
const component = await render(FixtureWithNgOnChangesComponent, {componentProperties});
43+
component.getByText('Sarah');
44+
45+
const name = 'Mark';
46+
component.rerender({
47+
name,
48+
});
49+
50+
component.getByText(name);
51+
expect(nameChanged).toBeCalledWith(name, false);
52+
})

0 commit comments

Comments
 (0)