diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 17974c6..761141b 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -55,12 +55,12 @@ 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< RenderTemplateOptions, - 'componentProperties' | 'componentInputs' | 'componentOutputs' + 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender' >, ) => Promise; /** diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index d2cf3ad..2d492d6 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -123,14 +123,43 @@ 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'>, + properties?: Pick< + RenderTemplateOptions, + 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender' + >, ) => { - 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); + + if (properties?.detectChangesOnRender !== false) { + fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges(); + } }; const changeInput = (changedInputProperties: Partial) => { @@ -360,6 +389,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..a06beaf 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,33 @@ test('rerenders the component with updated props and resets other props', async const firstName2 = 'Chris'; await rerender({ componentProperties: { firstName: firstName2 } }); - expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument(); - expect(screen.queryByText(`${firstName} ${lastName}`)).not.toBeInTheDocument(); + expect(screen.getByText(firstName2)).toBeInTheDocument(); + expect(screen.queryByText(firstName)).not.toBeInTheDocument(); + expect(screen.queryByText(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, + }, + }); +}); + +test('change detection gets not called if `detectChangesOnRender` is set to false', async () => { + const { rerender } = await render(FixtureComponent); + expect(screen.getByText('Sarah')).toBeInTheDocument(); + + const firstName = 'Mark'; + await rerender({ componentInputs: { firstName }, detectChangesOnRender: false }); + + expect(screen.getByText('Sarah')).toBeInTheDocument(); + expect(screen.queryByText(firstName)).not.toBeInTheDocument(); });