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);
+    });
+  });
 });