Skip to content

Commit f0077e8

Browse files
feat: support ngOnChanges with correct simple change object within rerender
ref testing-library#365
1 parent 2006502 commit f0077e8

File tree

3 files changed

+87
-8
lines changed

3 files changed

+87
-8
lines changed

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ 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<
@@ -64,11 +64,17 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
6464
>,
6565
) => Promise<void>;
6666
/**
67+
* @deprecated
68+
* Use rerender instead and pass in all properties you don't want to change unchanged again.
69+
*
6770
* @description
6871
* Keeps the current fixture intact and invokes ngOnChanges with the updated properties.
6972
*/
7073
change: (changedProperties: Partial<ComponentType>) => void;
7174
/**
75+
* @deprecated
76+
* Use rerender instead and pass in all input properties you don't want to change unchanged again.
77+
*
7278
* @description
7379
* Keeps the current fixture intact, update the @Input properties and invoke ngOnChanges with the updated properties.
7480
*/

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

+54-5
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,38 @@ 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 (
127130
properties?: Pick<RenderTemplateOptions<SutType>, 'componentProperties' | 'componentInputs' | 'componentOutputs'>,
128131
) => {
129-
await renderFixture(
130-
properties?.componentProperties ?? {},
131-
properties?.componentInputs ?? {},
132-
properties?.componentOutputs ?? {},
133-
);
132+
const newComponentInputs = properties?.componentInputs ?? {};
133+
for (const inputKey of renderedInputKeys) {
134+
if (!Object.prototype.hasOwnProperty.call(newComponentInputs, inputKey)) {
135+
delete (fixture.componentInstance as any)[inputKey];
136+
}
137+
}
138+
setComponentInputs(fixture, newComponentInputs);
139+
renderedInputKeys = Object.keys(newComponentInputs);
140+
141+
const newComponentOutputs = properties?.componentOutputs ?? {};
142+
for (const outputKey of renderedOutputKeys) {
143+
if (!Object.prototype.hasOwnProperty.call(newComponentOutputs, outputKey)) {
144+
delete (fixture.componentInstance as any)[outputKey];
145+
}
146+
}
147+
setComponentOutputs(fixture, newComponentOutputs);
148+
renderedOutputKeys = Object.keys(newComponentOutputs);
149+
150+
const newComponentProps = properties?.componentProperties ?? {};
151+
const changes = updateProps(fixture, renderedPropKeys, newComponentProps);
152+
if (hasOnChangesHook(fixture.componentInstance)) {
153+
fixture.componentInstance.ngOnChanges(changes);
154+
}
155+
renderedPropKeys = Object.keys(newComponentProps);
156+
157+
fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();
134158
};
135159

136160
const changeInput = (changedInputProperties: Partial<SutType>) => {
@@ -360,6 +384,31 @@ function getChangesObj(oldProps: Record<string, any> | null, newProps: Record<st
360384
);
361385
}
362386

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

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

+26-2
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,22 @@ test('rerenders the component with updated props and resets other props', async
5462
const firstName2 = 'Chris';
5563
await rerender({ componentProperties: { firstName: firstName2 } });
5664

65+
expect(screen.getByText(`${firstName2}`)).toBeInTheDocument();
5766
expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument();
5867
expect(screen.queryByText(`${firstName} ${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+
});
5983
});

0 commit comments

Comments
 (0)