Skip to content

Commit 83c323f

Browse files
feat: support ngOnChanges with correct simpleChange object within rerender (#366)
Closes #365
1 parent 70b918b commit 83c323f

File tree

3 files changed

+101
-12
lines changed

3 files changed

+101
-12
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
5555
/**
5656
* @description
5757
* Re-render the same component with different properties.
58-
* This creates a new instance of the component.
58+
* Properties not passed in again are removed.
5959
*/
6060
rerender: (
6161
properties?: Pick<
6262
RenderTemplateOptions<ComponentType>,
63-
'componentProperties' | 'componentInputs' | 'componentOutputs'
63+
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
6464
>,
6565
) => Promise<void>;
6666
/**

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

+60-6
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,43 @@ export async function render<SutType, WrapperType = SutType>(
123123

124124
await renderFixture(componentProperties, componentInputs, componentOutputs);
125125

126+
let renderedPropKeys = Object.keys(componentProperties);
127+
let renderedInputKeys = Object.keys(componentInputs);
128+
let renderedOutputKeys = Object.keys(componentOutputs);
126129
const rerender = async (
127-
properties?: Pick<RenderTemplateOptions<SutType>, 'componentProperties' | 'componentInputs' | 'componentOutputs'>,
130+
properties?: Pick<
131+
RenderTemplateOptions<SutType>,
132+
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
133+
>,
128134
) => {
129-
await renderFixture(
130-
properties?.componentProperties ?? {},
131-
properties?.componentInputs ?? {},
132-
properties?.componentOutputs ?? {},
133-
);
135+
const newComponentInputs = properties?.componentInputs ?? {};
136+
for (const inputKey of renderedInputKeys) {
137+
if (!Object.prototype.hasOwnProperty.call(newComponentInputs, inputKey)) {
138+
delete (fixture.componentInstance as any)[inputKey];
139+
}
140+
}
141+
setComponentInputs(fixture, newComponentInputs);
142+
renderedInputKeys = Object.keys(newComponentInputs);
143+
144+
const newComponentOutputs = properties?.componentOutputs ?? {};
145+
for (const outputKey of renderedOutputKeys) {
146+
if (!Object.prototype.hasOwnProperty.call(newComponentOutputs, outputKey)) {
147+
delete (fixture.componentInstance as any)[outputKey];
148+
}
149+
}
150+
setComponentOutputs(fixture, newComponentOutputs);
151+
renderedOutputKeys = Object.keys(newComponentOutputs);
152+
153+
const newComponentProps = properties?.componentProperties ?? {};
154+
const changes = updateProps(fixture, renderedPropKeys, newComponentProps);
155+
if (hasOnChangesHook(fixture.componentInstance)) {
156+
fixture.componentInstance.ngOnChanges(changes);
157+
}
158+
renderedPropKeys = Object.keys(newComponentProps);
159+
160+
if (properties?.detectChangesOnRender !== false) {
161+
fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();
162+
}
134163
};
135164

136165
const changeInput = (changedInputProperties: Partial<SutType>) => {
@@ -360,6 +389,31 @@ function getChangesObj(oldProps: Record<string, any> | null, newProps: Record<st
360389
);
361390
}
362391

392+
function updateProps<SutType>(
393+
fixture: ComponentFixture<SutType>,
394+
prevRenderedPropsKeys: string[],
395+
newProps: Record<string, any>,
396+
) {
397+
const componentInstance = fixture.componentInstance as Record<string, any>;
398+
const simpleChanges: SimpleChanges = {};
399+
400+
for (const key of prevRenderedPropsKeys) {
401+
if (!Object.prototype.hasOwnProperty.call(newProps, key)) {
402+
simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false);
403+
delete componentInstance[key];
404+
}
405+
}
406+
407+
for (const [key, value] of Object.entries(newProps)) {
408+
if (value !== componentInstance[key]) {
409+
simpleChanges[key] = new SimpleChange(componentInstance[key], value, false);
410+
}
411+
}
412+
setComponentProperties(fixture, newProps);
413+
414+
return simpleChanges;
415+
}
416+
363417
function addAutoDeclarations<SutType>(
364418
sut: Type<SutType> | string,
365419
{

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

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

4+
let ngOnChangesSpy: jest.Mock;
45
@Component({
56
selector: 'atl-fixture',
67
template: ` {{ firstName }} {{ lastName }} `,
78
})
8-
class FixtureComponent {
9+
class FixtureComponent implements OnChanges {
910
@Input() firstName = 'Sarah';
1011
@Input() lastName?: string;
12+
ngOnChanges(changes: SimpleChanges): void {
13+
ngOnChangesSpy(changes);
14+
}
1115
}
1216

17+
beforeEach(() => {
18+
ngOnChangesSpy = jest.fn();
19+
});
20+
1321
test('rerenders the component with updated props', async () => {
1422
const { rerender } = await render(FixtureComponent);
1523
expect(screen.getByText('Sarah')).toBeInTheDocument();
@@ -54,6 +62,33 @@ test('rerenders the component with updated props and resets other props', async
5462
const firstName2 = 'Chris';
5563
await rerender({ componentProperties: { firstName: firstName2 } });
5664

57-
expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument();
58-
expect(screen.queryByText(`${firstName} ${lastName}`)).not.toBeInTheDocument();
65+
expect(screen.getByText(firstName2)).toBeInTheDocument();
66+
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
67+
expect(screen.queryByText(lastName)).not.toBeInTheDocument();
68+
69+
expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
70+
const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
71+
expect(rerenderedChanges).toEqual({
72+
lastName: {
73+
previousValue: 'Peeters',
74+
currentValue: undefined,
75+
firstChange: false,
76+
},
77+
firstName: {
78+
previousValue: 'Mark',
79+
currentValue: 'Chris',
80+
firstChange: false,
81+
},
82+
});
83+
});
84+
85+
test('change detection gets not called if `detectChangesOnRender` is set to false', async () => {
86+
const { rerender } = await render(FixtureComponent);
87+
expect(screen.getByText('Sarah')).toBeInTheDocument();
88+
89+
const firstName = 'Mark';
90+
await rerender({ componentInputs: { firstName }, detectChangesOnRender: false });
91+
92+
expect(screen.getByText('Sarah')).toBeInTheDocument();
93+
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
5994
});

0 commit comments

Comments
 (0)