diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 4689937b..541bfc59 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -1,4 +1,4 @@ -import { Component, Type, NgZone } from '@angular/core'; +import { Component, Type, NgZone, SimpleChange, OnChanges, SimpleChanges } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -99,6 +99,12 @@ export async function render( } } + // Call ngOnChanges on initial render + if (hasOnChangesHook(fixture.componentInstance)) { + const changes = getChangesObj(null, fixture.componentInstance); + fixture.componentInstance.ngOnChanges(changes) + } + if (detectChangesOnRender) { detectChanges(); } @@ -113,7 +119,14 @@ export async function render( }, {} as FireFunction & FireObject); const rerender = (rerenderedProperties: Partial) => { + const changes = getChangesObj(fixture.componentInstance, rerenderedProperties); + setComponentProperties(fixture, { componentProperties: rerenderedProperties }); + + if (hasOnChangesHook(fixture.componentInstance)) { + fixture.componentInstance.ngOnChanges(changes); + } + detectChanges(); }; @@ -210,6 +223,22 @@ function setComponentProperties( return fixture; } +function hasOnChangesHook(componentInstance: SutType): componentInstance is SutType & OnChanges { + return 'ngOnChanges' in componentInstance + && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'; +}; + +function getChangesObj( + oldProps: Partial | null, + newProps: Partial +) { + const isFirstChange = oldProps === null; + return Object.keys(newProps).reduce((changes, key) => ({ + ...changes, + [key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange) + }), {}); +}; + function addAutoDeclarations( component: Type, { diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 250bcf18..a28e7cf6 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -1,4 +1,4 @@ -import { Component, NgModule } from '@angular/core'; +import { Component, NgModule, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestBed } from '@angular/core/testing'; import { render } from '../src/public_api'; @@ -71,3 +71,47 @@ describe('animationModule', () => { expect(() => TestBed.inject(NoopAnimationsModule)).toThrow(); }); }); + +@Component({ + selector: 'fixture', + template: ` {{ name }} `, +}) +class FixtureWithNgOnChangesComponent implements OnInit, OnChanges { + @Input() name = 'Sarah'; + @Input() nameInitialized?: (name: string) => void; + @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; + + ngOnInit() { + if (this.nameInitialized) { + this.nameInitialized(this.name); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.name && this.nameChanged) { + this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + } + } +} +describe('Angular component life-cycle hooks', () => { + test('will call ngOnInit on initial render', async () => { + const nameInitialized = jest.fn(); + const componentProperties = { nameInitialized }; + const component = await render(FixtureWithNgOnChangesComponent, { componentProperties }); + + component.getByText('Sarah'); + expect(nameInitialized).toBeCalledWith('Sarah'); + }); + + test('will call ngOnChanges on initial render before ngOnInit', async () => { + const nameInitialized = jest.fn(); + const nameChanged = jest.fn(); + const componentProperties = { nameInitialized, nameChanged }; + const component = await render(FixtureWithNgOnChangesComponent, { componentProperties }); + + component.getByText('Sarah'); + expect(nameChanged).toBeCalledWith('Sarah', true); + // expect `nameChanged` to be called before `nameInitialized` + expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); + }); +}); diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts index 9d654202..eb15220a 100644 --- a/projects/testing-library/tests/rerender.spec.ts +++ b/projects/testing-library/tests/rerender.spec.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { render } from '../src/public_api'; @Component({ @@ -20,3 +20,33 @@ test('will rerender the component with updated props', async () => { component.getByText(name); }); + +@Component({ + selector: 'fixture-onchanges', + template: ` {{ name }} `, +}) +class FixtureWithNgOnChangesComponent implements OnChanges { + @Input() name = 'Sarah'; + @Input() nameChanged: (name: string, isFirstChange: boolean) => void; + + ngOnChanges(changes: SimpleChanges) { + if (changes.name && this.nameChanged) { + this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + } + } +} + +test('will call ngOnChanges on rerender', async () => { + const nameChanged = jest.fn(); + const componentProperties = { nameChanged }; + const component = await render(FixtureWithNgOnChangesComponent, {componentProperties}); + component.getByText('Sarah'); + + const name = 'Mark'; + component.rerender({ + name, + }); + + component.getByText(name); + expect(nameChanged).toBeCalledWith(name, false); +})