Skip to content

Commit

Permalink
feat: support ngOnChanges with correct simple change object within re…
Browse files Browse the repository at this point in the history
…render

ref #365
  • Loading branch information
shaman-apprentice committed Feb 23, 2023
1 parent dc4c22f commit e4acbbd
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 8 deletions.
2 changes: 1 addition & 1 deletion projects/testing-library/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
/**
* @description
* Re-render the same component with different properties.
* This creates a new instance of the component.
* Properties not passed in again are removed.
*/
rerender: (
properties?: Pick<
Expand Down
59 changes: 54 additions & 5 deletions projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,38 @@ export async function render<SutType, WrapperType = SutType>(

await renderFixture(componentProperties, componentInputs, componentOutputs);

let renderedPropKeys = Object.keys(componentProperties);
let renderedInputKeys = Object.keys(componentInputs);
let renderedOutputKeys = Object.keys(componentOutputs);
const rerender = async (
properties?: Pick<RenderTemplateOptions<SutType>, 'componentProperties' | 'componentInputs' | 'componentOutputs'>,
) => {
await renderFixture(
properties?.componentProperties ?? {},
properties?.componentInputs ?? {},
properties?.componentOutputs ?? {},
);
const newComponentInputs = properties?.componentInputs ?? {};
for (const inputKey of renderedInputKeys) {
if (!Object.prototype.hasOwnProperty.call(newComponentInputs, inputKey)) {
delete (fixture.componentInstance as any)[inputKey];
}
}
setComponentInputs(fixture, newComponentInputs);
renderedInputKeys = Object.keys(newComponentInputs);

const newComponentOutputs = properties?.componentOutputs ?? {};
for (const outputKey of renderedOutputKeys) {
if (!Object.prototype.hasOwnProperty.call(newComponentOutputs, outputKey)) {
delete (fixture.componentInstance as any)[outputKey];
}
}
setComponentOutputs(fixture, newComponentOutputs);
renderedOutputKeys = Object.keys(newComponentOutputs);

const newComponentProps = properties?.componentProperties ?? {};
const changes = updateProps(fixture, renderedPropKeys, newComponentProps);
if (hasOnChangesHook(fixture.componentInstance)) {
fixture.componentInstance.ngOnChanges(changes);
}
renderedPropKeys = Object.keys(newComponentProps);

fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();
};

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

function updateProps<SutType>(
fixture: ComponentFixture<SutType>,
prevRenderedPropsKeys: string[],
newProps: Record<string, any>,
) {
const componentInstance = fixture.componentInstance as Record<string, any>;
const simpleChanges: SimpleChanges = {};

for (const key of prevRenderedPropsKeys) {
if (!Object.prototype.hasOwnProperty.call(newProps, key)) {
simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false);
delete componentInstance[key];
}
}

for (const [key, value] of Object.entries(newProps)) {
if (value !== componentInstance[key]) {
simpleChanges[key] = new SimpleChange(componentInstance[key], value, false);
}
}
setComponentProperties(fixture, newProps);

return simpleChanges;
}

function addAutoDeclarations<SutType>(
sut: Type<SutType> | string,
{
Expand Down
28 changes: 26 additions & 2 deletions projects/testing-library/tests/rerender.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { render, screen } from '../src/public_api';

let ngOnChangesSpy: jest.Mock;
@Component({
selector: 'atl-fixture',
template: ` {{ firstName }} {{ lastName }} `,
})
class FixtureComponent {
class FixtureComponent implements OnChanges {
@Input() firstName = 'Sarah';
@Input() lastName?: string;
ngOnChanges(changes: SimpleChanges): void {
ngOnChangesSpy(changes);
}
}

beforeEach(() => {
ngOnChangesSpy = jest.fn();
});

test('rerenders the component with updated props', async () => {
const { rerender } = await render(FixtureComponent);
expect(screen.getByText('Sarah')).toBeInTheDocument();
Expand Down Expand Up @@ -54,6 +62,22 @@ test('rerenders the component with updated props and resets other props', async
const firstName2 = 'Chris';
await rerender({ componentProperties: { firstName: firstName2 } });

expect(screen.getByText(`${firstName2}`)).toBeInTheDocument();
expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument();
expect(screen.queryByText(`${firstName} ${lastName}`)).not.toBeInTheDocument();

expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
expect(rerenderedChanges).toEqual({
lastName: {
previousValue: 'Peeters',
currentValue: undefined,
firstChange: false,
},
firstName: {
previousValue: 'Mark',
currentValue: 'Chris',
firstChange: false,
},
});
});

0 comments on commit e4acbbd

Please sign in to comment.