From e4acbbdc5dde190195379ff2e428608893dfdbdd Mon Sep 17 00:00:00 2001 From: shaman-apprentice Date: Sat, 11 Feb 2023 22:24:50 +0100 Subject: [PATCH] feat: support ngOnChanges with correct simple change object within rerender ref #365 --- projects/testing-library/src/lib/models.ts | 2 +- .../src/lib/testing-library.ts | 59 +++++++++++++++++-- .../testing-library/tests/rerender.spec.ts | 28 ++++++++- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 17974c6..3580a0b 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -55,7 +55,7 @@ export interface RenderResult 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< diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index d2cf3ad..135d113 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -123,14 +123,38 @@ export async function render( 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, '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) => { @@ -360,6 +384,31 @@ function getChangesObj(oldProps: Record | null, newProps: Record( + fixture: ComponentFixture, + prevRenderedPropsKeys: string[], + newProps: Record, +) { + const componentInstance = fixture.componentInstance as Record; + 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( sut: Type | string, { diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts index d0ee43b..ca95fa4 100644 --- a/projects/testing-library/tests/rerender.spec.ts +++ b/projects/testing-library/tests/rerender.spec.ts @@ -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(); @@ -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, + }, + }); });