From 458a846fcd213334e7154c88a6f0becfb3219922 Mon Sep 17 00:00:00 2001 From: Zachary Williams <ZachJW34@gmail.com> Date: Tue, 2 Aug 2022 17:42:18 -0500 Subject: [PATCH 1/5] feat: support angular template in ct (#23023) Co-authored-by: Jordan <jordan@jpdesigning.com> --- npm/angular/src/mount.ts | 163 +++++++++++++----- npm/angular/tsconfig.json | 1 + .../cypress/e2e/angular.cy.ts | 2 +- .../app/components/button-output.component.ts | 1 + .../angular/src/app/mount.cy.ts | 44 +++++ 5 files changed, 171 insertions(+), 40 deletions(-) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 15a41bdca9d7..d5ce5ae1a669 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 { Type } from '@angular/core' +import { Component, EventEmitter, Type } from '@angular/core' import { ComponentFixture, getTestBed, @@ -25,18 +25,46 @@ import { * providers, declarations, imports and even component @Inputs() * * - * @interface TestBedConfig + * @interface MountConfig * @see https://angular.io/api/core/testing/TestModuleMetadata */ -export interface TestBedConfig<T extends object> extends TestModuleMetadata { +export interface MountConfig<T> extends TestModuleMetadata { /** - * @memberof TestBedConfig + * @memberof MountConfig + * @description flag to automatically create a cy.spy() for every component @Output() property + * @example + * export class ButtonComponent { + * @Output clicked = new EventEmitter() + * } + * + * cy.mount(ButtonComponent, { autoSpyOutputs: true }) + * cy.get('@clickedSpy).should('have.been.called') + */ + autoSpyOutputs?: boolean + + /** + * @memberof MountConfig + * @description flag defaulted to true to automatically detect changes in your components + */ + autoDetectChanges?: boolean + /** + * @memberof MountConfig * @example * import { ButtonComponent } from 'button/button.component' * it('renders a button with Save text', () => { * cy.mount(ButtonComponent, { componentProperties: { text: 'Save' }}) * cy.get('button').contains('Save') * }) + * + * it('renders a button with a cy.spy() replacing EventEmitter', () => { + * cy.mount(ButtonComponent, { + * componentProperties: { + * clicked: cy.spy().as('mySpy) + * } + * }) + * cy.get('button').click() + * cy.get('@mySpy').should('have.been.called') + * }) */ componentProperties?: Partial<{ [P in keyof T]: T[P] }> } @@ -45,7 +73,7 @@ export interface TestBedConfig<T extends object> extends TestModuleMetadata { * Type that the `mount` function returns * @type MountResponse<T> */ -export type MountResponse<T extends object> = { +export type MountResponse<T> = { /** * Fixture for debugging and testing a component. * @@ -75,13 +103,13 @@ export type MountResponse<T extends object> = { * Bootstraps the TestModuleMetaData passed to the TestBed * * @param {Type<T>} component Angular component being mounted - * @param {TestBedConfig} config TestBed configuration passed into the mount function - * @returns {TestBedConfig} TestBedConfig + * @param {MountConfig} config TestBed configuration passed into the mount function + * @returns {MountConfig} MountConfig */ -function bootstrapModule<T extends object> ( +function bootstrapModule<T> ( component: Type<T>, - config: TestBedConfig<T>, -): TestBedConfig<T> { + config: MountConfig<T>, +): MountConfig<T> { const { componentProperties, ...testModuleMetaData } = config if (!testModuleMetaData.declarations) { @@ -104,14 +132,14 @@ function bootstrapModule<T extends object> ( /** * Initializes the TestBed * - * @param {Type<T>} component Angular component being mounted - * @param {TestBedConfig} config TestBed configuration passed into the mount function + * @param {Type<T> | string} component Angular component being mounted or its template + * @param {MountConfig} config TestBed configuration passed into the mount function * @returns {TestBed} TestBed */ -function initTestBed<T extends object> ( - component: Type<T>, - config: TestBedConfig<T>, -): TestBed { +function initTestBed<T> ( + component: Type<T> | string, + config: MountConfig<T>, +): { testBed: TestBed, componentFixture: Type<T> } { const { providers, ...configRest } = config const testBed: TestBed = getTestBed() @@ -126,52 +154,75 @@ function initTestBed<T extends object> ( }, ) + const componentFixture = createComponentFixture(component) as Type<T> + testBed.configureTestingModule({ - ...bootstrapModule(component, configRest), + ...bootstrapModule(componentFixture, configRest), }) if (providers != null) { - testBed.overrideComponent(component, { + testBed.overrideComponent(componentFixture, { add: { providers, }, }) } - return testBed + return { testBed, componentFixture } +} + +@Component({ selector: 'cy-wrapper-component', template: '' }) +class WrapperComponent { } + +/** + * Returns the Component if Type<T> or creates a WrapperComponent + * + * @param {Type<T> | string} component The component you want to create a fixture of + * @returns {Type<T> | WrapperComponent} + */ +function createComponentFixture<T> ( + component: Type<T> | string, +): Type<T | WrapperComponent> { + if (typeof component === 'string') { + TestBed.overrideTemplate(WrapperComponent, component) + + return WrapperComponent + } + + return component } /** * Creates the ComponentFixture * - * @param component Angular component being mounted - * @param testBed TestBed - * @param autoDetectChanges boolean flag defaulted to true that turns on change detection automatically + * @param {Type<T>} component Angular component being mounted + * @param {TestBed} testBed TestBed + * @returns {ComponentFixture<T>} ComponentFixture */ -function setupFixture<T extends object> ( +function setupFixture<T> ( component: Type<T>, testBed: TestBed, - autoDetectChanges: boolean, + config: MountConfig<T>, ): ComponentFixture<T> { const fixture = testBed.createComponent(component) fixture.whenStable().then(() => { - fixture.autoDetectChanges(autoDetectChanges) + fixture.autoDetectChanges(config.autoDetectChanges ?? true) }) return fixture } /** - * Gets the componentInstance and Object.assigns any componentProperties() passed in the TestBedConfig + * Gets the componentInstance and Object.assigns any componentProperties() passed in the MountConfig * - * @param {TestBedConfig} config TestBed configuration passed into the mount function + * @param {MountConfig} config TestBed configuration passed into the mount function * @param {ComponentFixture<T>} fixture Fixture for debugging and testing a component. * @returns {T} Component being mounted */ -function setupComponent<T extends object> ( - config: TestBedConfig<T>, +function setupComponent<T> ( + config: MountConfig<T>, fixture: ComponentFixture<T>, ): T { let component: T = fixture.componentInstance @@ -180,15 +231,24 @@ function setupComponent<T extends object> ( component = Object.assign(component, config.componentProperties) } + if (config.autoSpyOutputs) { + Object.keys(component).map((key: string, index: number, keys: string[]) => { + const property = component[key] + + if (property instanceof EventEmitter) { + component[key] = createOutputSpy(`${key}Spy`) + } + }) + } + return component } /** * Mounts an Angular component inside Cypress browser * - * @param {Type<T>} component imported from angular file - * @param {TestBedConfig<T>} config configuration used to configure the TestBed - * @param {boolean} autoDetectChanges boolean flag defaulted to true that turns on change detection automatically + * @param {Type<T> | string} component Angular component being mounted or its template + * @param {MountConfig<T>} config configuration used to configure the TestBed * @example * import { HelloWorldComponent } from 'hello-world/hello-world.component' * import { MyService } from 'services/my.service' @@ -201,15 +261,24 @@ function setupComponent<T extends object> ( * }) * cy.get('h1').contains('Hello World') * }) + * + * or + * + * it('can mount with template', () => { + * mount('<app-hello-world></app-hello-world>', { + * declarations: [HelloWorldComponent], + * providers: [MyService], + * imports: [SharedModule] + * }) + * }) * @returns Cypress.Chainable<MountResponse<T>> */ -export function mount<T extends object> ( - component: Type<T>, - config: TestBedConfig<T> = {}, - autoDetectChanges = true, +export function mount<T> ( + component: Type<T> | string, + config: MountConfig<T> = { }, ): Cypress.Chainable<MountResponse<T>> { - const testBed: TestBed = initTestBed(component, config) - const fixture = setupFixture(component, testBed, autoDetectChanges) + const { testBed, componentFixture } = initTestBed(component, config) + const fixture = setupFixture(componentFixture, testBed, config) const componentInstance = setupComponent(config, fixture) const mountResponse: MountResponse<T> = { @@ -218,11 +287,27 @@ export function mount<T extends object> ( component: componentInstance, } + const logMessage = typeof component === 'string' ? 'Component' : componentFixture.name + Cypress.log({ name: 'mount', - message: component.name, + message: logMessage, consoleProps: () => ({ result: mountResponse }), }) return cy.wrap(mountResponse, { log: false }) } + +/** + * Creates a new Event Emitter and then spies on it's `emit` method + * + * @param {string} alias name you want to use for your cy.spy() alias + * @returns EventEmitter<T> + */ +export const createOutputSpy = <T>(alias: string) => { + const emitter = new EventEmitter<T>() + + cy.spy(emitter, 'emit').as(alias) + + return emitter as any +} diff --git a/npm/angular/tsconfig.json b/npm/angular/tsconfig.json index d09e57c203ce..b21ac64dacea 100644 --- a/npm/angular/tsconfig.json +++ b/npm/angular/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "target": "es2020", "module": "es2020", "skipLibCheck": true, diff --git a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts index 7731aba900c4..bfbde413dbb4 100644 --- a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts @@ -75,7 +75,7 @@ for (const project of WEBPACK_REACT) { cy.contains('mount.cy.ts').click() cy.waitForSpecToFinish() - cy.get('.passed > .num').should('contain', 6) + cy.get('.passed > .num').should('contain', 10) }) }) } diff --git a/system-tests/project-fixtures/angular/src/app/components/button-output.component.ts b/system-tests/project-fixtures/angular/src/app/components/button-output.component.ts index e8228a5e7528..891dc8761a92 100644 --- a/system-tests/project-fixtures/angular/src/app/components/button-output.component.ts +++ b/system-tests/project-fixtures/angular/src/app/components/button-output.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Output } from "@angular/core"; @Component({ + selector: 'app-button-output', template: `<button (click)="clicked.emit(true)">Click Me</button>` }) export class ButtonOutputComponent { 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 f13c8e2ef981..a175b6356043 100644 --- a/system-tests/project-fixtures/angular/src/app/mount.cy.ts +++ b/system-tests/project-fixtures/angular/src/app/mount.cy.ts @@ -5,6 +5,8 @@ import { CounterService } from "./components/counter.service"; import { ChildComponent } from "./components/child.component"; import { WithDirectivesComponent } from "./components/with-directives.component"; import { ButtonOutputComponent } from "./components/button-output.component"; +import { createOutputSpy } from 'cypress/angular'; +import { EventEmitter } from '@angular/core'; describe("angular mount", () => { it("pushes CommonModule into component", () => { @@ -51,4 +53,46 @@ describe("angular mount", () => { cy.get('@mySpy').should('have.been.calledWith', true) }) }) + + it('can use a template string instead of Type<T> for component', () => { + cy.mount('<app-button-output (clicked)="login($event)"></app-button-output>', { + declarations: [ButtonOutputComponent], + componentProperties: { + login: cy.spy().as('myClickedSpy') + } + }) + cy.get('button').click() + cy.get('@myClickedSpy').should('have.been.calledWith', true) + }) + + it('can spy on EventEmitter for mount using template', () => { + cy.mount('<app-button-output (clicked)="handleClick.emit($event)"></app-button-output>', { + declarations: [ButtonOutputComponent], + componentProperties: { + handleClick: new EventEmitter() + } + }).then(({ component }) => { + cy.spy(component.handleClick, 'emit').as('handleClickSpy') + cy.get('button').click() + cy.get('@handleClickSpy').should('have.been.calledWith', true) + }) + }) + + it('can accept a createOutputSpy for an Output property', () => { + cy.mount(ButtonOutputComponent, { + componentProperties: { + clicked: createOutputSpy<boolean>('mySpy') + } + }) + cy.get('button').click(); + cy.get('@mySpy').should('have.been.calledWith', true) + }) + + it('can reference the autoSpyOutput alias on component @Outputs()', () => { + cy.mount(ButtonOutputComponent, { + autoSpyOutputs: true, + }) + cy.get('button').click() + cy.get('@clickedSpy').should('have.been.calledWith', true) + }) }); From 65cbac850b786f51c9866a6a8f8bb481f89499e0 Mon Sep 17 00:00:00 2001 From: Jordan <jordan@jpdesigning.com> Date: Thu, 4 Aug 2022 15:02:19 -0400 Subject: [PATCH 2/5] refactor(angular): handle mounting teardown before tests (#23098) Co-authored-by: Zachary Williams <zachjw34@gmail.com> --- npm/angular/src/mount.ts | 58 +++++++++---------- .../cypress/e2e/angular.cy.ts | 8 --- .../angular/src/app/mount.cy.ts | 10 ++++ system-tests/test/component_testing_spec.ts | 16 +++++ 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index d5ce5ae1a669..bcc5f9c10153 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -19,6 +19,9 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing' +import { + setupHooks, +} from '@cypress/mount-utils' /** * Additional module configurations needed while mounting the component, like @@ -82,14 +85,6 @@ export type MountResponse<T> = { */ fixture: ComponentFixture<T> - /** - * Configures and initializes environment and provides methods for creating components and services. - * - * @memberof MountResponse - * @see https://angular.io/api/core/testing/TestBed - */ - testBed: TestBed - /** * The instance of the root component class * @@ -134,41 +129,29 @@ function bootstrapModule<T> ( * * @param {Type<T> | string} component Angular component being mounted or its template * @param {MountConfig} config TestBed configuration passed into the mount function - * @returns {TestBed} TestBed + * @returns {Type<T>} componentFixture */ function initTestBed<T> ( component: Type<T> | string, config: MountConfig<T>, -): { testBed: TestBed, componentFixture: Type<T> } { +): Type<T> { const { providers, ...configRest } = config - const testBed: TestBed = getTestBed() - - testBed.resetTestEnvironment() - - testBed.initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting(), - { - teardown: { destroyAfterEach: false }, - }, - ) - const componentFixture = createComponentFixture(component) as Type<T> - testBed.configureTestingModule({ + TestBed.configureTestingModule({ ...bootstrapModule(componentFixture, configRest), }) if (providers != null) { - testBed.overrideComponent(componentFixture, { + TestBed.overrideComponent(componentFixture, { add: { providers, }, }) } - return { testBed, componentFixture } + return componentFixture } @Component({ selector: 'cy-wrapper-component', template: '' }) @@ -196,16 +179,15 @@ function createComponentFixture<T> ( * Creates the ComponentFixture * * @param {Type<T>} component Angular component being mounted - * @param {TestBed} testBed TestBed + * @param {MountConfig<T>} config MountConfig * @returns {ComponentFixture<T>} ComponentFixture */ function setupFixture<T> ( component: Type<T>, - testBed: TestBed, config: MountConfig<T>, ): ComponentFixture<T> { - const fixture = testBed.createComponent(component) + const fixture = TestBed.createComponent(component) fixture.whenStable().then(() => { fixture.autoDetectChanges(config.autoDetectChanges ?? true) @@ -277,12 +259,11 @@ export function mount<T> ( component: Type<T> | string, config: MountConfig<T> = { }, ): Cypress.Chainable<MountResponse<T>> { - const { testBed, componentFixture } = initTestBed(component, config) - const fixture = setupFixture(componentFixture, testBed, config) + const componentFixture = initTestBed(component, config) + const fixture = setupFixture(componentFixture, config) const componentInstance = setupComponent(config, fixture) const mountResponse: MountResponse<T> = { - testBed, fixture, component: componentInstance, } @@ -311,3 +292,18 @@ export const createOutputSpy = <T>(alias: string) => { return emitter as any } + +// Only needs to run once, we reset before each test +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + }, +) + +setupHooks(() => { + // Not public, we need to call this to remove the last component from the DOM + TestBed['tearDownTestingModule']() + TestBed.resetTestingModule() +}) diff --git a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts index bfbde413dbb4..312ac07c8161 100644 --- a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts @@ -69,13 +69,5 @@ for (const project of WEBPACK_REACT) { cy.waitForSpecToFinish() cy.get('.passed > .num').should('contain', 1) }) - - it('proves out mount API', () => { - cy.visitApp() - - cy.contains('mount.cy.ts').click() - cy.waitForSpecToFinish() - cy.get('.passed > .num').should('contain', 10) - }) }) } 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 a175b6356043..d573c805eebc 100644 --- a/system-tests/project-fixtures/angular/src/app/mount.cy.ts +++ b/system-tests/project-fixtures/angular/src/app/mount.cy.ts @@ -95,4 +95,14 @@ describe("angular mount", () => { cy.get('button').click() cy.get('@clickedSpy').should('have.been.calledWith', true) }) + + describe("teardown", () => { + beforeEach(() => { + cy.get("[id^=root]").should("not.exist"); + }); + + it("should mount", () => { + cy.mount(ButtonOutputComponent); + }); + }); }); diff --git a/system-tests/test/component_testing_spec.ts b/system-tests/test/component_testing_spec.ts index 2ca13e360723..92dd1d25f711 100644 --- a/system-tests/test/component_testing_spec.ts +++ b/system-tests/test/component_testing_spec.ts @@ -106,3 +106,19 @@ describe(`React major versions with Webpack`, function () { }) } }) + +const ANGULAR_MAJOR_VERSIONS = ['13', '14'] + +describe(`Angular CLI major versions`, () => { + systemTests.setup() + + for (const majorVersion of ANGULAR_MAJOR_VERSIONS) { + systemTests.it(`v${majorVersion} with mount tests`, { + project: `angular-${majorVersion}`, + spec: 'src/app/mount.cy.ts', + testingType: 'component', + browser: 'chrome', + expectedExitCode: 0, + }) + } +}) From 080cd05b7542b7bc8ee3255a79a186ca35a36e2a Mon Sep 17 00:00:00 2001 From: Jordan <jordan@jpdesigning.com> Date: Thu, 4 Aug 2022 17:24:05 -0400 Subject: [PATCH 3/5] feat(angular): add support for standalone components --- npm/angular/src/mount.ts | 7 +- .../app/components/projection.component.ts | 7 ++ .../angular/src/app/mount.cy.ts | 71 ++++++++++++++++++- .../app/components/standalone.component.cy.ts | 21 ++++++ .../app/components/standalone.component.ts | 10 +++ system-tests/test/component_testing_spec.ts | 4 +- 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 system-tests/project-fixtures/angular/src/app/components/projection.component.ts create mode 100644 system-tests/projects/angular-14/src/app/components/standalone.component.cy.ts create mode 100644 system-tests/projects/angular-14/src/app/components/standalone.component.ts diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index bcc5f9c10153..7fda61027c4a 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -115,7 +115,12 @@ function bootstrapModule<T> ( testModuleMetaData.imports = [] } - testModuleMetaData.declarations.push(component) + // check if the component is a standalone component + if ((component as any).ɵcmp.standalone) { + testModuleMetaData.imports.push(component) + } else { + testModuleMetaData.declarations.push(component) + } if (!testModuleMetaData.imports.includes(CommonModule)) { testModuleMetaData.imports.push(CommonModule) diff --git a/system-tests/project-fixtures/angular/src/app/components/projection.component.ts b/system-tests/project-fixtures/angular/src/app/components/projection.component.ts new file mode 100644 index 000000000000..69956687d92b --- /dev/null +++ b/system-tests/project-fixtures/angular/src/app/components/projection.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-projection', + template: `<h3><ng-content></ng-content></h3>` +}) +export class ProjectionComponent {} \ 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 d573c805eebc..d41db5dc8c15 100644 --- a/system-tests/project-fixtures/angular/src/app/mount.cy.ts +++ b/system-tests/project-fixtures/angular/src/app/mount.cy.ts @@ -6,7 +6,13 @@ import { ChildComponent } from "./components/child.component"; import { WithDirectivesComponent } from "./components/with-directives.component"; import { ButtonOutputComponent } from "./components/button-output.component"; import { createOutputSpy } from 'cypress/angular'; -import { EventEmitter } from '@angular/core'; +import { EventEmitter, Component } from '@angular/core'; +import { ProjectionComponent } from "./components/projection.component"; + +@Component({ + template: `<app-projection>Hello World</app-projection>` +}) +class WrapperComponent {} describe("angular mount", () => { it("pushes CommonModule into component", () => { @@ -46,6 +52,31 @@ describe("angular mount", () => { }); }); + it('can bind the spy to the componentProperties bypassing types', () => { + cy.mount(ButtonOutputComponent, { + componentProperties: { + clicked: { + emit: cy.spy().as('onClickedSpy') + } as any + } + }) + cy.get('button').click() + cy.get('@onClickedSpy').should('have.been.calledWith', true) + }) + + it('can bind the spy to the componentProperties bypassing types using template', () => { + cy.mount('<app-button-output (clicked)="clicked.emit($event)"></app-button-output>', { + declarations: [ButtonOutputComponent], + componentProperties: { + clicked: { + emit: cy.spy().as('onClickedSpy') + } as any + } + }) + cy.get('button').click() + cy.get('@onClickedSpy').should('have.been.calledWith', true) + }) + it('can spy on EventEmitters', () => { cy.mount(ButtonOutputComponent).then(({ component }) => { cy.spy(component.clicked, 'emit').as('mySpy') @@ -88,6 +119,17 @@ describe("angular mount", () => { cy.get('@mySpy').should('have.been.calledWith', true) }) + it('can accept a createOutputSpy for an Output property with a template', () => { + cy.mount('<app-button-output (click)="clicked.emit($event)"></app-button-output>', { + declarations: [ButtonOutputComponent], + componentProperties: { + clicked: createOutputSpy<boolean>('mySpy') + } + }) + cy.get('button').click() + cy.get('@mySpy').should('have.been.called') + }) + it('can reference the autoSpyOutput alias on component @Outputs()', () => { cy.mount(ButtonOutputComponent, { autoSpyOutputs: true, @@ -105,4 +147,31 @@ describe("angular mount", () => { cy.mount(ButtonOutputComponent); }); }); + it('can reference the autoSpyOutput alias on component @Outputs() with a template', () => { + cy.mount('<app-button-output (clicked)="clicked.emit($event)"></app-button-output>', { + declarations: [ButtonOutputComponent], + autoSpyOutputs: true, + componentProperties: { + clicked: new EventEmitter() + } + }) + cy.get('button').click() + cy.get('@clickedSpy').should('have.been.calledWith', true) + }) + + + + it('can handle content projection with a WrapperComponent', () => { + cy.mount(WrapperComponent, { + declarations: [ProjectionComponent] + }) + cy.get('h3').contains('Hello World') + }) + + it('can handle content projection using template', () => { + cy.mount('<app-projection>Hello World</app-projection>', { + declarations: [ProjectionComponent] + }) + cy.get('h3').contains('Hello World') + }) }); diff --git a/system-tests/projects/angular-14/src/app/components/standalone.component.cy.ts b/system-tests/projects/angular-14/src/app/components/standalone.component.cy.ts new file mode 100644 index 000000000000..74bf96cd41e6 --- /dev/null +++ b/system-tests/projects/angular-14/src/app/components/standalone.component.cy.ts @@ -0,0 +1,21 @@ +import { StandaloneComponent } from './standalone.component' + +describe('StandaloneComponent', () => { + it('can mount a standalone component', () => { + cy.mount(StandaloneComponent, { + componentProperties: { + name: 'Angular', + }, + }) + + cy.get('h1').contains('Hello Angular') + }) + + it('can mount a standalone component using template', () => { + cy.mount('<app-standalone name="Angular"></app-standalone>', { + imports: [StandaloneComponent], + }) + + cy.get('h1').contains('Hello Angular') + }) +}) diff --git a/system-tests/projects/angular-14/src/app/components/standalone.component.ts b/system-tests/projects/angular-14/src/app/components/standalone.component.ts new file mode 100644 index 000000000000..bf2355b493da --- /dev/null +++ b/system-tests/projects/angular-14/src/app/components/standalone.component.ts @@ -0,0 +1,10 @@ +import { Component, Input } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-standalone', + template: `<h1>Hello {{ name }}</h1>`, +}) +export class StandaloneComponent { + @Input() name!: string +} diff --git a/system-tests/test/component_testing_spec.ts b/system-tests/test/component_testing_spec.ts index 92dd1d25f711..f66340f65e6f 100644 --- a/system-tests/test/component_testing_spec.ts +++ b/system-tests/test/component_testing_spec.ts @@ -113,9 +113,11 @@ describe(`Angular CLI major versions`, () => { systemTests.setup() for (const majorVersion of ANGULAR_MAJOR_VERSIONS) { + const spec = `${majorVersion === '14' ? 'src/app/components/standalone.component.cy.ts,src/app/mount.cy.ts' : 'src/app/mount.cy.ts'}` + systemTests.it(`v${majorVersion} with mount tests`, { project: `angular-${majorVersion}`, - spec: 'src/app/mount.cy.ts', + spec, testingType: 'component', browser: 'chrome', expectedExitCode: 0, From 0d627fd35f86f4c1f341bc0e343c533804c05c97 Mon Sep 17 00:00:00 2001 From: Jordan <jordan@jpdesigning.com> Date: Thu, 4 Aug 2022 17:49:09 -0400 Subject: [PATCH 4/5] fix(angular): use rollup to handle including mount-utils --- npm/angular/package.json | 4 ++- npm/angular/rollup.config.js | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 npm/angular/rollup.config.js diff --git a/npm/angular/package.json b/npm/angular/package.json index bd35918c57ce..9ebd6c020c74 100644 --- a/npm/angular/package.json +++ b/npm/angular/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "prebuild": "rimraf dist", - "build": "tsc || echo 'built, with type errors'", + "build": "rollup -c rollup.config.js", "postbuild": "node ../../scripts/sync-exported-npm-with-cli.js", "build-prod": "yarn build", "check-ts": "tsc --noEmit" @@ -15,6 +15,8 @@ "@angular/common": "^14.0.6", "@angular/core": "^14.0.6", "@angular/platform-browser-dynamic": "^14.0.6", + "@rollup/plugin-node-resolve": "^11.1.1", + "rollup-plugin-typescript2": "^0.29.0", "typescript": "~4.2.3", "zone.js": "~0.11.4" }, diff --git a/npm/angular/rollup.config.js b/npm/angular/rollup.config.js new file mode 100644 index 000000000000..975310fc9131 --- /dev/null +++ b/npm/angular/rollup.config.js @@ -0,0 +1,61 @@ +import ts from 'rollup-plugin-typescript2' +import resolve from '@rollup/plugin-node-resolve' + +import pkg from './package.json' + +const banner = ` +/** + * ${pkg.name} v${pkg.version} + * (c) ${new Date().getFullYear()} Cypress.io + * Released under the MIT License + */ +` + +function createEntry () { + const input = 'src/index.ts' + const format = 'es' + + const config = { + input, + external: [ + '@angular/core', + '@angular/core/testing', + '@angular/common', + '@angular/platform-browser-dynamic/testing', + 'zone.js', + 'zone.js/testing', + ], + plugins: [ + resolve(), + ], + output: { + banner, + name: 'CypressAngular', + file: pkg.module, + format, + exports: 'auto', + }, + } + + console.log(`Building ${format}: ${config.output.file}`) + + config.plugins.push( + ts({ + check: true, + tsconfigOverride: { + compilerOptions: { + declaration: true, + target: 'es6', // not sure what this should be? + module: 'esnext', + }, + exclude: [], + }, + }), + ) + + return config +} + +export default [ + createEntry(), +] From bf83bc50d7b53b1d3600bbbcd1e3a70055529540 Mon Sep 17 00:00:00 2001 From: Jordan <jordan@jpdesigning.com> Date: Fri, 5 Aug 2022 09:00:00 -0400 Subject: [PATCH 5/5] chore(angular): change map to forEach --- npm/angular/src/mount.ts | 2 +- .../angular/src/app/mount.cy.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 7fda61027c4a..35b9c2f6917c 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -219,7 +219,7 @@ function setupComponent<T> ( } if (config.autoSpyOutputs) { - Object.keys(component).map((key: string, index: number, keys: string[]) => { + Object.keys(component).forEach((key: string, index: number, keys: string[]) => { const property = component[key] if (property instanceof EventEmitter) { 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 d41db5dc8c15..57ecc3c41c3e 100644 --- a/system-tests/project-fixtures/angular/src/app/mount.cy.ts +++ b/system-tests/project-fixtures/angular/src/app/mount.cy.ts @@ -138,15 +138,7 @@ describe("angular mount", () => { cy.get('@clickedSpy').should('have.been.calledWith', true) }) - describe("teardown", () => { - beforeEach(() => { - cy.get("[id^=root]").should("not.exist"); - }); - - it("should mount", () => { - cy.mount(ButtonOutputComponent); - }); - }); + it('can reference the autoSpyOutput alias on component @Outputs() with a template', () => { cy.mount('<app-button-output (clicked)="clicked.emit($event)"></app-button-output>', { declarations: [ButtonOutputComponent], @@ -158,20 +150,28 @@ describe("angular mount", () => { cy.get('button').click() cy.get('@clickedSpy').should('have.been.calledWith', true) }) - - - + it('can handle content projection with a WrapperComponent', () => { cy.mount(WrapperComponent, { declarations: [ProjectionComponent] }) cy.get('h3').contains('Hello World') }) - + it('can handle content projection using template', () => { cy.mount('<app-projection>Hello World</app-projection>', { declarations: [ProjectionComponent] }) cy.get('h3').contains('Hello World') }) + + describe("teardown", () => { + beforeEach(() => { + cy.get("[id^=root]").should("not.exist"); + }); + + it("should mount", () => { + cy.mount(ButtonOutputComponent); + }); + }); });