Skip to content

Commit daa20ac

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

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
@@ -120,14 +120,38 @@ export async function render<SutType, WrapperType = SutType>(
120120

121121
await renderFixture(componentProperties, componentInputs, componentOutputs);
122122

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

133157
const changeInput = (changedInputProperties: Partial<SutType>) => {
@@ -357,6 +381,31 @@ function getChangesObj(oldProps: Record<string, any> | null, newProps: Record<st
357381
);
358382
}
359383

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

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)