From 670d43830947c3ea93ef9fdc9c90932a817eb453 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 3 Oct 2022 18:17:46 -0400 Subject: [PATCH] fix(angular): call ngOnChanges after mount (#23596) * fix(angular): call ngOnChanges after mounting --- npm/angular/src/mount.ts | 24 +++++++-- .../src/app/components/lifecycle.component.ts | 23 ++++++++ .../angular/src/app/mount.cy.ts | 54 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 system-tests/project-fixtures/angular/src/app/components/lifecycle.component.ts diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 7f8d37863166..0743f3aa0bc8 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -8,7 +8,7 @@ window.Mocha['__zone_patch__'] = false import 'zone.js/testing' import { CommonModule } from '@angular/common' -import { Component, EventEmitter, Type } from '@angular/core' +import { Component, EventEmitter, SimpleChange, SimpleChanges, Type } from '@angular/core' import { ComponentFixture, getTestBed, @@ -215,10 +215,9 @@ function setupFixture ( * @param {ComponentFixture} fixture Fixture for debugging and testing a component. * @returns {T} Component being mounted */ -function setupComponent ( +function setupComponent ( config: MountConfig, - fixture: ComponentFixture, -): T { + fixture: ComponentFixture): T { let component: T = fixture.componentInstance if (config?.componentProperties) { @@ -235,6 +234,23 @@ function setupComponent ( }) } + // Manually call ngOnChanges when mounting components using the class syntax. + // This is necessary because we are assigning input values to the class directly + // on mount and therefore the ngOnChanges() lifecycle is not triggered. + if (component.ngOnChanges && config.componentProperties) { + const { componentProperties } = config + + const simpleChanges: SimpleChanges = Object.entries(componentProperties).reduce((acc, [key, value]) => { + acc[key] = new SimpleChange(null, value, true) + + return acc + }, {}) + + if (Object.keys(componentProperties).length > 0) { + component.ngOnChanges(simpleChanges) + } + } + return component } diff --git a/system-tests/project-fixtures/angular/src/app/components/lifecycle.component.ts b/system-tests/project-fixtures/angular/src/app/components/lifecycle.component.ts new file mode 100644 index 000000000000..7335946fb3dc --- /dev/null +++ b/system-tests/project-fixtures/angular/src/app/components/lifecycle.component.ts @@ -0,0 +1,23 @@ +import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core' + +@Component({ + selector: 'app-lifecycle', + template: `

Hi {{ name }}. ngOnInit fired: {{ ngOnInitFired }} and ngOnChanges fired: {{ ngOnChangesFired }} and conditionalName: {{ conditionalName }}

` +}) +export class LifecycleComponent implements OnInit, OnChanges { + @Input() name = '' + ngOnInitFired = false + ngOnChangesFired = false + conditionalName = false + + ngOnInit(): void { + this.ngOnInitFired = true + } + + ngOnChanges(changes: SimpleChanges): void { + this.ngOnChangesFired = true; + if (changes['name'].currentValue === 'CONDITIONAL NAME') { + this.conditionalName = true + } + } +} \ No newline at end of file diff --git a/system-tests/project-fixtures/angular/src/app/mount.cy.ts b/system-tests/project-fixtures/angular/src/app/mount.cy.ts index 57ecc3c41c3e..20abcf92ea7b 100644 --- a/system-tests/project-fixtures/angular/src/app/mount.cy.ts +++ b/system-tests/project-fixtures/angular/src/app/mount.cy.ts @@ -8,6 +8,7 @@ import { ButtonOutputComponent } from "./components/button-output.component"; import { createOutputSpy } from 'cypress/angular'; import { EventEmitter, Component } from '@angular/core'; import { ProjectionComponent } from "./components/projection.component"; +import { LifecycleComponent } from "./components/lifecycle.component"; @Component({ template: `Hello World` @@ -164,7 +165,60 @@ describe("angular mount", () => { }) cy.get('h3').contains('Hello World') }) + + it('handles ngOnChanges on mount', () => { + cy.mount(LifecycleComponent, { + componentProperties: { + name: 'Angular' + } + }) + + cy.get('p').should('have.text', 'Hi Angular. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: false') + }) + + it('handles ngOnChanges on mount with templates', () => { + cy.mount('', { + declarations: [LifecycleComponent], + componentProperties: { + name: 'Angular' + } + }) + + cy.get('p').should('have.text', 'Hi Angular. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: false') + }) + it('creates simpleChanges from componentProperties and calls ngOnChanges on Mount', () => { + cy.mount(LifecycleComponent, { + componentProperties: { + name: 'CONDITIONAL NAME' + } + }) + cy.get('p').should('have.text', 'Hi CONDITIONAL NAME. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: true') + }) + + it('creates simpleChanges from componentProperties and calls ngOnChanges on Mount with template', () => { + cy.mount('', { + declarations: [LifecycleComponent], + componentProperties: { + name: 'CONDITIONAL NAME' + } + }) + cy.get('p').should('have.text', 'Hi CONDITIONAL NAME. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: true') + }) + + it('ngOnChanges is not fired when no componentProperties given', () => { + cy.mount(LifecycleComponent) + cy.get('p').should('have.text', 'Hi . ngOnInit fired: true and ngOnChanges fired: false and conditionalName: false') + }) + + it('ngOnChanges is not fired when no componentProperties given with template', () => { + cy.mount('', { + declarations: [LifecycleComponent] + }) + cy.get('p').should('have.text', 'Hi . ngOnInit fired: true and ngOnChanges fired: false and conditionalName: false') + }) + + describe("teardown", () => { beforeEach(() => { cy.get("[id^=root]").should("not.exist");