From 37c8483db26fc8fa9f15b3e3bba38fb8b104b45d Mon Sep 17 00:00:00 2001 From: Caleb Ukle Date: Tue, 8 Nov 2022 18:11:52 -0600 Subject: [PATCH] feat(testing): support cypress 11 (#13075) --- .../__snapshots__/component-test.spec.ts.snap | 55 +-- .../component-test/component-test.spec.ts | 19 +- .../files/__componentFileName__.cy.ts__tpl__ | 20 +- ...press-component-configuration.spec.ts.snap | 260 ++++++++------ packages/cypress/migrations.json | 6 + packages/cypress/package.json | 2 +- .../__snapshots__/cypress-11.spec.ts.snap | 178 ++++++++++ .../update-15-1-0/cypress-11.spec.ts | 325 ++++++++++++++++++ .../migrations/update-15-1-0/cypress-11.ts | 178 ++++++++++ packages/cypress/src/utils/versions.ts | 2 +- scripts/check-imports.js | 4 + 11 files changed, 913 insertions(+), 136 deletions(-) create mode 100644 packages/cypress/src/migrations/update-15-1-0/__snapshots__/cypress-11.spec.ts.snap create mode 100644 packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts create mode 100644 packages/cypress/src/migrations/update-15-1-0/cypress-11.ts diff --git a/packages/angular/src/generators/component-test/__snapshots__/component-test.spec.ts.snap b/packages/angular/src/generators/component-test/__snapshots__/component-test.spec.ts.snap index 37c4b596a2..a97fea5bfc 100644 --- a/packages/angular/src/generators/component-test/__snapshots__/component-test.spec.ts.snap +++ b/packages/angular/src/generators/component-test/__snapshots__/component-test.spec.ts.snap @@ -1,19 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Angular Cypress Component Test Generator should generate a component test 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { MyLibComponent } from './my-lib.component'; describe(MyLibComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(MyLibComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(MyLibComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -22,42 +25,51 @@ describe(MyLibComponent.name, () => { } }); }) + }) " `; exports[`Angular Cypress Component Test Generator should handle component w/o inputs 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { MyLibComponent } from './my-lib.component'; describe(MyLibComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(MyLibComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(MyLibComponent, config); + cy.mount(MyLibComponent,); }) + }) " `; exports[`Angular Cypress Component Test Generator should work with standalone components 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { MyLibComponent } from './my-lib.component'; describe(MyLibComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(MyLibComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(MyLibComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -66,6 +78,7 @@ describe(MyLibComponent.name, () => { } }); }) + }) " `; diff --git a/packages/angular/src/generators/component-test/component-test.spec.ts b/packages/angular/src/generators/component-test/component-test.spec.ts index fd0d071326..ca2194fd8d 100644 --- a/packages/angular/src/generators/component-test/component-test.spec.ts +++ b/packages/angular/src/generators/component-test/component-test.spec.ts @@ -177,19 +177,24 @@ export class MyLibComponent implements OnInit { await componentGenerator(tree, { name: 'my-lib', project: 'my-lib' }); - const expected = `import { MountConfig } from 'cypress/angular'; + const expected = `import { TestBed } from '@angular/core/testing'; import { MyLibComponent } from './my-lib.component'; describe(MyLibComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(MyLibComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(MyLibComponent, config); + cy.mount(MyLibComponent,); }) + }) `; diff --git a/packages/angular/src/generators/component-test/files/__componentFileName__.cy.ts__tpl__ b/packages/angular/src/generators/component-test/files/__componentFileName__.cy.ts__tpl__ index c3f66dc311..1aa79b4ca2 100644 --- a/packages/angular/src/generators/component-test/files/__componentFileName__.cy.ts__tpl__ +++ b/packages/angular/src/generators/component-test/files/__componentFileName__.cy.ts__tpl__ @@ -1,19 +1,23 @@ -import { MountConfig } from 'cypress/angular'; +import { TestBed } from '@angular/core/testing'; import { <%= componentName %> } from './<%= componentFileName %>'; describe(<%= componentName %>.name, () => { - const config: MountConfig<<%= componentName %>> = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(<%= componentName %>, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(<%= componentName %>,<% if(props.length > 0) { %> { - ...config, componentProperties: {<% for (let prop of props) { %> <%= prop.name %>: <%- prop.defaultValue %>,<% } %> } - }<% } else { %> config<% } %>); + }<% } %>); }) + }) diff --git a/packages/angular/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap b/packages/angular/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap index 8e631ef354..92e34a53d3 100644 --- a/packages/angular/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap +++ b/packages/angular/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap @@ -1,19 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Cypress Component Testing Configuration should work with complex component 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingOneComponent } from './something-one.component'; describe(SomethingOneComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingOneComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingOneComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -22,24 +25,28 @@ describe(SomethingOneComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with complex component 2`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingTwoComponent } from './something-two.component'; describe(SomethingTwoComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingTwoComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingTwoComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -48,24 +55,28 @@ describe(SomethingTwoComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with complex component 3`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingThreeComponent } from './something-three.component'; describe(SomethingThreeComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingThreeComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingThreeComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -74,24 +85,28 @@ describe(SomethingThreeComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with complex standalone component 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingOneComponent } from './something-one.component'; describe(SomethingOneComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingOneComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingOneComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -100,24 +115,28 @@ describe(SomethingOneComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with complex standalone component 2`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingTwoComponent } from './something-two.component'; describe(SomethingTwoComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingTwoComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingTwoComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -126,24 +145,28 @@ describe(SomethingTwoComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with complex standalone component 3`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingThreeComponent } from './something-three.component'; describe(SomethingThreeComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingThreeComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { cy.mount(SomethingThreeComponent, { - ...config, componentProperties: { type: 'button', style: 'default', @@ -152,150 +175,191 @@ describe(SomethingThreeComponent.name, () => { } }); }) + }) " `; exports[`Cypress Component Testing Configuration should work with secondary entry point libs 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { FancyButtonComponent } from './fancy-button.component'; describe(FancyButtonComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(FancyButtonComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(FancyButtonComponent, config); + cy.mount(FancyButtonComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with secondary entry point libs 2`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { StandaloneFancyButtonComponent } from './standalone-fancy-button.component'; describe(StandaloneFancyButtonComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(StandaloneFancyButtonComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(StandaloneFancyButtonComponent, config); + cy.mount(StandaloneFancyButtonComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with simple components 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingOneComponent } from './something-one.component'; describe(SomethingOneComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingOneComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingOneComponent, config); + cy.mount(SomethingOneComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with simple components 2`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingTwoComponent } from './something-two.component'; describe(SomethingTwoComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingTwoComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingTwoComponent, config); + cy.mount(SomethingTwoComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with simple components 3`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingThreeComponent } from './something-three.component'; describe(SomethingThreeComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingThreeComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingThreeComponent, config); + cy.mount(SomethingThreeComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with standalone component 1`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingOneComponent } from './something-one.component'; describe(SomethingOneComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingOneComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingOneComponent, config); + cy.mount(SomethingOneComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with standalone component 2`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingTwoComponent } from './something-two.component'; describe(SomethingTwoComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingTwoComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingTwoComponent, config); + cy.mount(SomethingTwoComponent,); }) + }) " `; exports[`Cypress Component Testing Configuration should work with standalone component 3`] = ` -"import { MountConfig } from 'cypress/angular'; +"import { TestBed } from '@angular/core/testing'; import { SomethingThreeComponent } from './something-three.component'; describe(SomethingThreeComponent.name, () => { - const config: MountConfig = { - declarations: [], - imports: [], - providers: [] - } + + beforeEach(() => { + TestBed.overrideComponent(SomethingThreeComponent, { + add: { + imports: [], + providers: [] + } + }) + }) it('renders', () => { - cy.mount(SomethingThreeComponent, config); + cy.mount(SomethingThreeComponent,); }) + }) " `; diff --git a/packages/cypress/migrations.json b/packages/cypress/migrations.json index 2021873f6e..b228458191 100644 --- a/packages/cypress/migrations.json +++ b/packages/cypress/migrations.json @@ -29,6 +29,12 @@ "version": "15.0.0-beta.4", "description": "Update to using cy.mount in the commands.ts file instead of importing mount for each component test file", "factory": "./src/migrations/update-15-0-0/update-cy-mount-usage" + }, + "update-to-cypress-11": { + "cli": "nx", + "version": "15.1.0-beta.0", + "description": "Update to Cypress v11. This migration will only update if the workspace is already on v10. https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/", + "factory": "./src/migrations/update-15-1-0/cypress-11" } }, "packageJsonUpdates": {} diff --git a/packages/cypress/package.json b/packages/cypress/package.json index dd1e1b90f8..cf54c657b8 100644 --- a/packages/cypress/package.json +++ b/packages/cypress/package.json @@ -53,7 +53,7 @@ "webpack-node-externals": "^3.0.0" }, "peerDependencies": { - "cypress": ">= 3 < 11" + "cypress": ">= 3 < 12" }, "peerDependenciesMeta": { "cypress": { diff --git a/packages/cypress/src/migrations/update-15-1-0/__snapshots__/cypress-11.spec.ts.snap b/packages/cypress/src/migrations/update-15-1-0/__snapshots__/cypress-11.spec.ts.snap new file mode 100644 index 0000000000..5ae6cea965 --- /dev/null +++ b/packages/cypress/src/migrations/update-15-1-0/__snapshots__/cypress-11.spec.ts.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cypress 11 Migration should migrate to v11 1`] = ` +" +it('calls the prop', () => { + cy.mount() + cy.contains('My component') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + }) +})" +`; + +exports[`Cypress 11 Migration should migrate to v11 2`] = ` +"/** TODO: mountHook is deprecate. +* Use a wrapper component instead. +* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed +* */ +import { mountHook, getContainerEl } from 'cypress/react18' +import ReactDom from 'react-dom' +import { useCounter } from ‘./useCounter’ + +it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) +}) + +describe('blah', () => { + + it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) + }) +}) + + +it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') + }) +})" +`; + +exports[`Cypress 11 Migration should migrate to v11 3`] = ` +"/** TODO: mountHook is deprecate. +* Use a wrapper component instead. +* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed +* */ +import { mountHook, getContainerEl } from 'cypress/react' +import ReactDom from 'react-dom' +import { useCounter } from ‘./useCounter’ + +it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) +}) + +describe('blah', () => { + + it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) + }) +}) + + +it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') + }) +})" +`; + +exports[`Cypress 11 Migration should migrate to v11 4`] = ` +"import {TestBed} from '@angular/core/testing; + + import { MyComponent } from './my.component'; + describe('MyComponent', () => { + const config = { + imports: [], + declarations: [], + providers: [{provide: 'foo', useValue: 'bar'}] + }; + it('direct usage', () => { + TestBed.overrideComponent(MyComponent, {add: { providers: config.providers}}); +cy.mount(MyComponent, config); + }); + it('spread usage', () => { + TestBed.overrideComponent(MyComponent, { add: { providers: [{provide: 'foo', useValue: 'bar'}] }}); +cy.mount(MyComponent, {...config, providers: undefined }); + }); + it('inlined usage', () => { + TestBed.overrideComponent(MyComponent, { add: { providers: [{provide: 'foo', useValue: 'bar'}] }}); +cy.mount(MyComponent, {imports: [], declarations: [], providers: undefined}); + }); + " +`; + +exports[`Cypress 11 Migration should migrate to v11 5`] = ` +"import { MountConfig } from 'cypress/angular'; + import { MyComponent } from './my.component'; + import {TestBed} from '@angular/core/testing'; + describe('MyComponent', () => { + const config: MountConfig = { + imports: [], + declarations: [], + providers: [{provide: 'foo', useValue: 'bar'}] + }; + it('direct usage', () => { + TestBed.overrideComponent(MyComponent, {add: { providers: config.providers}}); +cy.mount(MyComponent, config); + }); + it('spread usage', () => { + TestBed.overrideComponent(MyComponent, { add: { providers: [{provide: 'foo', useValue: 'bar'}] }}); +cy.mount(MyComponent, {...config, providers: undefined }); + }); + it('inlined usage', () => { + TestBed.overrideComponent(MyComponent, { add: { providers: [{provide: 'foo', useValue: 'bar'}] }}); +cy.mount(MyComponent, {imports: [], declarations: [], providers: undefined}); + }); + " +`; diff --git a/packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts b/packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts new file mode 100644 index 0000000000..542710b111 --- /dev/null +++ b/packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts @@ -0,0 +1,325 @@ +import { addProjectConfiguration, Tree } from '@nrwl/devkit'; +import { libraryGenerator } from '@nrwl/workspace'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import updateToCypress11 from './cypress-11'; +import { installedCypressVersion } from '../../utils/cypress-version'; +jest.mock('../../utils/cypress-version'); +import { cypressComponentProject } from '../../generators/cypress-component-project/cypress-component-project'; + +describe('Cypress 11 Migration', () => { + let tree: Tree; + let mockInstalledCypressVersion: jest.Mock< + ReturnType + > = installedCypressVersion as never; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + jest.resetAllMocks(); + }); + + it('should not update if cypress { + // setup called the component setup. mock to v10 so it doesn't throw. + mockInstalledCypressVersion.mockReturnValue(10); + await setup(tree); + mockInstalledCypressVersion.mockReturnValue(9); + const beforeReact = tree.read( + 'libs/my-react-lib/src/lib/no-import.cy.ts', + 'utf-8' + ); + + const beforeNg = tree.read( + 'libs/my-ng-lib/src/lib/no-import.component.cy.ts', + 'utf-8' + ); + + await updateToCypress11(tree); + const actualReact = tree.read( + 'libs/my-react-lib/src/lib/no-import.cy.ts', + 'utf-8' + ); + + const actualNg = tree.read( + 'libs/my-ng-lib/src/lib/no-import.component.cy.ts', + 'utf-8' + ); + + expect(actualReact).toEqual(beforeReact); + expect(actualNg).toEqual(beforeNg); + }); + + it('should migrate to v11', async () => { + mockInstalledCypressVersion.mockReturnValue(10); + await setup(tree); + await updateToCypress11(tree); + expect( + tree.read('libs/my-react-lib/src/lib/no-import.cy.ts', 'utf-8') + ).toMatchSnapshot(); + + expect( + tree.read( + 'libs/my-react-lib/src/lib/with-import-18.component.cy.ts', + 'utf-8' + ) + ).toMatchSnapshot(); + expect( + tree.read( + 'libs/my-react-lib/src/lib/with-import.component.cy.ts', + 'utf-8' + ) + ).toMatchSnapshot(); + expect( + tree.read('libs/my-ng-lib/src/lib/no-import.component.cy.ts', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('libs/my-ng-lib/src/lib/with-import.component.cy.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should only update component projects', async () => { + addProjectConfiguration(tree, 'my-e2e-app', { + projectType: 'application', + root: 'apps/my-e2e-app', + sourceRoot: 'apps/my-e2e-app/src', + targets: { + e2e: { + executor: '@nrwl/cypress:cypress', + options: { + cypressConfig: 'apps/my-e2e-app/cypress.config.ts', + }, + }, + }, + }); + + const content = `import {MountConfig} from 'cypress/angular'; + import { MyComponent } from './my.component'; + describe('MyComponent', () => { + const config:MountConfig = { + imports: [], + declarations: [], + providers: [{provide: 'foo', useValue: 'bar'}] + }; + it('direct usage', () => { + cy.mount(MyComponent, config); + }); +`; + tree.write('apps/my-e2e-app/src/somthing.component.cy.ts', content); + await updateToCypress11(tree); + expect( + tree.read('apps/my-e2e-app/src/somthing.component.cy.ts', 'utf-8') + ).toEqual(content); + }); +}); + +async function setup(tree: Tree) { + await libraryGenerator(tree, { + name: 'my-react-lib', + }); + await cypressComponentProject(tree, { + project: 'my-react-lib', + skipFormat: true, + }); + tree.write( + 'libs/my-react-lib/cypress/support/commands.ts', + `/// +import { mount } from 'cypress/react18' + +declare global { +// eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(email: string, password: string): void; + mount: typeof mount; + } + } +} +Cypress.Commands.add('mount', mount) +` + ); + tree.write( + 'libs/my-react-lib/src/lib/no-import.cy.ts', + ` +it('calls the prop', () => { + cy.mount() + cy.contains('My component') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + }) +})` + ); + tree.write( + 'libs/my-react-lib/src/lib/with-import.component.cy.ts', + `import { mountHook, unmount } from 'cypress/react' +import { useCounter } from ‘./useCounter’ + +it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) +}) + +describe('blah', () => { + + it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) + }) +}) + + +it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + unmount() + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + unmount() + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') + }) +})` + ); + tree.write( + 'libs/my-react-lib/src/lib/with-import-18.component.cy.ts', + `import { mountHook, unmount } from 'cypress/react18' +import { useCounter } from ‘./useCounter’ + +it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) +}) + +describe('blah', () => { + + it('increments the count', () => { + mountHook(() => useCounter()).then((result) => { + expect(result.current.count).to.equal(0) + result.current.increment() + expect(result.current.count).to.equal(1) + result.current.increment() + expect(result.current.count).to.equal(2) + }) + }) +}) + + +it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + unmount() + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') +}) + +describe('again', () => { + it('calls the prop', () => { + cy.mount() + cy.contains('My component') + + unmount() + + cy.contains('My component').should('not.exist') + cy.get('@onUnmount').should('have.been.calledOnce') + }) +})` + ); + + await libraryGenerator(tree, { + name: 'my-ng-lib', + }); + + await cypressComponentProject(tree, { + project: 'my-ng-lib', + skipFormat: true, + }); + tree.write( + 'libs/my-ng-lib/cypress/support/commands.ts', + `/// +import { mount } from 'cypress/angular' + +declare global { +// eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(email: string, password: string): void; + mount: typeof mount; + } + } +} +Cypress.Commands.add('mount', mount) +` + ); + tree.write( + 'libs/my-ng-lib/src/lib/with-import.component.cy.ts', + `import { MountConfig } from 'cypress/angular'; + import { MyComponent } from './my.component'; + import {TestBed} from '@angular/core/testing'; + describe('MyComponent', () => { + const config: MountConfig = { + imports: [], + declarations: [], + providers: [{provide: 'foo', useValue: 'bar'}] + }; + it('direct usage', () => { + cy.mount(MyComponent, config); + }); + it('spread usage', () => { + cy.mount(MyComponent, {...config, providers: [{provide: 'foo', useValue: 'bar'}] }); + }); + it('inlined usage', () => { + cy.mount(MyComponent, {imports: [], declarations: [], providers: [{provide: 'foo', useValue: 'bar'}]}); + }); + ` + ); + tree.write( + 'libs/my-ng-lib/src/lib/no-import.component.cy.ts', + ` + import { MyComponent } from './my.component'; + describe('MyComponent', () => { + const config = { + imports: [], + declarations: [], + providers: [{provide: 'foo', useValue: 'bar'}] + }; + it('direct usage', () => { + cy.mount(MyComponent, config); + }); + it('spread usage', () => { + cy.mount(MyComponent, {...config, providers: [{provide: 'foo', useValue: 'bar'}] }); + }); + it('inlined usage', () => { + cy.mount(MyComponent, {imports: [], declarations: [], providers: [{provide: 'foo', useValue: 'bar'}]}); + }); + ` + ); +} diff --git a/packages/cypress/src/migrations/update-15-1-0/cypress-11.ts b/packages/cypress/src/migrations/update-15-1-0/cypress-11.ts new file mode 100644 index 0000000000..de26b1c8bf --- /dev/null +++ b/packages/cypress/src/migrations/update-15-1-0/cypress-11.ts @@ -0,0 +1,178 @@ +import { CY_FILE_MATCHER } from '../../utils/ct-helpers'; +import { + addDependenciesToPackageJson, + formatFiles, + getProjects, + joinPathFragments, + Tree, + visitNotIgnoredFiles, +} from '@nrwl/devkit'; +import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { extname } from 'path'; +import * as ts from 'typescript'; +import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl'; +import { installedCypressVersion } from '../../utils/cypress-version'; +import { cypressVersion } from '../../utils/versions'; + +export async function updateToCypress11(tree: Tree) { + const installedVersion = installedCypressVersion(); + if (installedVersion < 10) { + return; + } + + const projects = getProjects(tree); + forEachExecutorOptions( + tree, + '@nrwl/cypress:cypress', + (options, projectName, targetName, configurationName) => { + if ( + options.testingType !== 'component' || + !(options.cypressConfig && tree.exists(options.cypressConfig)) + ) { + return; + } + const projectConfig = projects.get(projectName); + const commandsFile = joinPathFragments( + projectConfig.root, + 'cypress', + 'support', + 'commands.ts' + ); + const framework = getFramework( + tree.exists(commandsFile) + ? tree.read(commandsFile, 'utf-8') + : tree.read(options.cypressConfig, 'utf-8') + ); + + visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => { + if (!CY_FILE_MATCHER.test(filePath)) { + return; + } + const frameworkFromFile = getFramework(tree.read(filePath, 'utf-8')); + + if (framework === 'react' || frameworkFromFile === 'react') { + updateUnmountUsage(tree, filePath); + updateMountHookUsage(tree, filePath); + } + if (framework === 'angular' || frameworkFromFile === 'angular') { + updateProviderUsage(tree, filePath); + } + }); + } + ); + + const installTask = addDependenciesToPackageJson( + tree, + {}, + { cypress: cypressVersion } + ); + + await formatFiles(tree); + return () => { + installTask(); + }; +} + +export function updateMountHookUsage(tree: Tree, filePath: string) { + const originalContents = tree.read(filePath, 'utf-8'); + const commentedMountHook = tsquery.replace( + originalContents, + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="mountHook"]):has(StringLiteral[value="cypress/react"], StringLiteral[value="cypress/react18"])', + (node) => { + return `/** TODO: mountHook is deprecate. +* Use a wrapper component instead. +* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed +* */\n${node.getText()}`; + } + ); + + tree.write(filePath, commentedMountHook); +} + +export function updateUnmountUsage(tree: Tree, filePath: string) { + const reactDomImport = extname(filePath).includes('ts') + ? `import ReactDom from 'react-dom'` + : `const ReactDom = require('react-dom')`; + + const originalContents = tree.read(filePath, 'utf-8'); + + const updatedImports = tsquery.replace( + originalContents, + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="unmount"]):has(StringLiteral[value="cypress/react"], StringLiteral[value="cypress/react18"])', + (node) => { + return `${node.getText().replace('unmount', 'getContainerEl')} +${reactDomImport}`; + } + ); + + const updatedUnmountApi = tsquery.replace( + updatedImports, + 'ExpressionStatement > CallExpression:has(Identifier[name="unmount"])', + (node: ts.ExpressionStatement) => { + if (node.expression.getText() === 'unmount') { + return `cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()))`; + } + } + ); + + tree.write(filePath, updatedUnmountApi); +} + +export function updateProviderUsage(tree: Tree, filePath: string) { + const originalContents = tree.read(filePath, 'utf-8'); + const isTestBedImported = + tsquery.query( + originalContents, + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="TestBed"]):has(StringLiteral[value="@angular/core/testing"])' + )?.length > 0; + + let updatedProviders = tsquery.replace( + originalContents, + 'CallExpression:has(PropertyAccessExpression:has(Identifier[name="mount"]))', + (node: ts.CallExpression) => { + const expressionName = node.expression.getText(); + if (expressionName === 'cy.mount' && node?.arguments?.length > 1) { + const component = node.arguments[0].getText(); + + if (ts.isObjectLiteralExpression(node.arguments[1])) { + const providers = node.arguments[1]?.properties + ?.find((p) => p.name?.getText() === 'providers') + ?.getText(); + const noProviders = tsquery.replace( + node.getText(), + 'PropertyAssignment:has(Identifier[name="providers"])', + (n) => { + // set it to undefined so we don't run into a hanging comma causing invalid syntax + return 'providers: undefined'; + } + ); + return `TestBed.overrideComponent(${component}, { add: { ${providers} }});\n${noProviders}`; + } else { + return `TestBed.overrideComponent(${component}, {add: { providers: ${node.arguments[1].getText()}.providers}});\n${node.getText()}`; + } + } + } + ); + tree.write( + filePath, + `${ + isTestBedImported ? '' : "import {TestBed} from '@angular/core/testing;\n" + }${updatedProviders}` + ); +} + +function getFramework(contents: string): 'react' | 'angular' | null { + if (contents.includes('cypress/react') || contents.includes('@nrwl/react')) { + return 'react'; + } + if ( + contents.includes('cypress/angular') || + contents.includes('@nrwl/angular') + ) { + return 'angular'; + } + return null; +} + +export default updateToCypress11; diff --git a/packages/cypress/src/utils/versions.ts b/packages/cypress/src/utils/versions.ts index 8aa2956d20..4a1a296adf 100644 --- a/packages/cypress/src/utils/versions.ts +++ b/packages/cypress/src/utils/versions.ts @@ -1,6 +1,6 @@ export const nxVersion = require('../../package.json').version; export const eslintPluginCypressVersion = '^2.10.3'; export const typesNodeVersion = '16.11.7'; -export const cypressVersion = '^10.7.0'; +export const cypressVersion = '^11.0.0'; export const cypressWebpackVersion = '^2.0.0'; export const webpackHttpPluginVersion = '^5.5.0'; diff --git a/scripts/check-imports.js b/scripts/check-imports.js index 63cd8317cb..4b309ab80d 100644 --- a/scripts/check-imports.js +++ b/scripts/check-imports.js @@ -41,6 +41,10 @@ function check() { 'packages/workspace/src/tasks-runner/task-orchestrator.ts', 'packages/nest/src/generators/init/lib/add-dependencies.ts', 'packages/nest/src/migrations/update-13-2-0/update-to-nest-8.ts', + // cypress v11 migration checks if the TestBed is imported by looking for the import + // which is @angular/core/testing. and the tests check for this + 'packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts', + 'packages/cypress/src/migrations/update-15-1-0/cypress-11.ts', ]; const files = [