feat(testing): Cypress 10 and component testing support (#9201)

* feat(testing): add generator to aid in the migration to cypress 10

cypress 10 introduces a new configuration format and new layout that requires update settings and
files for e2e projects

* feat(testing): cypress component tests for react/next

initial work for cypress component tests for react and next

* feat(testing): add support for v10 e2e cypress projects

create the correct files for cypress projects >v10 and reorganize tests based on version to allow
easier parsing of tests

* feat(testing): add utils for modifying cypress v10 config

provide ts transformers to take in an existing cypress config and update/add properties within the
given configuration

* fix(testing): fix tests affected by the cypress v10 changes

update tests to assert the correct files/folders/file contents due to the cypress changes in v10

* cleanup(testing): move cypress component testing plugins into the respective packages

move the plugins into out of cypress plugins into the specific vertical plugin to prevent issues
with circular refs

* cleanup(testing): bump cypress version

bump to latest cypress v10 release

* docs(testing): update docs for cypress 10

update cypress docs to include info about component testing and migration to cypress v10

* fix(repo): revert cypress version bump

keep v9 of cypress installed for nx repo until v10 release

* fix(testing): update cypress gen tsconfig and infer targets with converter

* fix(testing): make sure tests use the cypress v10 (for the intermediate)

* fix(testing): update target name after feedback

* fix(testing): support multiple target w/custom configs for cypress v10 migration

* fix(testing): refactor cy component tests into seperate verticals

* feat(testing): create storybook cypress preset

* fix(testing): clean up cy v10 migration

* fix(testing): don't branch for cypress executor testingType

* fix(testing): move cy comp test generator to next

* fix(testing): bump cypress deps

* fix(testing): clean up cy component testing generators

* fix(testing): update cy component testing docs

* fix(testign): dep check. runtime plugin pulls from @nrwl/react

* fix(testing): move e2e into verticals

* fix(testing): address PR feedback

* fix(testing): clean up unit tests

* feat(angular): support migrating angular cli workspaces using cypress v10

* chore(testing): update e2e tests

* fix(testing): address pr feedback

* chore(testing): remove cypress component testing for next.js

* fix(testing): address pr feedback

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
This commit is contained in:
Caleb Ukle 2022-07-08 14:34:00 -05:00 committed by GitHub
parent a34402fd9e
commit 8154191eb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 5803 additions and 889 deletions

View File

@ -10,7 +10,19 @@
"id": "overview",
"path": "/packages/cypress",
"file": "shared/cypress-plugin",
"content": "![Cypress logo](/shared/cypress-logo.png)\n\nCypress is an e2e test runner built for modern web. It has a lot of great features:\n\n- Time travel\n- Real-time reloads\n- Automatic waiting\n- Spies, stubs, and clocks\n- Network traffic control\n- Screenshots and videos\n\n## Setting Up Cypress\n\n### Generating Applications\n\nBy default, when creating a new frontend application, Nx will use Cypress to create the e2e tests project.\n\n```bash\nnx g @nrwl/web:app frontend\n```\n\n### Creating a Cypress E2E project for an existing project\n\nYou can create a new Cypress E2E project for an existing project.\n\nIf the `@nrwl/cypress` package is not installed, install the version that matches your `@nrwl/workspace` version.\n\n```bash\nyarn add --dev @nrwl/cypress\n```\n\n```bash\nnpm install --save-dev @nrwl/cypress\n```\n\nNext, generate an E2E project based on an existing project.\n\n```bash\nnx g @nrwl/cypress:cypress-project your-app-name-e2e --project=your-app-name\n```\n\nReplace `your-app-name` with the app's name as defined in your `workspace.json` file.\n\n## Using Cypress\n\n### Testing Applications\n\nSimply run `nx e2e frontend-e2e` to execute e2e tests with Cypress.\n\nBy default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your terminal. Screenshots and videos will be accessible in `dist/apps/frontend/screenshots` and `dist/apps/frontend/videos`.\n\n### Watching for Changes\n\nWith, `nx e2e frontend-e2e --watch` Cypress will start in the application mode.\n\nRunning Cypress with `--watch` is a great way to enhance dev workflow - you can build up test files with the application running and Cypress will re-run those tests as you enhance and add to the suite.\n\nCypress doesn't currently re-run your tests after changes are made to application code when it runs in “headed” mode.\n\n### Using Cypress in the Headed Mode\n\nYou can run Cypress in headed mode to see your app being tested. To do this, pass in the `--watch` option. E.g: `nx frontend-e2e --watch`\n\n### Testing Against Prod Build\n\nYou can run your e2e test against a production build like this: `nx e2e frontend-e2e --prod`.\n\n## Configuration\n\n### Specifying a Custom Url to Test\n\nThe `baseUrl` property provides you the ability to test an application hosted on a specific domain.\n\n```bash\nnx e2e frontend-e2e --baseUrl=https://frontend.com\n```\n\n> If no `baseUrl` and no `devServerTarget` are provided, Cypress will expect to have the `baseUrl` property in the `cypress.json` file, or will error.\n\n### Using cypress.json\n\nIf you need to fine tune your Cypress setup, you can do so by modifying `cypress.json` in the e2e project. For instance, you can easily add your `projectId` to save all the screenshots and videos into your Cypress dashboard. The complete configuration is documented on [the official website](https://docs.cypress.io/guides/references/configuration.html#Options).\n\n## More Documentation\n\nReact Nx Tutorial\n\n- [Step 2: Add E2E Tests](/react-tutorial/02-add-e2e-test)\n- [Step 3: Display Todos](/react-tutorial/03-display-todos)\n\nAngular Nx Tutorial\n\n- [Step 2: Add E2E Tests](/angular-tutorial/02-add-e2e-test)\n- [Step 3: Display Todos](/angular-tutorial/03-display-todos)\n"
"content": "![Cypress logo](/shared/cypress-logo.png)\n\nCypress is a test runner built for the modern web. It has a lot of great features:\n\n- Time travel\n- Real-time reloads\n- Automatic waiting\n- Spies, stubs, and clocks\n- Network traffic control\n- Screenshots and videos\n\n## Setting Up Cypress\n\n> Info about [Cypress Component Testing can be found here](/cypress/cypress-component-testing)\n\nIf the `@nrwl/cypress` package is not installed, install the version that matches your `nx` package version.\n\n```bash\nyarn add --dev @nrwl/cypress\n```\n\n```bash\nnpm install --save-dev @nrwl/cypress\n```\n\n## E2E Testing\n\nBy default, when creating a new frontend application, Nx will use Cypress to create the e2e tests project.\n\n```bash\nnx g @nrwl/web:app frontend\n```\n\n### Creating a Cypress E2E project for an existing project\n\nTo generate an E2E project based on an existing project, run the following generator\n\n```bash\nnx g @nrwl/cypress:cypress-project your-app-name-e2e --project=your-app-name\n```\n\nOptionally, you can use the `--baseUrl` option if you don't want cypress plugin to serve `your-app-name`.\n\n```bash\nnx g @nrwl/cypress:cypress-project your-app-name-e2e --baseUrl=http://localhost:4200\n```\n\nReplace `your-app-name` with the app's name as defined in your `workspace.json` file.\n\n### Testing Applications\n\nRun `nx e2e frontend-e2e` to execute e2e tests with Cypress.\n\nYou can run your e2e test against a production build with the `--prod` flag\n\n```bash\nnx e2e frontend-e2e --prod\n```\n\nBy default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your\nterminal. Screenshots and videos will be accessible in `dist/apps/frontend/screenshots` and `dist/apps/frontend/videos`.\n\n### Watching for Changes (Headed Mode)\n\nWith, `nx e2e frontend-e2e --watch` Cypress will start in headed mode where you can see your application being tested.\n\nRunning Cypress with `--watch` is a great way to enhance dev workflow - you can build up test files with the application\nrunning and Cypress will re-run those tests as you enhance and add to the suite.\n\n```bash\nnx e2e frontend-e2e --prod\n```\n\n### Specifying a Custom Url to Test\n\nThe `baseUrl` property provides you the ability to test an application hosted on a specific domain.\n\n```bash\nnx e2e frontend-e2e --baseUrl=https://frontend.com\n```\n\n> If no `baseUrl` and no `devServerTarget` are provided, Cypress will expect to have the `baseUrl` property in\n> the cypress config file, or will error.\n\n## Using cypress.config.ts\n\nIf you need to fine tune your Cypress setup, you can do so by modifying `cypress.config.ts` in the project root. For\ninstance,\nyou can easily add your `projectId` to save all the screenshots and videos into your Cypress dashboard. The complete\nconfiguration is documented\non [the official website](https://docs.cypress.io/guides/references/configuration.html#Options).\n"
},
{
"name": "Component Testing",
"id": "cypress-component-testing",
"file": "shared/cypress-component-testing",
"content": "# Cypress Component Testing\n\n> Component testing is in a early preview and requires Cypress v10 and above.\n> See our [guide for more information](/cypress/cypress-v10-migration) to migrate to Cypress v10.\n\nUnlike [E2E testing](/packages/cypress), component testing does not create a new project. Instead, Cypress component testing is added\ndirectly to a project.\n\n```bash\nnx g @nrwl/react:cypress-component-configuration --project=your-react-lib\n```\n\nYou can optionally pass in `--generate-tests` to create component tests for all components within the library.\n\n## Testing Projects\n\nRun `nx component-test your-lib` to execute the component tests with Cypress.\n\nBy default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your\nterminal. Screenshots and videos will be accessible in `dist/libs/your-lib/screenshots` and `dist/libs/your-lib/videos`.\n\n## Watching for Changes (Headed Mode)\n\nWith, `nx component-test your-lib --watch` Cypress will start in headed mode. Where you can see your component being tested.\n\nRunning Cypress with `--watch` is a great way to iterate on your components since cypress will rerun your tests as you make those changes validating the new behavior.\n\n## More Information\n\nYou can read more on component testing in the [Cypress documentation](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test).\n"
},
{
"name": "v10 Migration Guide",
"id": "v10-migration-guide",
"file": "shared/guides/cypress/cypress-v10-migration",
"content": "# Migrating to Cypress V10\n\nCypress v10 introduce new features, like component testing, along with some breaking changes.\n\nBefore continuing, make sure you have all your changes committed and have a clean working tree.\n\nYou can migrate an E2E project to v10 by running the following command:\n\n```bash\nnx g @nrwl/cypress:migrate-to-cypress-10\n```\n\nIn general, these are the steps taken to migrate your project:\n\n1. Migrates your existing `cypress.json` configuration to a new `cypress.config.ts` configuration file.\n - The `pluginsFile` option has been replaced for `setupNodeEvents`. We will import the file and add it to\n the `setupNodeEvents` config option. Double-check your plugins are working correctly.\n2. Rename all test files from `.spec.ts` to `.cy.ts`\n3. Rename the `support/index.ts` to `support/e2e.ts` and update any associated imports\n4. Rename the `integrations` folder to the `e2e` folder\n\nWe take the best effort to make this migration seamless, but there can be edge cases we didn't anticipate. So feel free to [open an issue](https://github.com/nrwl/nx/issues/new?assignees=&labels=type%3A+bug&template=1-bug.md) if you come across any problems.\n\nYou can also consult the [official Cypress migration guide](https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0) if you get stuck and want to manually migrate your projects.\n"
}
],
"generators": [
@ -108,6 +120,64 @@
"implementation": "/packages/cypress/src/generators/cypress-project/cypress-project#cypressProjectGenerator.ts",
"aliases": [],
"path": "/packages/cypress/src/generators/cypress-project/schema.json"
},
{
"name": "cypress-component-project",
"factory": "./src/generators/cypress-component-project/cypress-component-project#cypressComponentProject",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxCypressComponentProject",
"cli": "nx",
"title": "Set up Cypress component testing for a project",
"description": "Set up Cypress component test for a project.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/cypress:cypress-component-project --project=my-cool-lib ",
"description": "Add cypress component testing to an existing project named my-cool-lib"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing to",
"$default": { "$source": "projectName" },
"x-prompt": "What project should we add Cypress component testing to?"
}
},
"required": ["project"],
"presets": []
},
"description": "Set up Cypress Component Test for a project",
"hidden": true,
"implementation": "/packages/cypress/src/generators/cypress-component-project/cypress-component-project#cypressComponentProject.ts",
"aliases": [],
"path": "/packages/cypress/src/generators/cypress-component-project/schema.json"
},
{
"name": "migrate-to-cypress-10",
"factory": "./src/generators/migrate-to-cypress-ten/migrate-to-cypress-ten#migrateCypressProject",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxCypressMigrateToTen",
"cli": "nx",
"title": "Migrate e2e project to Cypress 10",
"description": "Migrate Cypress e2e project from v8/v9 to Cypress v10.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/cypress:migrate-to-cypress-10",
"description": "Migrate existing cypress projects to Cypress v10"
}
],
"properties": {},
"presets": []
},
"description": "Migrate existing Cypress e2e projects to Cypress v10",
"hidden": true,
"implementation": "/packages/cypress/src/generators/migrate-to-cypress-ten/migrate-to-cypress-ten#migrateCypressProject.ts",
"aliases": [],
"path": "/packages/cypress/src/generators/migrate-to-cypress-ten/schema.json"
}
],
"executors": [

View File

@ -1167,6 +1167,91 @@
"aliases": [],
"hidden": false,
"path": "/packages/react/src/generators/remote/schema.json"
},
{
"name": "cypress-component-configuration",
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxReactCypressComponentTestConfiguration",
"title": "Add Cypress component testing",
"description": "Add a Cypress component testing configuration to an existing project.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/react:cypress-component-configuration --project=my-react-project",
"description": "Add component testing to your react project"
},
{
"command": "nx g @nrwl/react:cypress-component-configuration --project=my-react-project --generate-tests",
"description": "Add component testing to your react project and generate component tests for your existing components"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing configuration to",
"$default": { "$source": "projectName" },
"x-prompt": "What project should we add Cypress component testing to?"
},
"generateTests": {
"type": "boolean",
"description": "Generate default component tests for existing components in the project",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false
}
},
"required": ["project"],
"presets": []
},
"description": "Setup Cypress component testing for a React project",
"hidden": false,
"implementation": "/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator.ts",
"aliases": [],
"path": "/packages/react/src/generators/cypress-component-configuration/schema.json"
},
{
"name": "component-test",
"factory": "./src/generators/component-test/component-test#componentTestGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxReactCypressComponentTest",
"title": "Add Cypress component test",
"description": "Add a Cypress component test for a component.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/react:component-test --project=my-react-project --component-path=src/lib/fancy-component.tsx",
"description": "Create a cypress component test for FancyComponent"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project the component is apart of",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this component apart of?"
},
"componentPath": {
"type": "string",
"description": "Path to component, from the project source root",
"x-prompt": "What is the path to the component?"
}
},
"required": ["project", "componentPath"],
"presets": []
},
"description": "Generate a Cypress component test for a React component",
"hidden": false,
"implementation": "/packages/react/src/generators/component-test/component-test#componentTestGenerator.ts",
"aliases": [],
"path": "/packages/react/src/generators/component-test/schema.json"
}
],
"executors": [

View File

@ -1015,6 +1015,16 @@
"id": "overview",
"path": "/packages/cypress",
"file": "shared/cypress-plugin"
},
{
"name": "Component Testing",
"id": "cypress-component-testing",
"file": "shared/cypress-component-testing"
},
{
"name": "v10 Migration Guide",
"id": "v10-migration-guide",
"file": "shared/guides/cypress/cypress-v10-migration"
}
]
},

View File

@ -74,7 +74,12 @@
"path": "generated/packages/cypress.json",
"schemas": {
"executors": ["cypress"],
"generators": ["init", "cypress-project"]
"generators": [
"init",
"cypress-project",
"cypress-component-project",
"migrate-to-cypress-10"
]
}
},
{
@ -210,7 +215,9 @@
"component-cypress-spec",
"hook",
"host",
"remote"
"remote",
"cypress-component-configuration",
"component-test"
]
}
},

View File

@ -0,0 +1,30 @@
# Cypress Component Testing
> Component testing is in a early preview and requires Cypress v10 and above.
> See our [guide for more information](/cypress/cypress-v10-migration) to migrate to Cypress v10.
Unlike [E2E testing](/packages/cypress), component testing does not create a new project. Instead, Cypress component testing is added
directly to a project.
```bash
nx g @nrwl/react:cypress-component-configuration --project=your-react-lib
```
You can optionally pass in `--generate-tests` to create component tests for all components within the library.
## Testing Projects
Run `nx component-test your-lib` to execute the component tests with Cypress.
By default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your
terminal. Screenshots and videos will be accessible in `dist/libs/your-lib/screenshots` and `dist/libs/your-lib/videos`.
## Watching for Changes (Headed Mode)
With, `nx component-test your-lib --watch` Cypress will start in headed mode. Where you can see your component being tested.
Running Cypress with `--watch` is a great way to iterate on your components since cypress will rerun your tests as you make those changes validating the new behavior.
## More Information
You can read more on component testing in the [Cypress documentation](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test).

View File

@ -1,6 +1,6 @@
![Cypress logo](/shared/cypress-logo.png)
Cypress is an e2e test runner built for modern web. It has a lot of great features:
Cypress is a test runner built for the modern web. It has a lot of great features:
- Time travel
- Real-time reloads
@ -11,19 +11,9 @@ Cypress is an e2e test runner built for modern web. It has a lot of great featur
## Setting Up Cypress
### Generating Applications
> Info about [Cypress Component Testing can be found here](/cypress/cypress-component-testing)
By default, when creating a new frontend application, Nx will use Cypress to create the e2e tests project.
```bash
nx g @nrwl/web:app frontend
```
### Creating a Cypress E2E project for an existing project
You can create a new Cypress E2E project for an existing project.
If the `@nrwl/cypress` package is not installed, install the version that matches your `@nrwl/workspace` version.
If the `@nrwl/cypress` package is not installed, install the version that matches your `nx` package version.
```bash
yarn add --dev @nrwl/cypress
@ -33,39 +23,53 @@ yarn add --dev @nrwl/cypress
npm install --save-dev @nrwl/cypress
```
Next, generate an E2E project based on an existing project.
## E2E Testing
By default, when creating a new frontend application, Nx will use Cypress to create the e2e tests project.
```bash
nx g @nrwl/web:app frontend
```
### Creating a Cypress E2E project for an existing project
To generate an E2E project based on an existing project, run the following generator
```bash
nx g @nrwl/cypress:cypress-project your-app-name-e2e --project=your-app-name
```
Replace `your-app-name` with the app's name as defined in your `workspace.json` file.
Optionally, you can use the `--baseUrl` option if you don't want cypress plugin to serve `your-app-name`.
## Using Cypress
```bash
nx g @nrwl/cypress:cypress-project your-app-name-e2e --baseUrl=http://localhost:4200
```
Replace `your-app-name` with the app's name as defined in your `workspace.json` file.
### Testing Applications
Simply run `nx e2e frontend-e2e` to execute e2e tests with Cypress.
Run `nx e2e frontend-e2e` to execute e2e tests with Cypress.
By default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your terminal. Screenshots and videos will be accessible in `dist/apps/frontend/screenshots` and `dist/apps/frontend/videos`.
You can run your e2e test against a production build with the `--prod` flag
### Watching for Changes
```bash
nx e2e frontend-e2e --prod
```
With, `nx e2e frontend-e2e --watch` Cypress will start in the application mode.
By default, Cypress will run in headless mode. You will have the result of all the tests and errors (if any) in your
terminal. Screenshots and videos will be accessible in `dist/apps/frontend/screenshots` and `dist/apps/frontend/videos`.
Running Cypress with `--watch` is a great way to enhance dev workflow - you can build up test files with the application running and Cypress will re-run those tests as you enhance and add to the suite.
### Watching for Changes (Headed Mode)
Cypress doesn't currently re-run your tests after changes are made to application code when it runs in “headed” mode.
With, `nx e2e frontend-e2e --watch` Cypress will start in headed mode where you can see your application being tested.
### Using Cypress in the Headed Mode
Running Cypress with `--watch` is a great way to enhance dev workflow - you can build up test files with the application
running and Cypress will re-run those tests as you enhance and add to the suite.
You can run Cypress in headed mode to see your app being tested. To do this, pass in the `--watch` option. E.g: `nx frontend-e2e --watch`
### Testing Against Prod Build
You can run your e2e test against a production build like this: `nx e2e frontend-e2e --prod`.
## Configuration
```bash
nx e2e frontend-e2e --prod
```
### Specifying a Custom Url to Test
@ -75,20 +79,13 @@ The `baseUrl` property provides you the ability to test an application hosted on
nx e2e frontend-e2e --baseUrl=https://frontend.com
```
> If no `baseUrl` and no `devServerTarget` are provided, Cypress will expect to have the `baseUrl` property in the `cypress.json` file, or will error.
> If no `baseUrl` and no `devServerTarget` are provided, Cypress will expect to have the `baseUrl` property in
> the cypress config file, or will error.
### Using cypress.json
## Using cypress.config.ts
If you need to fine tune your Cypress setup, you can do so by modifying `cypress.json` in the e2e project. For instance, you can easily add your `projectId` to save all the screenshots and videos into your Cypress dashboard. The complete configuration is documented on [the official website](https://docs.cypress.io/guides/references/configuration.html#Options).
## More Documentation
React Nx Tutorial
- [Step 2: Add E2E Tests](/react-tutorial/02-add-e2e-test)
- [Step 3: Display Todos](/react-tutorial/03-display-todos)
Angular Nx Tutorial
- [Step 2: Add E2E Tests](/angular-tutorial/02-add-e2e-test)
- [Step 3: Display Todos](/angular-tutorial/03-display-todos)
If you need to fine tune your Cypress setup, you can do so by modifying `cypress.config.ts` in the project root. For
instance,
you can easily add your `projectId` to save all the screenshots and videos into your Cypress dashboard. The complete
configuration is documented
on [the official website](https://docs.cypress.io/guides/references/configuration.html#Options).

View File

@ -0,0 +1,24 @@
# Migrating to Cypress V10
Cypress v10 introduce new features, like component testing, along with some breaking changes.
Before continuing, make sure you have all your changes committed and have a clean working tree.
You can migrate an E2E project to v10 by running the following command:
```bash
nx g @nrwl/cypress:migrate-to-cypress-10
```
In general, these are the steps taken to migrate your project:
1. Migrates your existing `cypress.json` configuration to a new `cypress.config.ts` configuration file.
- The `pluginsFile` option has been replaced for `setupNodeEvents`. We will import the file and add it to
the `setupNodeEvents` config option. Double-check your plugins are working correctly.
2. Rename all test files from `.spec.ts` to `.cy.ts`
3. Rename the `support/index.ts` to `support/e2e.ts` and update any associated imports
4. Rename the `integrations` folder to the `e2e` folder
We take the best effort to make this migration seamless, but there can be edge cases we didn't anticipate. So feel free to [open an issue](https://github.com/nrwl/nx/issues/new?assignees=&labels=type%3A+bug&template=1-bug.md) if you come across any problems.
You can also consult the [official Cypress migration guide](https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0) if you get stuck and want to manually migrate your projects.

View File

@ -5,6 +5,7 @@ import {
checkFilesExist,
cleanupProject,
getSelectedPackageManager,
packageInstall,
readJson,
runCLI,
runCommand,
@ -51,10 +52,12 @@ describe('convert Angular CLI workspace to an Nx workspace', () => {
updateFile('angular.json', JSON.stringify(angularJson, null, 2));
}
function addCypress() {
// TODO(leo): @cypress/schematic latest comes with Cypress 10 support
// which we don't support yet in our Cypress plugin.
function addCypress9() {
runNgAdd('@cypress/schematic', '--e2e-update', '1.7.0');
packageInstall('cypress', null, '^9.0.0');
}
function addCypress10() {
runNgAdd('@cypress/schematic', '--e2e-update', 'latest');
}
function addEsLint() {
@ -296,8 +299,8 @@ describe('convert Angular CLI workspace to an Nx workspace', () => {
// runCommand('mv src-bak src');
});
it('should handle wrong cypress setup', () => {
addCypress();
it('should handle a workspace with cypress v9', () => {
addCypress9();
// Remove cypress.json
runCommand('mv cypress.json cypress.json.bak');
@ -318,10 +321,6 @@ describe('convert Angular CLI workspace to an Nx workspace', () => {
);
// Restore cypress.json
runCommand('mv cypress-bak cypress');
});
it('should handle a workspace with cypress', () => {
addCypress();
runNgAdd('@nrwl/angular', '--npm-scope projscope --skip-install');
@ -387,6 +386,93 @@ describe('convert Angular CLI workspace to an Nx workspace', () => {
});
});
it('should handle a workspace with cypress v10', () => {
addCypress10();
// Remove cypress.config.ts
runCommand('mv cypress.config.ts cypress.config.ts.bak');
expect(() =>
runNgAdd('@nrwl/angular', '--npm-scope projscope --skip-install')
).toThrow(
'The "e2e" target is using the "@cypress/schematic:cypress" builder but the "configFile" option is not specified and a "cypress.config.{ts,js,mjs,cjs}" file could not be found at the project root.'
);
// Restore cypress.json
runCommand('mv cypress.config.ts.bak cypress.config.ts');
// Remove cypress directory
runCommand('mv cypress cypress-bak');
expect(() =>
runNgAdd('@nrwl/angular', '--npm-scope projscope --skip-install')
).toThrow(
'The "e2e" target is using the "@cypress/schematic:cypress" builder but the "cypress" directory could not be found at the project root.'
);
// Restore cypress.json
runCommand('mv cypress-bak cypress');
runNgAdd('@nrwl/angular', '--npm-scope projscope --skip-install');
const e2eProject = `${project}-e2e`;
//check e2e project files
checkFilesDoNotExist(
'cypress.config.ts',
'cypress/tsconfig.json',
'cypress/e2e/spec.cy.ts',
'cypress/fixtures/example.json',
'cypress/support/commands.ts',
'cypress/support/e2e.ts'
);
checkFilesExist(
`apps/${e2eProject}/cypress.config.ts`,
`apps/${e2eProject}/tsconfig.json`,
`apps/${e2eProject}/src/e2e/spec.cy.ts`,
`apps/${e2eProject}/src/fixtures/example.json`,
`apps/${e2eProject}/src/support/commands.ts`,
`apps/${e2eProject}/src/support/e2e.ts`
);
const projectConfig = readJson(`apps/${project}/project.json`);
expect(projectConfig.targets['cypress-run']).toBeUndefined();
expect(projectConfig.targets['cypress-open']).toBeUndefined();
expect(projectConfig.targets.e2e).toBeUndefined();
// check e2e project config
const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`);
expect(e2eProjectConfig.targets['cypress-run']).toEqual({
executor: '@nrwl/cypress:cypress',
options: {
devServerTarget: `${project}:serve`,
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
},
configurations: {
production: {
devServerTarget: `${project}:serve:production`,
},
},
});
expect(e2eProjectConfig.targets['cypress-open']).toEqual({
executor: '@nrwl/cypress:cypress',
options: {
watch: true,
headless: false,
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
},
});
expect(e2eProjectConfig.targets.e2e).toEqual({
executor: '@nrwl/cypress:cypress',
options: {
devServerTarget: `${project}:serve`,
watch: true,
headless: false,
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
},
configurations: {
production: {
devServerTarget: `${project}:serve:production`,
},
},
});
});
// TODO(leo): The current Verdaccio setup fails to resolve older versions
// of @nrwl/* packages, the @angular-eslint/builder package depends on an
// older version of @nrwl/devkit so we skip this test for now.

View File

@ -3,7 +3,6 @@ import {
cleanupProject,
newProject,
readFile,
readJson,
runCLI,
uniq,
updateFile,
@ -67,20 +66,38 @@ describe('Move Angular Project', () => {
/**
* Tries moving an e2e project from ${app1} -> ${newPath}
*/
it('should work for e2e projects', () => {
it('should work for e2e projects w/custom cypress config', () => {
// by default the cypress config doesn't contain any app specific paths
// create a custom config with some app specific paths
updateFile(
`apps/${app1}-e2e/cypress.config.ts`,
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
videosFolder: '../../dist/cypress/apps/${app1}-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/${app1}-e2e/screenshots',
},
});
`
);
const moveOutput = runCLI(
`generate @nrwl/angular:move --projectName=${app1}-e2e --destination=${newPath}-e2e`
);
// just check that the cypress.json is updated correctly
const cypressJsonPath = `apps/${newPath}-e2e/cypress.json`;
expect(moveOutput).toContain(`CREATE ${cypressJsonPath}`);
checkFilesExist(cypressJsonPath);
const cypressJson = readJson(cypressJsonPath);
expect(cypressJson.videosFolder).toEqual(
// just check that the cypress.config.ts is updated correctly
const cypressConfigPath = `apps/${newPath}-e2e/cypress.config.ts`;
expect(moveOutput).toContain(`CREATE ${cypressConfigPath}`);
checkFilesExist(cypressConfigPath);
const cypressConfig = readFile(cypressConfigPath);
expect(cypressConfig).toContain(
`../../../dist/cypress/apps/${newPath}-e2e/videos`
);
expect(cypressJson.screenshotsFolder).toEqual(
expect(cypressConfig).toContain(
`../../../dist/cypress/apps/${newPath}-e2e/screenshots`
);
});

View File

@ -2,7 +2,6 @@ import {
checkFilesExist,
killPorts,
newProject,
readFile,
readJson,
runCLI,
uniq,
@ -10,11 +9,11 @@ import {
} from '@nrwl/e2e/utils';
describe('Cypress E2E Test runner', () => {
beforeEach(() => newProject());
it('should generate an app with the Cypress as e2e test runner', () => {
const myapp = uniq('myapp');
beforeAll(() => {
newProject();
const myapp = uniq('myapp');
});
it('should generate an app with the Cypress as e2e test runner', () => {
runCLI(
`generate @nrwl/react:app ${myapp} --e2eTestRunner=cypress --linter=eslint`
);
@ -24,39 +23,40 @@ describe('Cypress E2E Test runner', () => {
expect(packageJson.devDependencies['cypress']).toBeTruthy();
// Making sure the cypress folders & files are created
checkFilesExist(`apps/${myapp}-e2e/cypress.json`);
checkFilesExist(`apps/${myapp}-e2e/cypress.config.ts`);
checkFilesExist(`apps/${myapp}-e2e/tsconfig.json`);
checkFilesExist(`apps/${myapp}-e2e/src/fixtures/example.json`);
checkFilesExist(`apps/${myapp}-e2e/src/integration/app.spec.ts`);
checkFilesExist(`apps/${myapp}-e2e/src/e2e/app.cy.ts`);
checkFilesExist(`apps/${myapp}-e2e/src/support/app.po.ts`);
checkFilesExist(`apps/${myapp}-e2e/src/support/index.ts`);
checkFilesExist(`apps/${myapp}-e2e/src/support/e2e.ts`);
checkFilesExist(`apps/${myapp}-e2e/src/support/commands.ts`);
}, 1000000);
it('should execute e2e tests using Cypress', async () => {
newProject();
const myapp = uniq('myapp');
runCLI(
`generate @nrwl/react:app ${myapp} --e2eTestRunner=cypress --linter=eslint`
);
// contains the correct output and works
const run1 = runCLI(`e2e ${myapp}-e2e --no-watch`);
expect(run1).toContain('All specs passed!');
expect(runCLI(`e2e ${myapp}-e2e --no-watch`)).toContain(
'All specs passed!'
);
await killPorts(4200);
const originalContents = JSON.parse(
readFile(`apps/${myapp}-e2e/cypress.json`)
);
delete originalContents.fixturesFolder;
// tests should not fail because of a config change
updateFile(
`apps/${myapp}-e2e/cypress.json`,
JSON.stringify(originalContents)
`apps/${myapp}-e2e/cypress.config.ts`,
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
fixturesFolder: undefined,
}
,
});`
);
expect(runCLI(`e2e ${myapp}-e2e --no-watch`)).toContain(
'All specs passed!'
);
const run2 = runCLI(`e2e ${myapp}-e2e --no-watch`);
expect(run2).toContain('All specs passed!');
expect(await killPorts(4200)).toBeTruthy();
}, 1000000);
});

View File

@ -1,4 +1,3 @@
import { stringUtils } from '@nrwl/workspace';
import {
checkFilesExist,
cleanupProject,
@ -18,6 +17,7 @@ import {
updateFile,
updateProjectConfig,
} from '@nrwl/e2e/utils';
import { stringUtils } from '@nrwl/workspace';
import * as http from 'http';
describe('Next.js Applications', () => {
@ -112,7 +112,7 @@ describe('Next.js Applications', () => {
)}`
);
const e2eTestPath = `apps/${appName}-e2e/src/integration/app.spec.ts`;
const e2eTestPath = `apps/${appName}-e2e/src/e2e/app.cy.ts`;
const e2eContent = readFile(e2eTestPath);
updateFile(
e2eTestPath,

View File

@ -0,0 +1,33 @@
import { newProject, runCLI, uniq } from '../../utils';
describe('React Cypress Component Tests', () => {
beforeAll(() => newProject());
it('should successfully test react app', () => {
const appName = uniq('cy-react-app');
runCLI(`generate @nrwl/react:app ${appName} --no-interactive`);
runCLI(
`generate @nrwl/react:component fancy-component --project=${appName} --no-interactive`
);
runCLI(
`generate @nrwl/react:cypress-component-configuration --project=${appName} --generate-tests`
);
expect(runCLI(`component-test ${appName} --no-watch`)).toContain(
'All specs passed!'
);
}, 1000000);
it('should successfully test react app', () => {
const libName = uniq('cy-react-lib');
runCLI(`generate @nrwl/react:lib ${libName} --component --no-interactive`);
runCLI(
`generate @nrwl/react:component fancy-component --project=${libName} --no-interactive`
);
runCLI(
`generate @nrwl/react:cypress-component-configuration --project=${libName} --generate-tests`
);
expect(runCLI(`component-test ${libName} --no-watch`)).toContain(
'All specs passed!'
);
}, 1000000);
});

View File

@ -702,7 +702,7 @@ export function checkFilesDoNotExist(...expectedFiles: string[]) {
expectedFiles.forEach((f) => {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
if (exists(ff)) {
throw new Error(`File '${ff}' does not exist`);
throw new Error(`File '${ff}' should not exist`);
}
});
}

View File

@ -46,7 +46,8 @@
"@angular/service-worker": "~14.0.0",
"@angular/upgrade": "~14.0.0",
"@babel/helper-create-regexp-features-plugin": "^7.14.5",
"@cypress/webpack-preprocessor": "^5.9.1",
"@cypress/react": "^6.0.0",
"@cypress/webpack-preprocessor": "^5.12.0",
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
@ -183,6 +184,7 @@
"license-webpack-plugin": "^4.0.2",
"loader-utils": "1.2.3",
"lockfile-lint": "^4.7.6",
"magic-string": "~0.26.2",
"memfs": "^3.0.1",
"metro-resolver": "^0.71.2",
"mime": "2.4.4",

View File

@ -19,7 +19,8 @@
"tsconfig-paths",
"semver",
"webpack",
"http-server"
"http-server",
"magic-string"
],
"keepLifecycleScripts": true
}

View File

@ -52,6 +52,7 @@
"http-server": "^14.1.0",
"ignore": "^5.0.4",
"jasmine-marbles": "~0.8.4",
"magic-string": "~0.26.2",
"rxjs-for-await": "0.0.2",
"semver": "7.3.4",
"ts-node": "~10.8.0",

View File

@ -139,8 +139,9 @@ Object {
},
},
"options": Object {
"cypressConfig": "apps/my-dir/my-app-e2e/cypress.json",
"cypressConfig": "apps/my-dir/my-app-e2e/cypress.config.ts",
"devServerTarget": "my-dir-my-app:serve:development",
"testingType": "e2e",
},
},
"lint": Object {
@ -192,6 +193,7 @@ Object {
"include": Array [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`;
@ -309,8 +311,9 @@ Object {
},
},
"options": Object {
"cypressConfig": "apps/my-app-e2e/cypress.json",
"cypressConfig": "apps/my-app-e2e/cypress.config.ts",
"devServerTarget": "my-app:serve:development",
"testingType": "e2e",
},
},
"lint": Object {

View File

@ -1,3 +1,4 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type { Tree } from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';
import {
@ -18,11 +19,16 @@ import {
} from '../../utils/versions';
import { applicationGenerator } from './application';
import type { Schema } from './schema';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('app', () => {
let appTree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
appTree = createTreeWithEmptyWorkspace();
});
@ -104,7 +110,7 @@ describe('app', () => {
);
expect(eslintrcJson.extends).toEqual(['../../.eslintrc.json']);
expect(appTree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
expect(appTree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy();
const tsconfigE2E = parseJson(
appTree.read('apps/my-app-e2e/tsconfig.json', 'utf-8')
);
@ -277,7 +283,7 @@ describe('app', () => {
'apps/my-dir/my-app/src/main.ts',
'apps/my-dir/my-app/src/app/app.module.ts',
'apps/my-dir/my-app/src/app/app.component.ts',
'apps/my-dir/my-app-e2e/cypress.json',
'apps/my-dir/my-app-e2e/cypress.config.ts',
].forEach((path) => {
expect(appTree.exists(path)).toBeTruthy();
});
@ -360,7 +366,7 @@ describe('app', () => {
'my-dir/my-app/src/main.ts',
'my-dir/my-app/src/app/app.module.ts',
'my-dir/my-app/src/app/app.component.ts',
'my-dir/my-app-e2e/cypress.json',
'my-dir/my-app-e2e/cypress.config.ts',
].forEach((path) => {
expect(appTree.exists(path)).toBeTruthy();
});

View File

@ -1,5 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`componentCypressSpec generator should generate .spec.ts when using cypress.json 1`] = `
"describe('ng-app1', () => {
beforeEach(() => cy.visit('/iframe.html?id=testbuttoncomponent--primary&args=buttonType:button;style:default;age;isOn:false;'));
it('should render the component', () => {
cy.get('proj-test-button').should('exist');
});
});"
`;
exports[`componentCypressSpec generator should generate the component spec file 1`] = `
"describe('ng-app1', () => {
beforeEach(() => cy.visit('/iframe.html?id=testbuttoncomponent--primary&args=buttonType:button;style:default;age;isOn:false;'));

View File

@ -1,16 +1,21 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type { Tree } from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { applicationGenerator } from '../application/application';
import * as storybookUtils from '../utils/storybook';
import { componentCypressSpecGenerator } from './component-cypress-spec';
import { applicationGenerator } from '../application/application';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('componentCypressSpec generator', () => {
let tree: Tree;
const appName = 'ng-app1';
const specFile = `apps/${appName}-e2e/src/integration/test-button/test-button.component.spec.ts`;
const specFile = `apps/${appName}-e2e/src/e2e/test-button/test-button.component.cy.ts`;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
@ -46,6 +51,7 @@ export class TestButtonComponent {
});
it('should not generate the component spec file when it already exists', () => {
mockedInstalledCypressVersion.mockReturnValue(10);
jest.spyOn(storybookUtils, 'getComponentProps');
jest.spyOn(devkit, 'generateFiles');
tree.write(specFile, '');
@ -64,6 +70,7 @@ export class TestButtonComponent {
});
it('should generate the component spec file', () => {
mockedInstalledCypressVersion.mockReturnValue(10);
componentCypressSpecGenerator(tree, {
componentFileName: 'test-button.component',
componentName: 'TestButtonComponent',
@ -76,4 +83,23 @@ export class TestButtonComponent {
const specFileContent = tree.read(specFile).toString();
expect(specFileContent).toMatchSnapshot();
});
it('should generate .spec.ts when using cypress.json', () => {
mockedInstalledCypressVersion.mockReturnValue(9);
const v9SpecFile = `apps/${appName}-e2e/src/integration/test-button/test-button.component.spec.ts`;
tree.delete(`apps/${appName}-e2e/cypress.config.ts`);
tree.write(`apps/${appName}-e2e/cypress.json`, `{}`);
componentCypressSpecGenerator(tree, {
componentFileName: 'test-button.component',
componentName: 'TestButtonComponent',
componentPath: `test-button`,
projectPath: `apps/${appName}/src/app`,
projectName: appName,
});
expect(tree.exists(v9SpecFile)).toBe(true);
const specFileContent = tree.read(v9SpecFile).toString();
expect(specFileContent).toMatchSnapshot();
});
});

View File

@ -6,8 +6,8 @@ import {
readProjectConfiguration,
} from '@nrwl/devkit';
import { getComponentProps } from '../utils/storybook';
import { getComponentSelector } from './lib/get-component-selector';
import { getArgsDefaultValue } from './lib/get-args-default-value';
import { getComponentSelector } from './lib/get-component-selector';
import type { ComponentCypressSpecGeneratorOptions } from './schema';
export function componentCypressSpecGenerator(
@ -24,13 +24,13 @@ export function componentCypressSpecGenerator(
specDirectory,
} = options;
const e2eProjectName = cypressProject || `${projectName}-e2e`;
const e2eProjectRoot = readProjectConfiguration(
tree,
e2eProjectName
).sourceRoot;
const { sourceRoot, root } = readProjectConfiguration(tree, e2eProjectName);
const isCypressV10 = tree.exists(
joinPathFragments(root, 'cypress.config.ts')
);
const e2eLibIntegrationFolderPath = joinPathFragments(
e2eProjectRoot,
'integration'
sourceRoot,
isCypressV10 ? 'e2e' : 'integration'
);
const templatesDir = joinPathFragments(__dirname, 'files');
@ -40,7 +40,7 @@ export function componentCypressSpecGenerator(
);
const storyFile = joinPathFragments(
destinationDir,
`${componentFileName}.spec.ts`
`${componentFileName}.${isCypressV10 ? 'cy' : 'spec'}.ts`
);
if (tree.exists(storyFile)) {
@ -61,7 +61,7 @@ export function componentCypressSpecGenerator(
componentName,
componentSelector,
props,
tmpl: '',
fileExt: isCypressV10 ? 'cy.ts' : 'spec.ts',
});
if (!options.skipFormat) {

View File

@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`e2e migrator cypress with project root at "" cypress version >=10 should add paths to the e2e config from the global config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
fixturesFolder: 'src/my-fixtures',
specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
e2e: {...nxE2EPreset(__dirname),fixturesFolder: 'src/my-fixtures',specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4200',
},
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should keep paths in the e2e config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
fixturesFolder: 'src/my-fixtures',
specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should remove paths in the e2e config when they match the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
},
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should update e2e config with the nx preset 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200'
},
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should update paths in the config 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
fixturesFolder: 'src/fixtures',
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
fixturesFolder: 'src/test-data',
specPattern: 'src/e2e/**/*.cy.ts',
},
component: {
supportFile: 'src/support/component.ts',
}
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should add paths to the e2e config from the global config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
fixturesFolder: 'src/my-fixtures',
specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
e2e: {...nxE2EPreset(__dirname),fixturesFolder: 'src/my-fixtures',specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4200',
},
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should keep paths in the e2e config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
fixturesFolder: 'src/my-fixtures',
specPattern: 'src/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should remove paths in the e2e config when they match the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
},
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should update e2e config with the nx preset 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200'
},
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should update paths in the config 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
fixturesFolder: 'src/fixtures',
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200',
fixturesFolder: 'src/test-data',
specPattern: 'src/e2e/**/*.cy.ts',
},
component: {
supportFile: 'src/support/component.ts',
}
});"
`;

View File

@ -1,3 +1,7 @@
// mock so we can test multiple versions
jest.mock('@nrwl/cypress/src/utils/cypress-version');
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
joinPathFragments,
offsetFromRoot,
@ -24,6 +28,9 @@ const mockedLogger = { warn: jest.fn() };
describe('e2e migrator', () => {
let tree: Tree;
let mockedInstalledCypressVersion = installedCypressVersion as jest.Mock<
ReturnType<typeof installedCypressVersion>
>;
function addProject(
name: string,
@ -47,6 +54,8 @@ describe('e2e migrator', () => {
tree.delete('workspace.json');
writeJson(tree, 'angular.json', { version: 2, projects: {} });
mockedInstalledCypressVersion.mockReturnValue(9);
jest.clearAllMocks();
});
@ -144,26 +153,6 @@ describe('e2e migrator', () => {
expect(result).toBe(null);
});
it('should fail validation when using Cypress and the cypress.json file does not exist', async () => {
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
expect(result).toHaveLength(1);
expect(result[0].message).toBe(
'The "e2e" target is using the "@cypress/schematic:cypress" builder but the "configFile" option is not specified and a "cypress.json" file could not be found at the project root.'
);
expect(result[0].hint).toBe(
'Make sure the "app1.architect.e2e.options.configFile" option is set to a valid path, or that a "cypress.json" file exists at the project root, or remove the "app1.architect.e2e" target if it is not valid.'
);
});
it('should fail validation when using Cypress and the specified config file does not exist', async () => {
const project = addProject('app1', {
root: '',
@ -208,20 +197,105 @@ describe('e2e migrator', () => {
);
});
it('should succeed validation when using Cypress', async () => {
writeJson(tree, 'cypress.json', {});
writeJson(tree, 'cypress/tsconfig.json', {});
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
describe('cypress version <10', () => {
it('should fail validation when using Cypress and the cypress.json file does not exist', async () => {
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
expect(result).toHaveLength(1);
expect(result[0].message).toBe(
'The "e2e" target is using the "@cypress/schematic:cypress" builder but the "configFile" option is not specified and a "cypress.json" file could not be found at the project root.'
);
expect(result[0].hint).toBe(
'Make sure the "app1.architect.e2e.options.configFile" option is set to a valid path, or that a "cypress.json" file exists at the project root, or remove the "app1.architect.e2e" target if it is not valid.'
);
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
it('should fail validation when using Cypress and the specified config file does not exist', async () => {
const project = addProject('app1', {
root: '',
architect: {
e2e: {
builder: '@cypress/schematic:cypress',
options: { configFile: 'cypress.conf.json' },
},
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
expect(result).toBe(null);
const result = migrator.validate();
expect(result).toHaveLength(1);
expect(result[0].message).toBe(
'The specified Cypress config file "cypress.conf.json" in the "e2e" target could not be found.'
);
expect(result[0].hint).toBe(
'Make sure the "app1.architect.e2e.options.configFile" option is set to a valid path or remove the "app1.architect.e2e" target if it is not valid.'
);
});
it('should succeed validation when using Cypress', async () => {
writeJson(tree, 'cypress.json', {});
writeJson(tree, 'cypress/tsconfig.json', {});
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
expect(result).toBe(null);
});
});
describe('cypress version >=10', () => {
it('should fail validation when using Cypress and a cypress.config.{ts,js,mjs,cjs} file does not exist', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
expect(result).toHaveLength(1);
expect(result[0].message).toBe(
'The "e2e" target is using the "@cypress/schematic:cypress" builder but the "configFile" option is not specified and a "cypress.config.{ts,js,mjs,cjs}" file could not be found at the project root.'
);
expect(result[0].hint).toBe(
'Make sure the "app1.architect.e2e.options.configFile" option is set to a valid path, or that a "cypress.config.{ts,js,mjs,cjs}" file exists at the project root, or remove the "app1.architect.e2e" target if it is not valid.'
);
});
it('should succeed validation when using Cypress', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
tree.write('cypress.config.ts', '');
writeJson(tree, 'cypress/tsconfig.json', {});
const project = addProject('app1', {
root: '',
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
const result = migrator.validate();
expect(result).toBe(null);
});
});
});
@ -437,6 +511,8 @@ describe('e2e migrator', () => {
await migrator.migrate();
expect(tree.exists('cypress.json')).toBe(false);
expect(tree.exists('cypress/tsconfig.json')).toBe(false);
expect(tree.exists('apps/app1-e2e/cypress.json')).toBe(true);
expect(tree.exists('apps/app1-e2e/tsconfig.json')).toBe(true);
});
@ -660,68 +736,325 @@ describe('e2e migrator', () => {
expect(tsConfigJson.compilerOptions.outDir).toBe('../../dist/out-tsc');
});
it('should create a cypress.json file when it does not exist', async () => {
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
const project = addProject('app1', {
root,
architect: {
e2e: {
builder: '@cypress/schematic:cypress',
options: { configFile: false },
describe('cypress version <10', () => {
it('should create a cypress.json file when it does not exist', async () => {
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
const project = addProject('app1', {
root,
architect: {
e2e: {
builder: '@cypress/schematic:cypress',
options: { configFile: false },
},
},
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.json')).toBe(true);
const cypressJson = readJson(tree, 'apps/app1-e2e/cypress.json');
expect(cypressJson).toStrictEqual({
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
supportFile: './src/support/index.ts',
pluginsFile: false,
video: true,
videosFolder: `../../dist/cypress/apps/app1-e2e/videos`,
screenshotsFolder: `../../dist/cypress/apps/app1-e2e/screenshots`,
chromeWebSecurity: false,
});
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
it('should update the cypress.json file', async () => {
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
writeJson(tree, joinPathFragments(root, 'cypress.json'), {
integrationFolder: 'cypress/integration',
supportFile: 'cypress/support/index.ts',
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
pluginsFile: 'cypress/plugins/index.ts',
fixturesFolder: 'cypress/fixtures',
baseUrl: 'http://localhost:4200',
});
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
expect(tree.exists('apps/app1-e2e/cypress.json')).toBe(true);
const cypressJson = readJson(tree, 'apps/app1-e2e/cypress.json');
expect(cypressJson).toStrictEqual({
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
supportFile: './src/support/index.ts',
pluginsFile: './src/plugins/index.ts',
video: true,
videosFolder: `../../dist/cypress/apps/app1-e2e/videos`,
screenshotsFolder: `../../dist/cypress/apps/app1-e2e/screenshots`,
chromeWebSecurity: false,
await migrator.migrate();
const cypressJson = readJson(tree, 'apps/app1-e2e/cypress.json');
expect(cypressJson).toStrictEqual({
integrationFolder: 'src/integration',
supportFile: 'src/support/index.ts',
videosFolder: `../../dist/cypress/apps/app1-e2e/videos`,
screenshotsFolder: `../../dist/cypress/apps/app1-e2e/screenshots`,
pluginsFile: 'src/plugins/index.ts',
fixturesFolder: 'src/fixtures',
baseUrl: 'http://localhost:4200',
fileServerFolder: '.',
});
});
});
it('should update the cypress.json file', async () => {
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
writeJson(tree, joinPathFragments(root, 'cypress.json'), {
integrationFolder: joinPathFragments(root, 'cypress/integration'),
supportFile: joinPathFragments(root, 'cypress/support/index.ts'),
videosFolder: joinPathFragments(root, 'cypress/videos'),
screenshotsFolder: joinPathFragments(root, 'cypress/screenshots'),
pluginsFile: joinPathFragments(root, 'cypress/plugins/index.ts'),
fixturesFolder: joinPathFragments(root, 'cypress/fixtures'),
baseUrl: 'http://localhost:4200',
});
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
describe('cypress version >=10', () => {
it('should create a cypress.config.ts file when it does not exist', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
const project = addProject('app1', {
root,
architect: {
e2e: {
builder: '@cypress/schematic:cypress',
options: {},
},
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
await migrator.migrate();
const cypressJson = readJson(tree, 'apps/app1-e2e/cypress.json');
expect(cypressJson).toStrictEqual({
integrationFolder: './src/integration',
supportFile: './src/support/index.ts',
videosFolder: `../../dist/cypress/apps/app1-e2e/videos`,
screenshotsFolder: `../../dist/cypress/apps/app1-e2e/screenshots`,
pluginsFile: './src/plugins/index.ts',
fixturesFolder: './src/fixtures',
baseUrl: 'http://localhost:4200',
fileServerFolder: '.',
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toBe(`import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
`);
});
it('should update e2e config with the nx preset', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200'
},
});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should update paths in the config', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
fixturesFolder: 'cypress/fixtures',
specPattern: 'cypress/**/*.cy.{js,jsx,ts,tsx}',
e2e: {
baseUrl: 'http://localhost:4200',
fixturesFolder: 'cypress/test-data',
specPattern: 'cypress/e2e/**/*.cy.ts',
},
component: {
supportFile: 'cypress/support/component.ts',
}
});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should remove paths in the e2e config when they match the nx preset defaults', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200',
fixturesFolder: 'cypress/fixtures',
specPattern: 'cypress/**/*.cy.{js,jsx,ts,tsx}',
},
});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should keep paths in the e2e config when they differ from the nx preset defaults', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200',
fixturesFolder: 'cypress/my-fixtures',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should add paths to the e2e config from the global config when they differ from the nx preset defaults', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
fixturesFolder: 'cypress/my-fixtures',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
e2e: {
baseUrl: 'http://localhost:4200',
},
});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await migrator.migrate();
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(true);
const cypressConfig = tree.read(
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should not throw when the e2e config is not defined', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({});`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await expect(migrator.migrate()).resolves.not.toThrow();
});
it('should not throw when the e2e config is not an object literal', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
const e2e = {};
export default defineConfig({ e2e });`
);
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await expect(migrator.migrate()).resolves.not.toThrow();
});
it('should not throw when the "defineConfig" call is not found', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
writeJson(tree, joinPathFragments(root, 'cypress/tsconfig.json'), {});
tree.write(joinPathFragments(root, 'cypress.config.ts'), '');
const project = addProject('app1', {
root,
architect: {
e2e: { builder: '@cypress/schematic:cypress', options: {} },
},
});
const migrator = new E2eMigrator(tree, {}, project, undefined);
await expect(migrator.migrate()).resolves.not.toThrow();
});
});
}

View File

@ -1,4 +1,6 @@
import { cypressProjectGenerator } from '@nrwl/cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
addProjectConfiguration,
joinPathFragments,
@ -8,6 +10,7 @@ import {
readJson,
readProjectConfiguration,
removeProjectConfiguration,
stripIndents,
TargetConfiguration,
Tree,
updateJson,
@ -16,9 +19,22 @@ import {
writeJson,
} from '@nrwl/devkit';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils';
import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript';
import { tsquery } from '@phenomnomnominal/tsquery';
import { basename, relative } from 'path';
import {
isObjectLiteralExpression,
isPropertyAssignment,
isStringLiteralLike,
isTemplateExpression,
Node,
ObjectLiteralExpression,
PropertyAssignment,
SyntaxKind,
} from 'typescript';
import { GeneratorOptions } from '../schema';
import { FileChangeRecorder } from './file-change-recorder';
import { Logger } from './logger';
import { ProjectMigrator } from './project.migrator';
import {
@ -38,10 +54,32 @@ const supportedTargets: Record<SupportedTargets, Target> = {
},
};
type CypressCommonConfig = {
fixturesFolder?: string;
screenshotsFolder?: string;
specPattern?: string;
videosFolder?: string;
};
const cypressConfig = {
v9OrLess: {
srcPaths: ['supportFile', 'supportFolder', 'fixturesFolder'],
distPaths: ['videosFolder', 'screenshotsFolder', 'downloadsFolder'],
globPatterns: ['excludeSpecPattern', 'specPattern'],
},
v10OrMore: {
srcPaths: ['supportFile', 'supportFolder', 'fixturesFolder'],
distPaths: ['videosFolder', 'screenshotsFolder', 'downloadsFolder'],
globPatterns: ['excludeSpecPattern', 'specPattern'],
},
};
export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
private appConfig: ProjectConfiguration;
private appName: string;
private isProjectUsingEsLint: boolean;
private cypressInstalledVersion: number;
private cypressPreset: ReturnType<typeof nxE2EPreset>;
constructor(
tree: Tree,
@ -146,20 +184,20 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
if (this.isCypressE2eProject()) {
const configFile =
this.projectConfig.targets[this.targetNames.e2e].options?.configFile;
if (
configFile === undefined &&
!this.tree.exists(
joinPathFragments(this.project.oldRoot, 'cypress.json')
)
) {
if (configFile === undefined && !this.getOldCypressConfigFilePath()) {
const expectedConfigFile =
this.cypressInstalledVersion < 10
? 'cypress.json'
: 'cypress.config.{ts,js,mjs,cjs}';
return [
{
message:
`The "e2e" target is using the "@cypress/schematic:cypress" builder but the "configFile" option is not specified ` +
`and a "cypress.json" file could not be found at the project root.`,
`and a "${expectedConfigFile}" file could not be found at the project root.`,
hint:
`Make sure the "${this.appName}.architect.e2e.options.configFile" option is set to a valid path, ` +
`or that a "cypress.json" file exists at the project root, ` +
`or that a "${expectedConfigFile}" file exists at the project root, ` +
`or remove the "${this.appName}.architect.e2e" target if it is not valid.`,
},
];
@ -217,6 +255,7 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
newSourceRoot,
};
} else if (this.isCypressE2eProject()) {
this.cypressInstalledVersion = installedCypressVersion();
this.project = {
...this.project,
name,
@ -280,7 +319,7 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
}
private async migrateCypressE2eProject(): Promise<void> {
const oldCypressConfigFilePath = this.getCypressConfigFile();
const oldCypressConfigFilePath = this.getOldCypressConfigFilePath();
await cypressProjectGenerator(this.tree, {
name: this.project.name,
@ -317,35 +356,18 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
}
private updateOrCreateCypressConfigFile(configFile: string): string {
let cypressConfigFilePath: string;
if (configFile) {
cypressConfigFilePath = joinPathFragments(
this.project.newRoot,
basename(configFile)
);
this.updateCypressConfigFilePaths(configFile);
this.tree.delete(cypressConfigFilePath);
this.moveFile(configFile, cypressConfigFilePath);
} else {
cypressConfigFilePath = joinPathFragments(
this.project.newRoot,
'cypress.json'
);
writeJson(this.tree, cypressConfigFilePath, {
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
supportFile: './src/support/index.ts',
pluginsFile: './src/plugins/index.ts',
video: true,
videosFolder: `../../dist/cypress/${this.project.newRoot}/videos`,
screenshotsFolder: `../../dist/cypress/${this.project.newRoot}/screenshots`,
chromeWebSecurity: false,
});
if (!configFile) {
return this.getDefaultCypressConfigFilePath();
}
const cypressConfigFilePath = joinPathFragments(
this.project.newRoot,
basename(configFile)
);
this.updateCypressConfigFilePaths(configFile);
this.tree.delete(cypressConfigFilePath);
this.moveFile(configFile, cypressConfigFilePath);
return cypressConfigFilePath;
}
@ -441,46 +463,50 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
}
private updateCypressConfigFilePaths(configFilePath: string): void {
const srcFoldersAndFiles = [
if (this.cypressInstalledVersion >= 10) {
this.updateCypress10ConfigFile(configFilePath);
return;
}
const srcPaths = [
'integrationFolder',
'supportFile',
'pluginsFile',
'fixturesFolder',
];
const distFolders = ['videosFolder', 'screenshotsFolder'];
const stringOrArrayGlobs = ['ignoreTestFiles', 'testFiles'];
const distPaths = ['videosFolder', 'screenshotsFolder'];
const globPatterns = ['ignoreTestFiles', 'testFiles'];
const cypressConfig = readJson(this.tree, configFilePath);
cypressConfig.fileServerFolder = '.';
srcFoldersAndFiles.forEach((folderOrFile) => {
if (cypressConfig[folderOrFile]) {
cypressConfig[folderOrFile] = `./src/${relative(
this.project.oldSourceRoot,
cypressConfig[folderOrFile]
)}`;
srcPaths.forEach((path) => {
if (cypressConfig[path]) {
cypressConfig[path] = this.cypressConfigSrcPathToNewPath(
cypressConfig[path]
);
}
});
distFolders.forEach((folder) => {
if (cypressConfig[folder]) {
cypressConfig[folder] = `../../dist/cypress/${
this.project.newRoot
}/${relative(this.project.oldSourceRoot, cypressConfig[folder])}`;
distPaths.forEach((path) => {
if (cypressConfig[path]) {
cypressConfig[path] = this.cypressConfigDistPathToNewPath(
cypressConfig[path]
);
}
});
stringOrArrayGlobs.forEach((stringOrArrayGlob) => {
globPatterns.forEach((stringOrArrayGlob) => {
if (!cypressConfig[stringOrArrayGlob]) {
return;
}
if (Array.isArray(cypressConfig[stringOrArrayGlob])) {
cypressConfig[stringOrArrayGlob] = cypressConfig[stringOrArrayGlob].map(
(glob: string) => this.replaceCypressGlobConfig(glob)
(glob: string) => this.cypressConfigGlobToNewGlob(glob)
);
} else {
cypressConfig[stringOrArrayGlob] = this.replaceCypressGlobConfig(
cypressConfig[stringOrArrayGlob] = this.cypressConfigGlobToNewGlob(
cypressConfig[stringOrArrayGlob]
);
}
@ -489,25 +515,388 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
writeJson(this.tree, configFilePath, cypressConfig);
}
private replaceCypressGlobConfig(globPattern: string): string {
return globPattern.replace(
new RegExp(`^(\\.\\/|\\/)?${this.project.oldSourceRoot}\\/`),
'./src/'
);
private cypressConfigGlobToNewGlob(
globPattern: string | undefined
): string | undefined {
return globPattern
? globPattern.replace(
new RegExp(
`^(\\.\\/|\\/)?${relative(
this.project.oldRoot,
this.project.oldSourceRoot
)}\\/`
),
'src/'
)
: undefined;
}
private getCypressConfigFile(): string | undefined {
let cypressConfig = joinPathFragments(this.project.oldRoot, 'cypress.json');
private cypressConfigSrcPathToNewPath(
path: string | undefined
): string | undefined {
return path
? joinPathFragments(
'src',
relative(
this.project.oldSourceRoot,
joinPathFragments(this.project.oldRoot, path)
)
)
: undefined;
}
private cypressConfigDistPathToNewPath(
path: string | undefined
): string | undefined {
return path
? joinPathFragments(
'../../dist/cypress/',
this.project.newRoot,
relative(
this.project.oldSourceRoot,
joinPathFragments(this.project.oldRoot, path)
)
)
: undefined;
}
private updateCypress10ConfigFile(configFilePath: string): void {
this.cypressPreset = nxE2EPreset(this.project.newRoot);
const fileContent = this.tree.read(configFilePath, 'utf-8');
let sourceFile = tsquery.ast(fileContent);
const recorder = new FileChangeRecorder(this.tree, configFilePath);
const defineConfigExpression = tsquery.query(
sourceFile,
'CallExpression:has(Identifier[name=defineConfig]) > ObjectLiteralExpression'
)[0] as ObjectLiteralExpression;
if (!defineConfigExpression) {
this.logger.warn(
`Could not find a "defineConfig" expression in "${configFilePath}". Skipping updating the Cypress configuration.`
);
return;
}
let e2eNode: PropertyAssignment;
let componentNode: PropertyAssignment;
const globalConfig: CypressCommonConfig = {};
defineConfigExpression.forEachChild((node: Node) => {
if (isPropertyAssignment(node) && node.name.getText() === 'component') {
componentNode = node;
return;
}
if (isPropertyAssignment(node) && node.name.getText() === 'e2e') {
e2eNode = node;
return;
}
if (isPropertyAssignment(node)) {
this.updateCypressConfigNodeValue(recorder, node, globalConfig);
}
});
this.updateCypressComponentConfig(componentNode, recorder);
this.updateCypressE2EConfig(
configFilePath,
defineConfigExpression,
e2eNode,
recorder,
globalConfig
);
recorder.applyChanges();
}
private updateCypressComponentConfig(
componentNode: PropertyAssignment,
recorder: FileChangeRecorder
): void {
if (!componentNode) {
return;
}
if (!isObjectLiteralExpression(componentNode.initializer)) {
this.logger.warn(
'The automatic migration only supports having an object literal in the "component" option of the Cypress configuration. ' +
`The configuration won't be updated. Please make sure to update any paths you may have in the "component" option ` +
'manually to point to the new location.'
);
return;
}
componentNode.initializer.properties.forEach((node: Node) => {
if (isPropertyAssignment(node)) {
this.updateCypressConfigNodeValue(recorder, node);
}
});
}
private updateCypressE2EConfig(
configFilePath: string,
defineConfigNode: ObjectLiteralExpression,
e2eNode: PropertyAssignment,
recorder: FileChangeRecorder,
{ ...globalConfig }: CypressCommonConfig
): void {
const e2eConfig = {};
const presetSpreadAssignment = `...nxE2EPreset(__dirname),`;
if (!e2eNode) {
// add the e2e node with the preset and properties that need to overwrite
// the preset
const e2eAssignment = stripIndents`e2e: {
${presetSpreadAssignment}
${Object.entries(globalConfig)
.filter(
([key, value]) =>
!e2eConfig[key] && value !== this.cypressPreset[key]
)
.map(([key, value]) => `${key}: '${value}'`)
.join(',\n')}
},`;
recorder.insertRight(defineConfigNode.getStart() + 1, e2eAssignment);
} else {
if (!isObjectLiteralExpression(e2eNode.initializer)) {
this.logger.warn(
'The automatic migration only supports having an object literal in the "e2e" option of the Cypress configuration. ' +
`The configuration won't be updated. Please make sure to update any paths you might have in the "e2e" option ` +
'manually to point to the new location.'
);
return;
}
recorder.insertRight(
e2eNode.initializer.getStart() + 1,
presetSpreadAssignment
);
e2eNode.initializer.properties.forEach((node: Node) => {
if (!isPropertyAssignment(node)) {
return;
}
let change: {
type: 'replace' | 'remove' | 'ignore';
value?: string;
} = { type: 'ignore' };
const property = this.normalizeNodeText(node.name.getText());
const oldValue = this.normalizeNodeText(node.initializer.getText());
e2eConfig[property] = oldValue;
const createChange = (newValue: string): typeof change => {
if (newValue === this.cypressPreset[property]) {
return { type: 'remove' };
}
return { type: 'replace', value: newValue };
};
if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.srcPaths
)
) {
const newValue = this.cypressConfigSrcPathToNewPath(oldValue);
change = createChange(newValue);
} else if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.distPaths
)
) {
const newValue = this.cypressConfigDistPathToNewPath(oldValue);
change = createChange(newValue);
} else if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.globPatterns
)
) {
const newValue = this.cypressConfigGlobToNewGlob(oldValue);
change = createChange(newValue);
}
if (change.type === 'replace') {
recorder.replace(node.initializer, `'${change.value}'`);
e2eConfig[property] = change.value;
} else if (change.type === 'remove') {
const trailingCommaMatch = recorder.originalContent
.slice(node.getEnd())
.match(/^\s*,/);
if (trailingCommaMatch) {
recorder.remove(
node.getFullStart(),
node.getEnd() + trailingCommaMatch[0].length
);
} else {
recorder.remove(node.getFullStart(), node.getEnd());
}
delete e2eConfig[property];
delete globalConfig[property];
}
});
// add any global config that was present and that would be overwritten
// by the preset
Object.entries(globalConfig).forEach(([key, value]) => {
if (e2eConfig[key] || value === this.cypressPreset[key]) {
return;
}
recorder.insertRight(
e2eNode.initializer.getStart() + 1,
`${key}: '${value}',`
);
});
}
// apply changes so we can apply AST transformations
recorder.applyChanges();
const sourceFile = tsquery.ast(recorder.content);
insertImport(
this.tree,
sourceFile,
configFilePath,
'nxE2EPreset',
'@nrwl/cypress/plugins/cypress-preset'
);
// update recorder with the new content from the file
recorder.setContentToFileContent();
}
private updateCypressConfigNodeValue(
recorder: FileChangeRecorder,
node: PropertyAssignment,
configCollected?: CypressCommonConfig
): void {
let newValue: string;
const oldValue = this.normalizeNodeText(node.initializer.getText());
if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.srcPaths
)
) {
newValue = this.cypressConfigSrcPathToNewPath(oldValue);
} else if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.distPaths
)
) {
newValue = this.cypressConfigDistPathToNewPath(oldValue);
} else if (
this.isValidPathLikePropertyWithStringLiteralValue(
node,
cypressConfig.v10OrMore.globPatterns
)
) {
newValue = this.cypressConfigGlobToNewGlob(oldValue);
}
if (newValue) {
recorder.replace(node.initializer, `'${newValue}'`);
if (configCollected) {
configCollected[node.name.getText()] = newValue;
}
}
}
private isValidPathLikePropertyWithStringLiteralValue(
node: Node,
properties: string[]
): boolean {
if (!isPropertyAssignment(node)) {
// TODO(leo): handle more scenarios (spread assignments, etc)
return false;
}
const property = properties.find((p) => p === node.name.getText());
if (!property) {
return false;
}
if (
node.initializer.kind === SyntaxKind.UndefinedKeyword ||
node.initializer.kind === SyntaxKind.NullKeyword ||
node.initializer.kind === SyntaxKind.FalseKeyword
) {
return false;
}
if (!isStringLiteralLike(node.initializer)) {
if (isTemplateExpression(node.initializer)) {
this.logger.warn(
`The "${node.name.getText()}" in the Cypress configuration file is set to a template expression ("${node.initializer.getText()}"). ` +
`This is not supported by the automatic migration and its value won't be automatically migrated. ` +
`Please make sure to update its value to match the new location if needed.`
);
} else {
this.logger.warn(
`The "${node.name.getText()}" in the Cypress configuration file is not set to a string literal ("${node.initializer.getText()}"). ` +
`This is not supported by the automatic migration and its value won't be automatically migrated. ` +
`Please make sure to update its value to match the new location if needed.`
);
}
return false;
}
return true;
}
private normalizeNodeText(value: string): string {
return value.replace(/['"`]/g, '');
}
private getOldCypressConfigFilePath(): string | null {
let cypressConfig: string | null;
const configFileOption = this.projectConfig.targets.e2e.options.configFile;
if (configFileOption === false) {
cypressConfig = undefined;
cypressConfig = null;
} else if (typeof configFileOption === 'string') {
cypressConfig = basename(configFileOption);
} else {
cypressConfig = this.findCypressConfigFilePath(this.project.oldRoot);
}
return cypressConfig;
}
private getDefaultCypressConfigFilePath(): string {
return this.cypressInstalledVersion < 10
? joinPathFragments(this.project.newRoot, 'cypress.json')
: joinPathFragments(this.project.newRoot, 'cypress.config.ts');
}
private findCypressConfigFilePath(dir: string): string | null {
if (this.cypressInstalledVersion < 10) {
return this.tree.exists(joinPathFragments(dir, 'cypress.json'))
? joinPathFragments(dir, 'cypress.json')
: null;
}
// https://docs.cypress.io/guides/references/configuration#Configuration-File
const possibleFiles = [
joinPathFragments(dir, 'cypress.config.ts'),
joinPathFragments(dir, 'cypress.config.js'),
joinPathFragments(dir, 'cypress.config.mjs'),
joinPathFragments(dir, 'cypress.config.cjs'),
];
for (const file of possibleFiles) {
if (this.tree.exists(file)) {
return file;
}
}
return null;
}
private isCypressE2eProject(): boolean {
return (
this.projectConfig.targets[this.targetNames.e2e].executor ===

View File

@ -0,0 +1,44 @@
import type { Tree } from '@nrwl/devkit';
import MagicString from 'magic-string';
import type { Node } from 'typescript';
export class FileChangeRecorder {
private mutableContent: MagicString;
get content(): string {
return this.mutableContent.toString();
}
get originalContent(): string {
return this.mutableContent.original;
}
constructor(private readonly tree: Tree, private readonly filePath: string) {
this.setContentToFileContent();
}
applyChanges(): void {
this.tree.write(this.filePath, this.mutableContent.toString());
}
insertLeft(index: number, content: string): void {
this.mutableContent.appendLeft(index, content);
}
insertRight(index: number, content: string): void {
this.mutableContent.appendRight(index, content);
}
remove(index: number, length: number): void {
this.mutableContent.remove(index, length);
}
replace(node: Node, content: string): void {
this.mutableContent.overwrite(node.getStart(), node.getEnd(), content);
}
setContentToFileContent(): void {
this.mutableContent = new MagicString(
this.tree.read(this.filePath, 'utf-8')
);
}
}

View File

@ -1,14 +1,21 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { applicationGenerator } from '../application/application';
import { scamGenerator } from '../scam/scam';
import { angularStoriesGenerator } from './stories';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('angularStories generator: applications', () => {
let tree: Tree;
const appName = 'test-app';
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
tree = createTreeWithEmptyWorkspace();
await applicationGenerator(tree, {
name: appName,
@ -58,7 +65,7 @@ describe('angularStories generator: applications', () => {
});
expect(
tree.exists(`apps/${appName}-e2e/src/integration/app.component.spec.ts`)
tree.exists(`apps/${appName}-e2e/src/e2e/app.component.cy.ts`)
).toBeTruthy();
});
});

View File

@ -1,3 +1,4 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type { Tree } from '@nrwl/devkit';
import { writeJson } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
@ -9,9 +10,18 @@ import { libraryGenerator } from '../library/library';
import { scamGenerator } from '../scam/scam';
import { createStorybookTestWorkspaceForLib } from '../utils/testing';
import { angularStoriesGenerator } from './stories';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('angularStories generator: libraries', () => {
const libName = 'test-ui-lib';
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
});
describe('Stories for empty Angular library', () => {
let tree: Tree;
@ -100,27 +110,27 @@ describe('angularStories generator: libraries', () => {
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/barrel-button/barrel-button.component.spec.ts`
`apps/${libName}-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/nested-button/nested-button.component.spec.ts`
`apps/${libName}-e2e/src/e2e/nested-button/nested-button.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/test-button/test-button.component.spec.ts`
`apps/${libName}-e2e/src/e2e/test-button/test-button.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/test-other/test-other.component.spec.ts`
`apps/${libName}-e2e/src/e2e/test-other/test-other.component.cy.ts`
)
).toBeTruthy();
expect(
tree.read(
`apps/${libName}-e2e/src/integration/test-button/test-button.component.spec.ts`,
`apps/${libName}-e2e/src/e2e/test-button/test-button.component.cy.ts`,
'utf-8'
)
).toMatchSnapshot();
@ -166,12 +176,12 @@ describe('angularStories generator: libraries', () => {
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/variable-declare-button/variable-declare-button.component.spec.ts`
`apps/${libName}-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/variable-declare-view/variable-declare-view.component.spec.ts`
`apps/${libName}-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts`
)
).toBeTruthy();
});
@ -205,17 +215,17 @@ describe('angularStories generator: libraries', () => {
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/variable-spread-declare-button/variable-spread-declare-button.component.spec.ts`
`apps/${libName}-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/variable-spread-declare-view/variable-spread-declare-view.component.spec.ts`
`apps/${libName}-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts`
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.spec.ts`
`apps/${libName}-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts`
)
).toBeTruthy();
});
@ -242,14 +252,10 @@ describe('angularStories generator: libraries', () => {
)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/cmp1/cmp1.component.spec.ts`
)
tree.exists(`apps/${libName}-e2e/src/e2e/cmp1/cmp1.component.cy.ts`)
).toBeTruthy();
expect(
tree.exists(
`apps/${libName}-e2e/src/integration/cmp2/cmp2.component.spec.ts`
)
tree.exists(`apps/${libName}-e2e/src/e2e/cmp2/cmp2.component.cy.ts`)
).toBeTruthy();
});

View File

@ -42,24 +42,24 @@ Array [
".storybook/main.js",
".storybook/tsconfig.json",
"apps/one/two/test-ui-lib-e2e/.eslintrc.json",
"apps/one/two/test-ui-lib-e2e/cypress.json",
"apps/one/two/test-ui-lib-e2e/cypress.config.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/cmp1/cmp1.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/cmp2/cmp2.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/nested-button/nested-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-button/secondary-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-standalone/secondary-standalone.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/standalone/standalone.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts",
"apps/one/two/test-ui-lib-e2e/src/fixtures/example.json",
"apps/one/two/test-ui-lib-e2e/src/integration/barrel-button/barrel-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/cmp1/cmp1.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/cmp2/cmp2.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/nested-button/nested-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/secondary-entry-point/secondary-button/secondary-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/secondary-entry-point/secondary-standalone/secondary-standalone.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/standalone/standalone.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/test-button/test-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/test-other/test-other.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/variable-declare-button/variable-declare-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/variable-declare-view/variable-declare-view.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/variable-spread-declare-button/variable-spread-declare-button.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/integration/variable-spread-declare-view/variable-spread-declare-view.component.spec.ts",
"apps/one/two/test-ui-lib-e2e/src/support/commands.ts",
"apps/one/two/test-ui-lib-e2e/src/support/index.ts",
"apps/one/two/test-ui-lib-e2e/src/support/e2e.ts",
"apps/one/two/test-ui-lib-e2e/tsconfig.json",
"jest.config.ts",
"jest.preset.js",
@ -170,24 +170,24 @@ Array [
".storybook/main.js",
".storybook/tsconfig.json",
"apps/test-ui-lib-e2e/.eslintrc.json",
"apps/test-ui-lib-e2e/cypress.json",
"apps/test-ui-lib-e2e/cypress.config.ts",
"apps/test-ui-lib-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/cmp1/cmp1.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/cmp2/cmp2.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/nested-button/nested-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-button/secondary-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-standalone/secondary-standalone.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/standalone/standalone.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts",
"apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts",
"apps/test-ui-lib-e2e/src/fixtures/example.json",
"apps/test-ui-lib-e2e/src/integration/barrel-button/barrel-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/cmp1/cmp1.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/cmp2/cmp2.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/nested-button/nested-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/secondary-entry-point/secondary-button/secondary-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/secondary-entry-point/secondary-standalone/secondary-standalone.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/standalone/standalone.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/test-button/test-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/test-other/test-other.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/variable-declare-button/variable-declare-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/variable-declare-view/variable-declare-view.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/variable-spread-declare-button/variable-spread-declare-button.component.spec.ts",
"apps/test-ui-lib-e2e/src/integration/variable-spread-declare-view/variable-spread-declare-view.component.spec.ts",
"apps/test-ui-lib-e2e/src/support/commands.ts",
"apps/test-ui-lib-e2e/src/support/index.ts",
"apps/test-ui-lib-e2e/src/support/e2e.ts",
"apps/test-ui-lib-e2e/tsconfig.json",
"jest.config.ts",
"jest.preset.js",

View File

@ -1,3 +1,4 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type { Tree } from '@nrwl/devkit';
import { joinPathFragments, writeJson } from '@nrwl/devkit';
import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter';
@ -7,7 +8,9 @@ import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-
import { createStorybookTestWorkspaceForLib } from '../utils/testing';
import type { StorybookConfigurationOptions } from './schema';
import { storybookConfigurationGenerator } from './storybook-configuration';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
function listFiles(tree: Tree): string[] {
const files = new Set<string>();
tree.listChanges().forEach((change) => {
@ -22,8 +25,12 @@ function listFiles(tree: Tree): string[] {
describe('StorybookConfiguration generator', () => {
let tree: Tree;
const libName = 'test-ui-lib';
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
tree = await createStorybookTestWorkspaceForLib(libName);
overrideCollectionResolutionForTesting({
@ -76,7 +83,7 @@ describe('StorybookConfiguration generator', () => {
expect(
tree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
).toBeTruthy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.json')).toBeFalsy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeFalsy();
expect(
tree.exists(
'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts'
@ -125,7 +132,7 @@ describe('StorybookConfiguration generator', () => {
expect(
tree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
).toBeTruthy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.json')).toBeTruthy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.exists(
'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts'
@ -138,12 +145,12 @@ describe('StorybookConfiguration generator', () => {
).toBeTruthy();
expect(
tree.exists(
'apps/test-ui-lib-e2e/src/integration/test-button/test-button.component.spec.ts'
'apps/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts'
)
).toBeTruthy();
expect(
tree.exists(
'apps/test-ui-lib-e2e/src/integration/test-other/test-other.component.spec.ts'
'apps/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts'
)
).toBeTruthy();
});

View File

@ -13,6 +13,16 @@
"factory": "./src/generators/cypress-project/cypress-project#cypressProjectSchematic",
"schema": "./src/generators/cypress-project/schema.json",
"description": "Add a Cypress E2E Project."
},
"cypress-component-project": {
"factory": "./src/generators/cypress-component-project/cypress-component-project#cypressComponentProject",
"schema": "./src/generators/cypress-component-project/schema.json",
"description": "Set up Cypress Component Test for a project"
},
"migrate-to-cypress-10": {
"factory": "./src/generators/migrate-to-cypress-ten/migrate-to-cypress-ten#migrateCypressProject",
"schema": "./src/generators/migrate-to-cypress-ten/schema.json",
"description": "Migrate existing Cypress e2e projects to Cypress v10"
}
},
"generators": {
@ -28,6 +38,18 @@
"schema": "./src/generators/cypress-project/schema.json",
"description": "Add a Cypress E2E Project.",
"hidden": true
},
"cypress-component-project": {
"factory": "./src/generators/cypress-component-project/cypress-component-project#cypressComponentProject",
"schema": "./src/generators/cypress-component-project/schema.json",
"description": "Set up Cypress Component Test for a project",
"hidden": true
},
"migrate-to-cypress-10": {
"factory": "./src/generators/migrate-to-cypress-ten/migrate-to-cypress-ten#migrateCypressProject",
"schema": "./src/generators/migrate-to-cypress-ten/schema.json",
"description": "Migrate existing Cypress e2e projects to Cypress v10",
"hidden": true
}
}
}

View File

@ -1,3 +1,5 @@
export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project';
export { cypressInitGenerator } from './src/generators/init/init';
export { conversionGenerator } from './src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint';
export { cypressComponentProject } from './src/generators/cypress-component-project/cypress-component-project';
export { migrateCypressProject } from './src/generators/migrate-to-cypress-ten/migrate-to-cypress-ten';

View File

@ -33,14 +33,14 @@
"migrations": "./migrations.json"
},
"dependencies": {
"@cypress/webpack-preprocessor": "^5.9.1",
"@babel/core": "^7.0.1",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.2",
"webpack": "^4 || ^5",
"@cypress/webpack-preprocessor": "^5.12.0",
"@nrwl/devkit": "file:../devkit",
"@nrwl/linter": "file:../linter",
"@nrwl/workspace": "file:../workspace",
"@phenomnomnominal/tsquery": "4.1.1",
"babel-loader": "^8.0.2",
"chalk": "4.1.0",
"enhanced-resolve": "^5.8.3",
"fork-ts-checker-webpack-plugin": "6.2.10",
@ -49,10 +49,11 @@
"tsconfig-paths": "^3.9.0",
"tsconfig-paths-webpack-plugin": "3.5.2",
"tslib": "^2.3.0",
"webpack": "^4 || ^5",
"webpack-node-externals": "^3.0.0"
},
"peerDependencies": {
"cypress": ">= 3 < 10"
"cypress": ">= 3 < 11"
},
"peerDependenciesMeta": {
"cypress": {

View File

@ -0,0 +1,55 @@
import { workspaceRoot } from '@nrwl/devkit';
import { join, relative } from 'path';
interface BaseCypressPreset {
videosFolder: string;
screenshotsFolder: string;
video: boolean;
chromeWebSecurity: boolean;
}
export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
const projectPath = relative(workspaceRoot, pathToConfig);
const offset = relative(pathToConfig, workspaceRoot);
const videosFolder = join(offset, 'dist', 'cypress', projectPath, 'videos');
const screenshotsFolder = join(
offset,
'dist',
'cypress',
projectPath,
'screenshots'
);
return {
videosFolder,
screenshotsFolder,
video: true,
chromeWebSecurity: false,
};
}
/**
* nx E2E Preset for Cypress
* @description
* this preset contains the base configuration
* for your e2e tests that nx recommends.
* you can easily extend this within your cypress config via spreading the preset
* @example
* export default defineConfig({
* e2e: {
* ...nxE2EPreset(__dirname)
* // add your own config here
* }
* })
*
* @param pathToConfig will be used to construct the output paths for videos and screenshots
*/
export function nxE2EPreset(pathToConfig: string) {
return {
...nxBaseCypressPreset(pathToConfig),
fileServerFolder: '.',
supportFile: 'src/support/e2e.ts',
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
fixturesFolder: 'src/fixtures',
};
}

View File

@ -1,11 +1,12 @@
import { stripIndents } from '@nrwl/devkit';
import * as path from 'path';
import { installedCypressVersion } from '../../utils/cypress-version';
import cypressExecutor, { CypressExecutorOptions } from './cypress.impl';
jest.mock('@nrwl/devkit');
let devkit = require('@nrwl/devkit');
jest.mock('../../utils/cypress-version');
import { installedCypressVersion } from '../../utils/cypress-version';
const Cypress = require('cypress');
@ -148,8 +149,15 @@ describe('Cypress builder', () => {
},
mockContext
);
const deprecatedMessage = stripIndents`
NOTE:
Support for Cypress versions < 10 is deprecated. Please upgrade to at least Cypress version 10.
A generator to migrate from v8 to v10 is provided. See https://nx.dev/cypress/v10-migration-guide
`;
expect(devkit.logger.warn).not.toHaveBeenCalled();
// expect the warning about the using < v10 but should not also warn about headless
expect(devkit.logger.warn).toHaveBeenCalledTimes(1);
expect(devkit.logger.warn).toHaveBeenCalledWith(deprecatedMessage);
});
it('should call `Cypress.run` with provided baseUrl', async () => {
@ -384,22 +392,6 @@ describe('Cypress builder', () => {
expect(Object.keys(runExecutor.mock.calls[0][1])).toContain('watch');
});
it('should forward testingType', async () => {
const { success } = await cypressExecutor(
{
...cypressOptions,
testingType: 'component',
},
mockContext
);
expect(success).toEqual(true);
expect(cypressRun).toHaveBeenCalledWith(
expect.objectContaining({
testingType: 'component',
})
);
});
it('should forward headed', async () => {
const { success } = await cypressExecutor(
{
@ -415,4 +407,22 @@ describe('Cypress builder', () => {
})
);
});
describe('Component Testing', () => {
it('should forward testingType', async () => {
const { success } = await cypressExecutor(
{
...cypressOptions,
testingType: 'component',
},
mockContext
);
expect(success).toEqual(true);
expect(cypressRun).toHaveBeenCalledWith(
expect.objectContaining({
testingType: 'component',
})
);
});
});
});

View File

@ -1,6 +1,3 @@
import 'dotenv/config';
import { basename, dirname, join } from 'path';
import { installedCypressVersion } from '../../utils/cypress-version';
import {
ExecutorContext,
logger,
@ -9,6 +6,9 @@ import {
runExecutor,
stripIndents,
} from '@nrwl/devkit';
import 'dotenv/config';
import { basename, dirname, join } from 'path';
import { installedCypressVersion } from '../../utils/cypress-version';
const Cypress = require('cypress'); // @NOTE: Importing via ES6 messes the whole test dependencies.
@ -47,6 +47,7 @@ export default async function cypressExecutor(
options = normalizeOptions(options, context);
let success;
for await (const baseUrl of startDevServer(options, context)) {
try {
success = await runCypress(baseUrl, options);
@ -73,6 +74,7 @@ function normalizeOptions(
}
checkSupportedBrowser(options);
warnDeprecatedHeadless(options);
warnDeprecatedCypressVersion();
return options;
}
@ -120,6 +122,16 @@ function warnDeprecatedHeadless({ headless }: CypressExecutorOptions) {
}
}
function warnDeprecatedCypressVersion() {
if (installedCypressVersion() < 10) {
logger.warn(stripIndents`
NOTE:
Support for Cypress versions < 10 is deprecated. Please upgrade to at least Cypress version 10.
A generator to migrate from v8 to v10 is provided. See https://nx.dev/cypress/v10-migration-guide
`);
}
}
async function* startDevServer(
opts: CypressExecutorOptions,
context: ExecutorContext
@ -169,7 +181,6 @@ async function runCypress(baseUrl: string, opts: CypressExecutorOptions) {
project: projectFolderPath,
configFile: basename(opts.cypressConfig),
};
// If not, will use the `baseUrl` normally from `cypress.json`
if (baseUrl) {
options.config = { baseUrl };

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cypress Component Project should add base cypress component testing config 1`] = `
Object {
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "libs/cool-lib/cypress.config.ts",
"testingType": "component",
},
}
`;
exports[`Cypress Component Project should not error when rerunning on an existing project 1`] = `
Object {
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "libs/cool-lib/cypress.config.ts",
"testingType": "component",
},
}
`;

View File

@ -0,0 +1,165 @@
import {
addProjectConfiguration,
ProjectConfiguration,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressComponentProject } from './cypress-component-project';
jest.mock('../../utils/cypress-version');
let projectConfig: ProjectConfiguration = {
projectType: 'library',
sourceRoot: 'libs/cool-lib/src',
root: 'libs/cool-lib',
targets: {
build: {
executor: '@nrwl/web:rollup',
options: {
tsConfig: 'libs/cool-lib/tsconfig.lib.json',
},
},
test: {
executor: '@nrwl/jest:jest',
options: {
jestConfig: 'libs/cool-lib/jest.config.js',
},
},
},
};
describe('Cypress Component Project', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'cool-lib', projectConfig);
tree.write(
'.gitignore',
`
# compiled output
/dist
/tmp
/out-tsc
`
);
tree.write(
'libs/cool-lib/tsconfig.lib.json',
`
{
"extends": "./tsconfig.json",
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.cy.ts"
],
"include": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx"
]
}
`
);
tree.write(
'libs/cool-lib/tsconfig.json',
`
{
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
`
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should add base cypress component testing config', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
await cypressComponentProject(tree, {
project: 'cool-lib',
skipFormat: false,
});
const projectConfig = readProjectConfiguration(tree, 'cool-lib');
expect(tree.exists('libs/cool-lib/cypress.config.ts')).toEqual(true);
expect(tree.exists('libs/cool-lib/cypress')).toEqual(true);
expect(
tree.exists('libs/cool-lib/cypress/support/component-index.html')
).toEqual(true);
expect(tree.exists('libs/cool-lib/cypress/fixtures/example.json')).toEqual(
true
);
expect(tree.exists('libs/cool-lib/cypress/support/commands.ts')).toEqual(
true
);
expect(tree.exists('libs/cool-lib/cypress/support/component.ts')).toEqual(
true
);
expect(tree.exists('libs/cool-lib/tsconfig.cy.json')).toEqual(true);
expect(projectConfig.targets['component-test']).toMatchSnapshot();
});
it('should not error when rerunning on an existing project', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
tree.write('libs/cool-lib/cypress.config.ts', '');
const newTarget = {
['component-test']: {
executor: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'libs/cool-lib/cypress.config.ts',
testingType: 'component',
},
},
};
updateProjectConfiguration(tree, 'cool-lib', {
...projectConfig,
targets: {
...projectConfig.targets,
...newTarget,
},
});
await cypressComponentProject(tree, {
project: 'cool-lib',
skipFormat: true,
});
const actualProjectConfig = readProjectConfiguration(tree, 'cool-lib');
expect(tree.exists('libs/cool-lib/cypress.config.ts')).toEqual(true);
expect(tree.exists('libs/cool-lib/cypress')).toEqual(true);
expect(tree.exists('libs/cool-lib/tsconfig.cy.json')).toEqual(true);
expect(actualProjectConfig.targets['component-test']).toMatchSnapshot();
});
it('should error when using cypress < v10', async () => {
mockedInstalledCypressVersion.mockReturnValue(9);
await expect(
async () =>
await cypressComponentProject(tree, {
project: 'cool-lib',
skipFormat: true,
})
).rejects.toThrowError(
'Cypress version of 10 or higher is required to use component testing. See the migration guide to upgrade. https://nx.dev/cypress/v10-migration-guide'
);
});
});

View File

@ -0,0 +1,89 @@
import {
addDependenciesToPackageJson,
formatFiles,
generateFiles,
joinPathFragments,
offsetFromRoot,
ProjectConfiguration,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { installedCypressVersion } from '../../utils/cypress-version';
import {
cypressVersion,
cypressWebpackVersion,
webpackHttpPluginVersion,
} from '../../utils/versions';
import { CypressComponentProjectSchema } from './schema';
export async function cypressComponentProject(
tree: Tree,
options: CypressComponentProjectSchema
) {
const cyVersion = installedCypressVersion();
if (cyVersion && cyVersion < 10) {
throw new Error(
'Cypress version of 10 or higher is required to use component testing. See the migration guide to upgrade. https://nx.dev/cypress/v10-migration-guide'
);
}
const projectConfig = readProjectConfiguration(tree, options.project);
const installDepsTask = updateDeps(tree);
addProjectFiles(tree, projectConfig, options);
addTargetToProject(tree, projectConfig, options);
if (!options.skipFormat) {
await formatFiles(tree);
}
return () => {
installDepsTask();
};
}
function updateDeps(tree: Tree) {
const devDeps = {
'@cypress/webpack-dev-server': cypressWebpackVersion,
'html-webpack-plugin': webpackHttpPluginVersion,
cypress: cypressVersion,
};
return addDependenciesToPackageJson(tree, {}, devDeps);
}
function addProjectFiles(
tree: Tree,
projectConfig: ProjectConfiguration,
options: CypressComponentProjectSchema
) {
generateFiles(
tree,
joinPathFragments(__dirname, 'files'),
projectConfig.root,
{
...options,
projectRoot: projectConfig.root,
offsetFromRoot: offsetFromRoot(projectConfig.root),
ext: '',
}
);
}
function addTargetToProject(
tree: Tree,
projectConfig: ProjectConfiguration,
options: CypressComponentProjectSchema
) {
projectConfig.targets['component-test'] = {
executor: '@nrwl/cypress:cypress',
options: {
cypressConfig: joinPathFragments(projectConfig.root, 'cypress.config.ts'),
testingType: 'component',
},
};
updateProjectConfiguration(tree, options.project, projectConfig);
}

View File

@ -0,0 +1,3 @@
import { defineConfig } from 'cypress';
export default defineConfig({});

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,33 @@
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= project %> Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import './commands';

View File

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["cypress", "node"]
},
"include": [
"cypress.config.ts",
"**/*.cy.ts",
"**/*.cy.tsx",
"**/*.cy.js",
"**/*.cy.jsx",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,4 @@
export interface CypressComponentProjectSchema {
project: string;
skipFormat: boolean;
}

View File

@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxCypressComponentProject",
"cli": "nx",
"title": "Set up Cypress component testing for a project",
"description": "Set up Cypress component test for a project.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/cypress:cypress-component-project --project=my-cool-lib ",
"description": "Add cypress component testing to an existing project named my-cool-lib"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing to",
"$default": {
"$source": "projectName"
},
"x-prompt": "What project should we add Cypress component testing to?"
}
},
"required": ["project"]
}

View File

@ -1,6 +1,214 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`schematic:cypress-project Cypress Project nested should set right path names in \`tsconfig.e2e.json\` 1`] = `
exports[`Cypress Project < v7 --linter eslint should add eslint-plugin-cypress 1`] = `
Object {
"extends": Array [
"plugin:cypress/recommended",
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"src/plugins/index.js",
],
"rules": Object {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off",
},
},
],
}
`;
exports[`Cypress Project < v7 nested should update workspace.json 1`] = `
Object {
"e2e": Object {
"builder": "@nrwl/cypress:cypress",
"configurations": Object {
"production": Object {
"devServerTarget": "my-dir-my-app:serve:production",
},
},
"options": Object {
"cypressConfig": "apps/my-dir/my-app-e2e/cypress.json",
"devServerTarget": "my-dir-my-app:serve",
"testingType": "e2e",
"tsConfig": "apps/my-dir/my-app-e2e/tsconfig.json",
},
},
"lint": Object {
"builder": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/my-dir/my-app-e2e/**/*",
],
"tsConfig": Array [
"apps/my-dir/my-app-e2e/tsconfig.json",
],
},
},
}
`;
exports[`Cypress Project < v7 project with directory in its name should set right path names in \`cypress.json\` 1`] = `
"{
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"integrationFolder\\": \\"./src/integration\\",
\\"modifyObstructiveCode\\": false,
\\"supportFile\\": \\"./src/support/index.ts\\",
\\"pluginsFile\\": \\"./src/plugins/index\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../../dist/cypress/apps/my-dir/my-app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../../dist/cypress/apps/my-dir/my-app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false
}
"
`;
exports[`Cypress Project < v7 project with directory in its name should update workspace.json 1`] = `
Object {
"e2e": Object {
"builder": "@nrwl/cypress:cypress",
"configurations": Object {
"production": Object {
"devServerTarget": "my-dir-my-app:serve:production",
},
},
"options": Object {
"cypressConfig": "apps/my-dir/my-app-e2e/cypress.json",
"devServerTarget": "my-dir-my-app:serve",
"testingType": "e2e",
"tsConfig": "apps/my-dir/my-app-e2e/tsconfig.json",
},
},
"lint": Object {
"builder": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/my-dir/my-app-e2e/**/*",
],
"tsConfig": Array [
"apps/my-dir/my-app-e2e/tsconfig.json",
],
},
},
}
`;
exports[`Cypress Project < v7 should add update \`workspace.json\` file (baseUrl) 1`] = `
Object {
"e2e": Object {
"builder": "@nrwl/cypress:cypress",
"options": Object {
"baseUrl": "http://localhost:3000",
"cypressConfig": "apps/my-app-e2e/cypress.json",
"testingType": "e2e",
"tsConfig": "apps/my-app-e2e/tsconfig.json",
},
},
"lint": Object {
"builder": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/my-app-e2e/**/*",
],
"tsConfig": Array [
"apps/my-app-e2e/tsconfig.json",
],
},
},
}
`;
exports[`Cypress Project < v7 should add update \`workspace.json\` file 1`] = `
Object {
"e2e": Object {
"builder": "@nrwl/cypress:cypress",
"configurations": Object {
"production": Object {
"devServerTarget": "my-app:serve:production",
},
},
"options": Object {
"cypressConfig": "apps/my-app-e2e/cypress.json",
"devServerTarget": "my-app:serve",
"testingType": "e2e",
"tsConfig": "apps/my-app-e2e/tsconfig.json",
},
},
"lint": Object {
"builder": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/my-app-e2e/**/*",
],
"tsConfig": Array [
"apps/my-app-e2e/tsconfig.json",
],
},
},
}
`;
exports[`Cypress Project < v7 should add update \`workspace.json\` file for a project with a defaultConfiguration 1`] = `
Object {
"e2e": Object {
"builder": "@nrwl/cypress:cypress",
"configurations": Object {
"production": Object {
"devServerTarget": "my-app:serve:production",
},
},
"options": Object {
"cypressConfig": "apps/my-app-e2e/cypress.json",
"devServerTarget": "my-app:serve:development",
"testingType": "e2e",
"tsConfig": "apps/my-app-e2e/tsconfig.json",
},
},
"lint": Object {
"builder": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/my-app-e2e/**/*",
],
"tsConfig": Array [
"apps/my-app-e2e/tsconfig.json",
],
},
},
}
`;
exports[`Cypress Project > v10 nested should set right path names in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
"
`;
exports[`Cypress Project > v10 nested should set right path names in \`tsconfig.e2e.json\` 1`] = `
Object {
"compilerOptions": Object {
"allowJs": true,
@ -15,11 +223,36 @@ Object {
"include": Array [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`;
exports[`schematic:cypress-project Cypress Project should set right path names in \`tsconfig.e2e.json\` 1`] = `
exports[`Cypress Project > v10 should add update \`workspace.json\` file properly when eslint is passed 1`] = `
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app-e2e/**/*.{js,ts}",
],
},
"outputs": Array [
"{options.outputFile}",
],
}
`;
exports[`Cypress Project > v10 should set right path names in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
"
`;
exports[`Cypress Project > v10 should set right path names in \`tsconfig.e2e.json\` 1`] = `
Object {
"compilerOptions": Object {
"allowJs": true,
@ -34,6 +267,7 @@ Object {
"include": Array [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`;

View File

@ -1,22 +1,28 @@
import {
readJson,
addProjectConfiguration,
readJson,
readProjectConfiguration,
updateProjectConfiguration,
Tree,
updateProjectConfiguration,
WorkspaceJsonConfiguration,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { cypressProjectGenerator } from './cypress-project';
import { Schema } from './schema';
import { Linter } from '@nrwl/linter';
import { installedCypressVersion } from '../../utils/cypress-version';
describe('schematic:cypress-project', () => {
jest.mock('../../utils/cypress-version');
describe('Cypress Project', () => {
let tree: Tree;
const defaultOptions: Omit<Schema, 'name' | 'project'> = {
linter: Linter.EsLint,
standaloneConfig: false,
};
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
@ -47,145 +53,31 @@ describe('schematic:cypress-project', () => {
},
});
});
afterEach(() => jest.clearAllMocks());
describe('Cypress Project', () => {
it('should generate files', async () => {
describe('> v10', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
});
it('should generate files for v10 and above', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
expect(tree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/fixtures/example.json')
).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/integration/app.spec.ts')
).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/src/support/app.po.ts')).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/support/commands.ts')
).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/src/support/index.ts')).toBeTruthy();
});
it('should generate a plugin file if cypress is below version 7', async () => {
jest.mock('cypress/package.json', () => ({
version: '6.0.0',
}));
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
expect(tree.exists('apps/my-app-e2e/src/plugins/index.js')).toBeTruthy();
});
it('should add update `workspace.json` file', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect.lint).toEqual({
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: ['apps/my-app-e2e/tsconfig.json'],
exclude: ['**/node_modules/**', '!apps/my-app-e2e/**/*'],
},
});
expect(project.architect.e2e).toEqual({
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-app-e2e/cypress.json',
devServerTarget: 'my-app:serve',
tsConfig: 'apps/my-app-e2e/tsconfig.json',
},
configurations: {
production: {
devServerTarget: 'my-app:serve:production',
},
},
});
});
it('should add update `workspace.json` file (baseUrl)', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
baseUrl: 'http://localhost:3000',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect.lint).toEqual({
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: ['apps/my-app-e2e/tsconfig.json'],
exclude: ['**/node_modules/**', '!apps/my-app-e2e/**/*'],
},
});
expect(project.architect.e2e).toEqual({
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-app-e2e/cypress.json',
baseUrl: 'http://localhost:3000',
tsConfig: 'apps/my-app-e2e/tsconfig.json',
},
});
});
it('should add update `workspace.json` file for a project with a defaultConfiguration', async () => {
const originalProject = readProjectConfiguration(tree, 'my-app');
originalProject.targets.serve.defaultConfiguration = 'development';
originalProject.targets.serve.configurations.development = {};
updateProjectConfiguration(tree, 'my-app', originalProject);
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect.lint).toEqual({
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: ['apps/my-app-e2e/tsconfig.json'],
exclude: ['**/node_modules/**', '!apps/my-app-e2e/**/*'],
},
});
expect(project.architect.e2e).toEqual({
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-app-e2e/cypress.json',
devServerTarget: 'my-app:serve:development',
tsConfig: 'apps/my-app-e2e/tsconfig.json',
},
configurations: {
production: {
devServerTarget: 'my-app:serve:production',
},
},
});
expect(tree.exists('apps/my-app-e2e/src/support/e2e.ts')).toBeTruthy();
});
it('should add update `workspace.json` file properly when eslint is passed', async () => {
@ -198,13 +90,7 @@ describe('schematic:cypress-project', () => {
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.architect.lint).toEqual({
builder: '@nrwl/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: ['apps/my-app-e2e/**/*.{js,ts}'],
},
});
expect(project.architect.lint).toMatchSnapshot();
});
it('should not add lint target when "none" is passed', async () => {
@ -233,26 +119,17 @@ describe('schematic:cypress-project', () => {
expect(project.implicitDependencies).toEqual(['my-app']);
});
it('should set right path names in `cypress.json`', async () => {
it('should set right path names in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
const cypressJson = readJson(tree, 'apps/my-app-e2e/cypress.json');
expect(cypressJson).toEqual({
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
pluginsFile: './src/plugins/index',
supportFile: './src/support/index.ts',
video: true,
videosFolder: '../../dist/cypress/apps/my-app-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/my-app-e2e/screenshots',
chromeWebSecurity: false,
});
const cypressConfig = tree.read(
'apps/my-app-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
@ -290,67 +167,19 @@ describe('schematic:cypress-project', () => {
});
describe('nested', () => {
it('should update workspace.json', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
linter: Linter.TsLint,
standaloneConfig: false,
});
const projectConfig = readJson(tree, 'workspace.json').projects[
'my-dir-my-app-e2e'
];
expect(projectConfig).toBeDefined();
expect(projectConfig.architect.lint).toEqual({
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: ['apps/my-dir/my-app-e2e/tsconfig.json'],
exclude: ['**/node_modules/**', '!apps/my-dir/my-app-e2e/**/*'],
},
});
expect(projectConfig.architect.e2e).toEqual({
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-dir/my-app-e2e/cypress.json',
devServerTarget: 'my-dir-my-app:serve',
tsConfig: 'apps/my-dir/my-app-e2e/tsconfig.json',
},
configurations: {
production: {
devServerTarget: 'my-dir-my-app:serve:production',
},
},
});
});
it('should set right path names in `cypress.json`', async () => {
it('should set right path names in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
});
const cypressJson = readJson(
tree,
'apps/my-dir/my-app-e2e/cypress.json'
);
expect(cypressJson).toEqual({
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
pluginsFile: './src/plugins/index',
supportFile: './src/support/index.ts',
video: true,
videosFolder: '../../../dist/cypress/apps/my-dir/my-app-e2e/videos',
screenshotsFolder:
'../../../dist/cypress/apps/my-dir/my-app-e2e/screenshots',
chromeWebSecurity: false,
});
const cypressConfig = tree.read(
'apps/my-dir/my-app-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
@ -432,6 +261,133 @@ describe('schematic:cypress-project', () => {
});
});
it('should generate in the correct folder', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'other-e2e',
project: 'my-app',
directory: 'one/two',
});
const workspace = readJson(tree, 'workspace.json');
expect(workspace.projects['one-two-other-e2e']).toBeDefined();
[
'apps/one/two/other-e2e/cypress.config.ts',
'apps/one/two/other-e2e/src/e2e/app.cy.ts',
].forEach((path) => expect(tree.exists(path)).toBeTruthy());
});
});
describe('v9 - v7', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(9);
});
it('should generate files', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
expect(tree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/fixtures/example.json')
).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/integration/app.spec.ts')
).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/src/support/app.po.ts')).toBeTruthy();
expect(
tree.exists('apps/my-app-e2e/src/support/commands.ts')
).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/src/support/index.ts')).toBeTruthy();
});
});
describe('< v7', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(6);
});
it('should generate a plugin file if cypress is below version 7', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
expect(tree.exists('apps/my-app-e2e/src/plugins/index.js')).toBeTruthy();
});
it('should add update `workspace.json` file', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect).toMatchSnapshot();
});
it('should add update `workspace.json` file (baseUrl)', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
baseUrl: 'http://localhost:3000',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect).toMatchSnapshot();
});
it('should add update `workspace.json` file for a project with a defaultConfiguration', async () => {
const originalProject = readProjectConfiguration(tree, 'my-app');
originalProject.targets.serve.defaultConfiguration = 'development';
originalProject.targets.serve.configurations.development = {};
updateProjectConfiguration(tree, 'my-app', originalProject);
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.TsLint,
standaloneConfig: false,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
expect(project.architect).toMatchSnapshot();
});
describe('nested', () => {
it('should update workspace.json', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
linter: Linter.TsLint,
standaloneConfig: false,
});
const projectConfig = readJson(tree, 'workspace.json').projects[
'my-dir-my-app-e2e'
];
expect(projectConfig).toBeDefined();
expect(projectConfig.architect).toMatchSnapshot();
});
});
describe('--linter', () => {
describe('eslint', () => {
it('should add eslint-plugin-cypress', async () => {
@ -447,54 +403,10 @@ describe('schematic:cypress-project', () => {
).toBeTruthy();
const eslintrcJson = readJson(tree, 'apps/my-app-e2e/.eslintrc.json');
expect(eslintrcJson).toMatchInlineSnapshot(`
Object {
"extends": Array [
"plugin:cypress/recommended",
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"src/plugins/index.js",
],
"rules": Object {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off",
},
},
],
}
`);
expect(eslintrcJson).toMatchSnapshot();
});
});
});
it('should generate in the correct folder', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'other-e2e',
project: 'my-app',
directory: 'one/two',
});
const workspace = readJson(tree, 'workspace.json');
expect(workspace.projects['one-two-other-e2e']).toBeDefined();
[
'apps/one/two/other-e2e/cypress.json',
'apps/one/two/other-e2e/src/integration/app.spec.ts',
].forEach((path) => expect(tree.exists(path)).toBeTruthy());
});
describe('project with directory in its name', () => {
beforeEach(async () => {
@ -512,27 +424,7 @@ describe('schematic:cypress-project', () => {
];
expect(projectConfig).toBeDefined();
expect(projectConfig.architect.lint).toEqual({
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: ['apps/my-dir/my-app-e2e/tsconfig.json'],
exclude: ['**/node_modules/**', '!apps/my-dir/my-app-e2e/**/*'],
},
});
expect(projectConfig.architect.e2e).toEqual({
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-dir/my-app-e2e/cypress.json',
devServerTarget: 'my-dir-my-app:serve',
tsConfig: 'apps/my-dir/my-app-e2e/tsconfig.json',
},
configurations: {
production: {
devServerTarget: 'my-dir-my-app:serve:production',
},
},
});
expect(projectConfig.architect).toMatchSnapshot();
});
it('should update nx.json', async () => {
@ -542,24 +434,12 @@ describe('schematic:cypress-project', () => {
});
it('should set right path names in `cypress.json`', async () => {
const cypressJson = readJson(
tree,
'apps/my-dir/my-app-e2e/cypress.json'
const cypressConfig = tree.read(
'apps/my-dir/my-app-e2e/cypress.json',
'utf-8'
);
expect(cypressJson).toEqual({
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
integrationFolder: './src/integration',
modifyObstructiveCode: false,
pluginsFile: './src/plugins/index',
supportFile: './src/support/index.ts',
video: true,
videosFolder: '../../../dist/cypress/apps/my-dir/my-app-e2e/videos',
screenshotsFolder:
'../../../dist/cypress/apps/my-dir/my-app-e2e/screenshots',
chromeWebSecurity: false,
});
expect(cypressConfig).toMatchSnapshot();
});
});
});

View File

@ -1,34 +1,34 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
readProjectConfiguration,
convertNxGenerator,
formatFiles,
generateFiles,
getWorkspaceLayout,
joinPathFragments,
logger,
names,
offsetFromRoot,
ProjectConfiguration,
readProjectConfiguration,
stripIndents,
toJS,
Tree,
updateJson,
ProjectConfiguration,
stripIndents,
logger,
} from '@nrwl/devkit';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { join } from 'path';
// app
import { Schema } from './schema';
import { installedCypressVersion } from '../../utils/cypress-version';
import { filePathPrefix } from '../../utils/project-name';
import {
cypressVersion,
eslintPluginCypressVersion,
} from '../../utils/versions';
import { filePathPrefix } from '../../utils/project-name';
import { installedCypressVersion } from '../../utils/cypress-version';
// app
import { Schema } from './schema';
export interface CypressProjectSchema extends Schema {
projectName: string;
@ -36,23 +36,39 @@ export interface CypressProjectSchema extends Schema {
}
function createFiles(tree: Tree, options: CypressProjectSchema) {
generateFiles(tree, join(__dirname, './files'), options.projectRoot, {
tmpl: '',
...options,
project: options.project || 'Project',
ext: options.js ? 'js' : 'ts',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(tree, options.projectRoot),
});
// if not installed or >v10 use v10 folder
// else use v9 folder
const cypressVersion = installedCypressVersion();
if (!cypressVersion || cypressVersion >= 7) {
tree.delete(join(options.projectRoot, 'src/plugins/index.js'));
} else {
const cypressFiles =
cypressVersion && cypressVersion < 10 ? 'v9-and-under' : 'v10-and-after';
generateFiles(
tree,
join(__dirname, './files', cypressFiles),
options.projectRoot,
{
tmpl: '',
...options,
project: options.project || 'Project',
ext: options.js ? 'js' : 'ts',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
tree,
options.projectRoot
),
}
);
if (cypressVersion && cypressVersion < 7) {
updateJson(tree, join(options.projectRoot, 'cypress.json'), (json) => {
json.pluginsFile = './src/plugins/index';
return json;
});
} else if (cypressVersion < 10) {
const pluginPath = join(options.projectRoot, 'src/plugins/index.js');
if (tree.exists(pluginPath)) {
tree.delete(pluginPath);
}
}
if (options.js) {
@ -62,6 +78,12 @@ function createFiles(tree: Tree, options: CypressProjectSchema) {
function addProject(tree: Tree, options: CypressProjectSchema) {
let e2eProjectConfig: ProjectConfiguration;
const detectedCypressVersion = installedCypressVersion() ?? cypressVersion;
const cypressConfig =
detectedCypressVersion < 10 ? 'cypress.json' : 'cypress.config.ts';
if (options.baseUrl) {
e2eProjectConfig = {
root: options.projectRoot,
@ -73,9 +95,10 @@ function addProject(tree: Tree, options: CypressProjectSchema) {
options: {
cypressConfig: joinPathFragments(
options.projectRoot,
'cypress.json'
cypressConfig
),
baseUrl: options.baseUrl,
testingType: 'e2e',
},
},
},
@ -105,9 +128,10 @@ function addProject(tree: Tree, options: CypressProjectSchema) {
options: {
cypressConfig: joinPathFragments(
options.projectRoot,
'cypress.json'
cypressConfig
),
devServerTarget,
testingType: 'e2e',
},
configurations: {
production: {
@ -123,7 +147,6 @@ function addProject(tree: Tree, options: CypressProjectSchema) {
throw new Error(`Either project or baseUrl should be specified.`);
}
const detectedCypressVersion = installedCypressVersion() ?? cypressVersion;
if (detectedCypressVersion < 7) {
e2eProjectConfig.targets.e2e.options.tsConfig = joinPathFragments(
options.projectRoot,

View File

@ -0,0 +1,6 @@
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});

View File

@ -0,0 +1,10 @@
{
"extends": "<%= rootTsConfigPath %>",
"compilerOptions": {
"sourceMap": false,
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"]
}

View File

@ -0,0 +1,4 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

View File

@ -0,0 +1,13 @@
import { getGreeting } from '../support/app.po';
describe('<%= project %>', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome <%= project %>');
});
});

View File

@ -0,0 +1 @@
export const getGreeting = () => cy.get('h1');

View File

@ -0,0 +1,33 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
declare namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/index.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import './commands';

View File

@ -1,5 +1,5 @@
{
"extends": "<%= rootTsConfigPath %>",
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "<%= offsetFromRoot %>dist/out-tsc",

View File

@ -0,0 +1,278 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convertToCypressTen convertCypressProject should handle custom target names 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": \\"src/support/e2e.ts\\"
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
}
})"
`;
exports[`convertToCypressTen convertCypressProject should handle multiple configurations 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": \\"src/support/e2e.ts\\"
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
}
})"
`;
exports[`convertToCypressTen convertCypressProject should handle multiple configurations 2`] = `
Object {
"configurations": Object {
"production": Object {
"devServerTarget": "target:serve:production",
},
"static": Object {
"baseUrl": "http://localhost:3000",
},
},
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "apps/app-e2e/cypress.config.ts",
"devServerTarget": "app:serve",
"testingType": "e2e",
},
}
`;
exports[`convertToCypressTen convertCypressProject should infer targets with --all flag 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": \\"src/support/e2e.ts\\"
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
}
})"
`;
exports[`convertToCypressTen convertCypressProject should infer targets with --all flag 2`] = `
Object {
"e2e": Object {
"configurations": Object {
"production": Object {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "apps/app-e2e/cypress.config.ts",
"devServerTarget": "app:serve",
"testingType": "e2e",
},
},
"e2e-custom": Object {
"configurations": Object {
"production": Object {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "apps/app-e2e/cypress.config.ts",
"devServerTarget": "app:serve",
"testingType": "e2e",
},
},
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/app-e2e/**/*.{js,ts}",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
}
`;
exports[`convertToCypressTen convertCypressProject should not break when an invalid target is passed in 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": \\"src/support/e2e.ts\\"
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
}
})"
`;
exports[`convertToCypressTen convertCypressProject should not break when an invalid target is passed in 2`] = `
Object {
"e2e": Object {
"configurations": Object {
"production": Object {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "apps/app-e2e/cypress.config.ts",
"devServerTarget": "app:serve",
"testingType": "e2e",
},
},
"e2e-custom": Object {
"configurations": Object {
"production": Object {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nrwl/cypress:cypress",
"options": Object {
"cypressConfig": "apps/app-e2e/cypress.config.ts",
"devServerTarget": "app:serve",
"testingType": "e2e",
},
},
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/app-e2e/**/*.{js,ts}",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
}
`;
exports[`convertToCypressTen convertCypressProject should update project w/customized config 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
import setupNodeEvents from './src/plugins/index.js';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"baseUrl\\": \\"http://localhost:4200\\",
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": false
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
setupNodeEvents
}
})"
`;
exports[`convertToCypressTen convertCypressProject should update project w/defaults 1`] = `
"import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const cypressJsonConfig = {
\\"fileServerFolder\\": \\".\\",
\\"fixturesFolder\\": \\"./src/fixtures\\",
\\"video\\": true,
\\"videosFolder\\": \\"../../dist/cypress/apps/app-e2e/videos\\",
\\"screenshotsFolder\\": \\"../../dist/cypress/apps/app-e2e/screenshots\\",
\\"chromeWebSecurity\\": false,
\\"specPattern\\": \\"src/e2e/**/*.cy.{js,jsx,ts,tsx}\\",
\\"supportFile\\": \\"src/support/e2e.ts\\"
}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
}
})"
`;
exports[`convertToCypressTen updateImports should update imports 1`] = `
"
import { getGreeting } from '../support/app.po';
import { blah } from '../support/e2e';
const eh = require('../support/e2e')
import { blah } from '../support/e2e';
const eh = require('../support/e2e')
describe('a', () => {
beforeEach(() => {
cy.visit('/')
blah()
eh()
});
it('should display welcome message', () => {
// Custom command example, see \\\\\`../support/commands.ts\\\\\` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see \\\\\`../support/app.po.ts\\\\\` file
getGreeting().contains('Welcome a');
});
});
"
`;

View File

@ -0,0 +1,347 @@
import {
joinPathFragments,
ProjectConfiguration,
readJson,
stripIndents,
Tree,
updateJson,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import { basename, dirname, extname, relative } from 'path';
import {
isCallExpression,
isExportDeclaration,
isImportDeclaration,
StringLiteral,
} from 'typescript';
const validFilesEndingsToUpdate = [
'.js',
'.jsx',
'.ts',
'.tsx',
'.mjs',
'.cjs',
];
export function findCypressConfigs(
tree: Tree,
projectConfig: ProjectConfiguration,
target: string,
config: string
): {
cypressConfigPathJson: string;
cypressConfigPathTs: string;
} {
const cypressConfigPathJson =
projectConfig.targets[target]?.configurations?.[config]?.cypressConfig ||
// make sure it's a json file, since it could have been updated to ts file from previous configuration migration
(projectConfig.targets[target]?.options?.cypressConfig.endsWith('json')
? projectConfig.targets[target]?.options?.cypressConfig
: joinPathFragments(projectConfig.root, 'cypress.json'));
const cypressConfigPathTs = joinPathFragments(
projectConfig.root,
// create matching ts config for custom cypress config if present
cypressConfigPathJson.endsWith('cypress.json')
? 'cypress.config.ts' //default
: `${basename(
cypressConfigPathJson,
extname(cypressConfigPathJson)
)}.config.ts`
);
return {
cypressConfigPathJson,
cypressConfigPathTs,
};
}
/**
* update the existing cypress.json config to the new cypress.config.ts structure.
* return both the old and new configs
*/
export function createNewCypressConfig(
tree: Tree,
projectConfig: ProjectConfiguration,
cypressConfigPathJson: string
): {
cypressConfigTs: Record<string, any>;
cypressConfigJson: Record<string, any>;
} {
console.log({ cypressConfigPathJson });
const cypressConfigJson = readJson(tree, cypressConfigPathJson);
const {
modifyObstructiveCode = null, // no longer needed in configs
integrationFolder = 'src/e2e', // provide the new defaults if the value isn't present
supportFile = 'src/support/e2e.ts',
...restOfConfig
} = cypressConfigJson;
const newIntegrationFolder = tree.exists(
joinPathFragments(projectConfig.sourceRoot, 'integration')
)
? 'src/e2e'
: integrationFolder;
const cypressConfigTs = {
e2e: {
...restOfConfig,
specPattern: `${newIntegrationFolder}/**/*.cy.{js,jsx,ts,tsx}`,
// if supportFile is defined (can be false if not using it) and in the default location (or in the new default location),
// then use the new default location.
// otherwise we will use the existing folder location/falsey value
supportFile:
(supportFile &&
tree.exists(
joinPathFragments(projectConfig.sourceRoot, 'support', 'index.ts')
)) ||
tree.exists(
joinPathFragments(projectConfig.sourceRoot, 'support', 'e2e.ts')
)
? 'src/support/e2e.ts'
: supportFile,
// if the default location is used then will update to the new location otherwise keep the custom location
// this is used down the line, but won't be in the final config file since it's a deprecated option
integrationFolder: newIntegrationFolder,
},
};
return { cypressConfigTs, cypressConfigJson };
}
export function createSupportFileImport(
oldSupportFilePath: string,
newSupportFilePath: string,
projectSourceRoot: string
): { oldImportPathLeaf: string; newImportPathLeaf: string } {
// need to get the new import path for the support file.
// before it was "<relative path>/support/index.ts" and the new path will be "<relative path>/support/e2e.ts"
// i.e. take ../support => ../support/e2e.ts
// 1. take apps/app-e2e/support/index.ts => support (this cant have a / in it. must grab the leaf for tsquery)
// 2. if the leaf value is index.ts then grab the parent directory (i.e. so we have support/index.ts => support)
// 3. take apps/app-e2e/support/e2e.ts => support/e2e
// apps/app-e2e/support/e2e.ts => support/e2e
const newFileExt = extname(newSupportFilePath);
const newImportPathLeaf = relative(
projectSourceRoot,
newSupportFilePath
).replace(newFileExt, '');
// apps/app-e2e/support/index.ts => support/index
const oldFileExt = extname(oldSupportFilePath);
const oldImportPathLeaf = relative(
projectSourceRoot,
oldSupportFilePath
).replace(oldFileExt, '');
// support/index => support
const oldRelativeImportPath = basename(oldImportPathLeaf, oldFileExt);
return {
newImportPathLeaf,
// don't import from 'support/index' it's just 'support'
oldImportPathLeaf:
oldRelativeImportPath === 'index'
? dirname(oldImportPathLeaf)
: oldImportPathLeaf,
};
}
export function updateProjectPaths(
tree: Tree,
projectConfig: ProjectConfiguration,
{
cypressConfigTs,
cypressConfigJson,
}: {
cypressConfigTs: Record<string, any>;
cypressConfigJson: Record<string, any>;
}
) {
const { integrationFolder, supportFile } = cypressConfigTs['e2e'];
const oldIntegrationFolder = joinPathFragments(
projectConfig.root,
cypressConfigJson.integrationFolder
);
const newIntegrationFolder = joinPathFragments(
projectConfig.root,
integrationFolder
);
let newSupportFile: string;
let oldSupportFile: string;
let oldImportLeafPath: string;
let newImportLeafPath: string;
let shouldUpdateSupportFileImports = false;
// supportFile can be falsey or a string path to the file
if (cypressConfigJson.supportFile) {
// we need to check the test files to see if
// support file import is used and then update it if so
shouldUpdateSupportFileImports = true;
oldSupportFile = joinPathFragments(
projectConfig.root,
cypressConfigJson.supportFile
);
newSupportFile = joinPathFragments(projectConfig.root, supportFile);
// oldSupportFile might have already been updated
// to the new default location so must be guarded for
if (oldSupportFile !== newSupportFile && tree.exists(oldSupportFile)) {
tree.rename(oldSupportFile, newSupportFile);
}
} else {
shouldUpdateSupportFileImports = false;
newSupportFile = supportFile;
// rename the default support file even if not in use to keep the system in sync with cypress v10
const defaultSupportFile = joinPathFragments(
projectConfig.sourceRoot,
'support',
'index.ts'
);
if (tree.exists(defaultSupportFile)) {
const newSupportDefaultPath = joinPathFragments(
projectConfig.sourceRoot,
'support',
'e2e.ts'
);
if (
defaultSupportFile !== newSupportDefaultPath &&
tree.exists(defaultSupportFile)
) {
tree.rename(defaultSupportFile, newSupportDefaultPath);
}
}
}
if (shouldUpdateSupportFileImports) {
const newImportPaths = createSupportFileImport(
oldSupportFile,
newSupportFile,
projectConfig.sourceRoot
);
oldImportLeafPath = newImportPaths.oldImportPathLeaf;
newImportLeafPath = newImportPaths.newImportPathLeaf;
}
// tree.rename doesn't work on directories must update each file within
// the directory to the new directory
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (path) => {
if (!path.includes(oldIntegrationFolder)) {
return;
}
const fileName = basename(path);
let newPath = path.replace(oldIntegrationFolder, newIntegrationFolder);
if (fileName.includes('.spec.')) {
newPath = newPath.replace('.spec.', '.cy.');
}
if (newPath !== path && tree.exists(path)) {
tree.rename(path, newPath);
}
// if they weren't using the supportFile then there is no need to update the imports.
if (
shouldUpdateSupportFileImports &&
validFilesEndingsToUpdate.some((e) => path.endsWith(e))
) {
updateImports(tree, newPath, oldImportLeafPath, newImportLeafPath);
}
});
if (tree.children(oldIntegrationFolder).length === 0) {
tree.delete(oldIntegrationFolder);
}
}
export function updateImports(
tree: Tree,
filePath: string,
oldImportPath: string,
newImportPath: string
) {
const endOfImportSelector = `StringLiteral[value=/${oldImportPath}$/]`;
const fileContent = tree.read(filePath, 'utf-8');
const newContent = tsquery.replace(
fileContent,
endOfImportSelector,
(node: StringLiteral) => {
// if node.parent is an CallExpression require() ||ImportDeclaration
if (
node?.parent &&
(isCallExpression(node.parent) ||
isImportDeclaration(node.parent) ||
isExportDeclaration(node.parent))
) {
return `'${node.text.replace(oldImportPath, newImportPath)}'`;
}
return node.text;
}
);
tree.write(filePath, newContent);
}
export function writeNewConfig(
tree: Tree,
cypressConfigPathTs: string,
cypressConfigs: {
cypressConfigTs: Record<string, any>;
cypressConfigJson: Record<string, any>;
}
) {
// remove deprecated configs options
const {
pluginsFile = false,
integrationFolder = '',
...restOfConfig
} = cypressConfigs.cypressConfigTs.e2e;
const pluginImport = pluginsFile
? `import setupNodeEvents from '${pluginsFile}';`
: '';
const convertedConfig = JSON.stringify(restOfConfig, null, 2);
tree.write(
cypressConfigPathTs,
stripIndents`
import { defineConfig } from 'cypress'
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
${pluginImport}
const cypressJsonConfig = ${convertedConfig}
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
...cypressJsonConfig,
${pluginsFile ? 'setupNodeEvents' : ''}
}
})
`
);
}
export function addConfigToTsConfig(
tree: Tree,
tsconfigPath: string,
cypressConfigPath: string
) {
if (tree.exists(tsconfigPath)) {
updateJson(
tree,
tsconfigPath,
(json) => {
json.include = Array.from(
new Set([
...(json.include || []),
relative(dirname(tsconfigPath), cypressConfigPath),
])
);
return json;
},
{ expectComments: true }
);
}
}

View File

@ -0,0 +1,450 @@
import {
addProjectConfiguration,
joinPathFragments,
readJson,
readProjectConfiguration,
Tree,
updateJson,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressProjectGenerator } from '../cypress-project/cypress-project';
import {
createSupportFileImport,
updateImports,
updateProjectPaths,
} from './conversion.util';
import { migrateCypressProject } from './migrate-to-cypress-ten';
jest.mock('../../utils/cypress-version');
describe('convertToCypressTen', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace(2);
mockedInstalledCypressVersion.mockReturnValue(9);
});
afterEach(() => {
jest.resetAllMocks();
});
describe('convertCypressProject', () => {
beforeEach(async () => {
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
targets: {
serve: {
executor: '@nrwl/web:file-server',
options: {},
},
},
});
mockedInstalledCypressVersion.mockReturnValue(9);
await cypressProjectGenerator(tree, {
name: 'app-e2e',
skipFormat: true,
project: 'app',
});
});
it('should update project w/defaults', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
});
it('should update project w/customized config', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
updateJson(tree, 'apps/app-e2e/cypress.json', (json) => {
json = {
...json,
baseUrl: 'http://localhost:4200',
supportFile: false,
pluginsFile: './src/plugins/index.js',
};
return json;
});
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
});
it('should not update a non e2e project', async () => {
await migrateCypressProject(tree);
expect(tree.exists('apps/app/cypress.config.ts')).toBeFalsy();
expect(tree.exists('apps/app/src/e2e/app.cy.ts')).toBeFalsy();
expect(tree.exists('apps/app/src/support/e2e.ts')).toBeFalsy();
});
it('should handle custom target names', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
const pc = readProjectConfiguration(tree, 'app-e2e');
pc.targets = {
'e2e-custom': {
...pc.targets['e2e'],
},
};
delete pc.targets['e2e'];
updateProjectConfiguration(tree, 'app-e2e', pc);
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
});
it('should infer targets with --all flag', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
const pc = readProjectConfiguration(tree, 'app-e2e');
pc.targets = {
...pc.targets,
'e2e-custom': {
...pc.targets['e2e'],
},
};
updateProjectConfiguration(tree, 'app-e2e', pc);
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
expect(
readProjectConfiguration(tree, 'app-e2e').targets
).toMatchSnapshot();
});
it('should not break when an invalid target is passed in', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
const pc = readProjectConfiguration(tree, 'app-e2e');
pc.targets = {
...pc.targets,
'e2e-custom': {
...pc.targets['e2e'],
},
};
updateProjectConfiguration(tree, 'app-e2e', pc);
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
expect(
readProjectConfiguration(tree, 'app-e2e').targets
).toMatchSnapshot();
});
it('should handle multiple configurations', async () => {
expect(tree.exists('apps/app-e2e/cypress.json')).toBeTruthy();
const pc = readProjectConfiguration(tree, 'app-e2e');
pc.targets = {
...pc.targets,
e2e: {
...pc.targets['e2e'],
configurations: {
production: {
devServerTarget: 'target:serve:production',
},
static: {
baseUrl: 'http://localhost:3000',
},
},
},
};
updateProjectConfiguration(tree, 'app-e2e', pc);
await migrateCypressProject(tree);
expect(tree.exists('apps/app-e2e/cypress.config.ts')).toBeTruthy();
expect(
tree.read('apps/app-e2e/cypress.config.ts', 'utf-8')
).toMatchSnapshot();
expect(readJson(tree, 'apps/app-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
'src/**/*.js',
'cypress.config.ts',
]);
expect(tree.exists('apps/app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('apps/app-e2e/src/support/e2e.ts')).toBeTruthy();
expect(
readProjectConfiguration(tree, 'app-e2e').targets['e2e']
).toMatchSnapshot();
});
});
describe('updateProjectPaths', () => {
const cypressConfigs = {
cypressConfigTs: {
e2e: {
integrationFolder: 'src/e2e',
supportFile: 'src/support/e2e.ts',
},
},
cypressConfigJson: {
integrationFolder: 'src/integration',
supportFile: 'src/support/index.ts',
},
};
const projectConfig = {
root: 'apps/app-e2e',
sourceRoot: 'apps/app-e2e/src',
};
const filePaths = [
'src/integration/something.spec.ts',
'src/integration/another.spec.ts',
'src/integration/another.spec.js',
'src/integration/another.spec.tsx',
'src/integration/another.spec.jsx',
'src/integration/another.spec.cjs',
'src/integration/another.spec.mjs',
'src/integration/blah/another/a.spec.ts',
'src/integration/blah/c/a.spec.ts',
'src/support/index.ts',
'src/plugins/index.ts',
'src/fixtures/example.json',
];
beforeEach(() => {
for (const path of filePaths) {
tree.write(
joinPathFragments(projectConfig.root, path),
String.raw`
import { getGreeting } from '../support/app.po';
import { blah } from '../support';
const eh = require('../support')
import { blah } from "../support";
const eh = require("../support")
import { blah } from '../../support';
const eh = require('../../support')
import { blah } from "../../support";
const eh = require("../../support")
`
);
}
});
it('should rename old support file to e2e.ts', () => {
const newSupportFile = joinPathFragments(
projectConfig.root,
cypressConfigs.cypressConfigTs.e2e.supportFile
);
const oldSupportFile = joinPathFragments(
projectConfig.root,
cypressConfigs.cypressConfigJson.supportFile
);
updateProjectPaths(tree, projectConfig, cypressConfigs);
expect(tree.exists(newSupportFile)).toEqual(true);
expect(tree.exists(oldSupportFile)).toEqual(false);
});
it('should rename files .spec. to .cy.', () => {
const specs = tree
.children(projectConfig.root)
.filter((path) => path.endsWith('.spec.ts'));
updateProjectPaths(tree, projectConfig, cypressConfigs);
const actualSpecs = tree
.children(projectConfig.root)
.filter((path) => path.endsWith('.spec.ts'));
const actualCy = tree
.children(projectConfig.root)
.filter((path) => path.endsWith('.cy.ts'));
expect(actualSpecs.length).toEqual(0);
expect(actualCy.length).toEqual(specs.length);
});
it('should move files to the new integration folder (e2e/)', () => {
const newIntegrationFolder = joinPathFragments(
projectConfig.root,
cypressConfigs.cypressConfigTs.e2e.integrationFolder
);
const oldIntegrationFolder = joinPathFragments(
projectConfig.root,
cypressConfigs.cypressConfigJson.integrationFolder
);
const oldIntegrationFolderContents = tree.children(oldIntegrationFolder);
updateProjectPaths(tree, projectConfig, cypressConfigs);
const newIntegrationFolderContents = tree.children(newIntegrationFolder);
expect(tree.exists(oldIntegrationFolder)).toEqual(false);
expect(newIntegrationFolderContents.length).toEqual(
oldIntegrationFolderContents.length
);
expect(tree.exists('apps/app-e2e/src/fixtures/example.json')).toEqual(
true
);
});
});
describe('updateImports', () => {
const filePath = 'apps/app-e2e/src/e2e/sometest.cy.ts';
const fileContents = String.raw`
import { getGreeting } from '../support/app.po';
import { blah } from '../support';
const eh = require('../support')
import { blah } from "../support";
const eh = require("../support")
describe('a', () => {
beforeEach(() => {
cy.visit('/')
blah()
eh()
});
it('should display welcome message', () => {
// Custom command example, see \`../support/commands.ts\` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see \`../support/app.po.ts\` file
getGreeting().contains('Welcome a');
});
});
`;
beforeEach(() => {
tree.write(filePath, fileContents);
});
it('should update imports', () => {
updateImports(tree, filePath, 'support', 'support/e2e');
const actual = tree.read(filePath, 'utf-8');
expect(actual).toMatchSnapshot();
});
});
describe('Support File Imports', () => {
const newImport = 'apps/app-e2e/src/support/e2e.ts';
it('should update imports w/defaults', () => {
const oldImport = 'apps/app-e2e/src/support/index.ts';
const actual = createSupportFileImport(
oldImport,
newImport,
'apps/app-e2e/src'
);
expect(actual).toEqual({
oldImportPathLeaf: 'support',
newImportPathLeaf: 'support/e2e',
});
});
it('should handle custom support file location', () => {
const oldImport = 'apps/app-e2e/src/support/blah.ts';
const actual = createSupportFileImport(
oldImport,
newImport,
'apps/app-e2e/src'
);
expect(actual).toEqual({
oldImportPathLeaf: 'support/blah',
newImportPathLeaf: 'support/e2e',
});
});
it('should handle nested custom support location', () => {
const oldImport = 'apps/app-e2e/src/support/blah/abc.ts';
const actual = createSupportFileImport(
oldImport,
newImport,
'apps/app-e2e/src'
);
expect(actual).toEqual({
oldImportPathLeaf: 'support/blah/abc',
newImportPathLeaf: 'support/e2e',
});
});
it('should handle nested custom support location w/index.ts', () => {
const oldImport = 'apps/app-e2e/src/support/something/neat/index.ts';
const actual = createSupportFileImport(
oldImport,
newImport,
'apps/app-e2e/src'
);
expect(actual).toEqual({
oldImportPathLeaf: 'support/something/neat',
newImportPathLeaf: 'support/e2e',
});
});
});
});

View File

@ -0,0 +1,95 @@
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
formatFiles,
installPackagesTask,
joinPathFragments,
logger,
readProjectConfiguration,
stripIndents,
Tree,
updateJson,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
import { cypressVersion } from '../../utils/versions';
import {
addConfigToTsConfig,
createNewCypressConfig,
findCypressConfigs,
updateProjectPaths,
writeNewConfig,
} from './conversion.util';
export async function migrateCypressProject(tree: Tree) {
assertMinimumCypressVersion(8);
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(currentValue, projectName, target, configuration) => {
try {
const projectConfig = readProjectConfiguration(tree, projectName);
const { cypressConfigPathJson, cypressConfigPathTs } =
findCypressConfigs(tree, projectConfig, target, configuration);
// a matching cypress ts file hasn't been made yet. need to migrate.
if (
tree.exists(cypressConfigPathJson) &&
!tree.exists(cypressConfigPathTs)
) {
const cypressConfigs = createNewCypressConfig(
tree,
projectConfig,
cypressConfigPathJson
);
updateProjectPaths(tree, projectConfig, cypressConfigs);
writeNewConfig(tree, cypressConfigPathTs, cypressConfigs);
addConfigToTsConfig(
tree,
projectConfig.targets?.[target]?.configurations?.tsConfig ||
projectConfig.targets?.[target]?.options?.tsConfig ||
joinPathFragments(projectConfig.root, 'tsconfig.json'),
cypressConfigPathTs
);
tree.delete(cypressConfigPathJson);
}
// ts file has been made and matching json file has been removed only need to update the project config
if (
!tree.exists(cypressConfigPathJson) &&
tree.exists(cypressConfigPathTs)
) {
projectConfig.targets[target].options = {
...projectConfig.targets[target].options,
cypressConfig: cypressConfigPathTs,
testingType: 'e2e',
};
updateProjectConfiguration(tree, projectName, projectConfig);
}
} catch (e) {
logger.error(stripIndents`
NX There was an error converting ${projectName}:${target}.
You can manually update the project by following the migration guide if need be.
https://nx.dev/cypress/v10-migration-guide
`);
throw e;
}
}
);
updateJson(tree, 'package.json', (json) => {
json.devDependencies['cypress'] = cypressVersion;
return json;
});
await formatFiles(tree);
return () => {
installPackagesTask(tree);
};
}
export default migrateCypressProject;

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxCypressMigrateToTen",
"cli": "nx",
"title": "Migrate e2e project to Cypress 10",
"description": "Migrate Cypress e2e project from v8/v9 to Cypress v10.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/cypress:migrate-to-cypress-10",
"description": "Migrate existing cypress projects to Cypress v10"
}
],
"properties": {}
}

View File

@ -18,3 +18,15 @@ export function installedCypressVersion() {
}
return +majorVersion;
}
/**
* will not throw if cypress is not installed
*/
export function assertMinimumCypressVersion(minVersion: number) {
const version = installedCypressVersion();
if (version && version < minVersion) {
throw new Error(
`Cypress version of ${minVersion} or higher is not installed. Expected Cypress v${minVersion}+, found Cypress v${version} instead.`
);
}
}

View File

@ -1,4 +1,6 @@
export const nxVersion = require('../../package.json').version;
export const cypressVersion = '^9.1.0';
export const eslintPluginCypressVersion = '^2.10.3';
export const typesNodeVersion = '16.11.7';
export const cypressVersion = '^10.2.0';
export const cypressWebpackVersion = '^2.0.0';
export const webpackHttpPluginVersion = '^5.5.0';

View File

@ -1,8 +1,8 @@
import { addStyleDependencies } from '../../utils/styles';
import { convertNxGenerator, getProjects, Tree } from '@nrwl/devkit';
import type { SupportedStyles } from '@nrwl/react';
import { componentGenerator as reactComponentGenerator } from '@nrwl/react';
import { convertNxGenerator, getProjects, Tree } from '@nrwl/devkit';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { addStyleDependencies } from '../../utils/styles';
interface Schema {
name: string;

View File

@ -1,10 +1,16 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from './library';
import { Linter } from '@nrwl/linter';
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import { readJson } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import libraryGenerator from './library';
import { Schema } from './schema';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('next library', () => {
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
it('should use "@nrwl/next/babel" preset in babelrc', async () => {
const baseOptions: Schema = {
name: '',

View File

@ -1,5 +1,3 @@
import { libraryGenerator as reactLibraryGenerator } from '@nrwl/react';
import { nextInitGenerator } from '../init/init';
import {
convertNxGenerator,
GeneratorCallback,
@ -9,8 +7,10 @@ import {
Tree,
updateJson,
} from '@nrwl/devkit';
import { Schema } from './schema';
import { libraryGenerator as reactLibraryGenerator } from '@nrwl/react';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { nextInitGenerator } from '../init/init';
import { Schema } from './schema';
export async function libraryGenerator(host: Tree, options: Schema) {
const name = names(options.name).fileName;

View File

@ -1,5 +1,5 @@
import type { SupportedStyles } from '@nrwl/react';
import { Linter } from '@nrwl/linter';
import type { SupportedStyles } from '@nrwl/react';
export interface Schema {
name: string;

View File

@ -74,6 +74,20 @@
"schema": "./src/generators/hook/schema.json",
"description": "Create a hook.",
"aliases": ["h"]
},
"cypress-component-configuration": {
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
"schema": "./src/generators/cypress-component-configuration/schema.json",
"description": "Setup Cypress component testing for a React project",
"hidden": false
},
"component-test": {
"factory": "./src/generators/component-test/component-test#componentTestGenerator",
"schema": "./src/generators/component-test/schema.json",
"description": "Generate a Cypress component test for a React component",
"hidden": false
}
},
"generators": {
@ -162,6 +176,20 @@
"schema": "./src/generators/remote/schema.json",
"x-type": "application",
"description": "Generate a remote react application"
},
"cypress-component-configuration": {
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
"schema": "./src/generators/cypress-component-configuration/schema.json",
"description": "Setup Cypress component testing for a React project",
"hidden": false
},
"component-test": {
"factory": "./src/generators/component-test/component-test#componentTestGenerator",
"schema": "./src/generators/component-test/schema.json",
"description": "Generate a Cypress component test for a React component",
"hidden": false
}
}
}

View File

@ -16,4 +16,6 @@ export { reduxGenerator } from './src/generators/redux/redux';
export { storiesGenerator } from './src/generators/stories/stories';
export { storybookConfigurationGenerator } from './src/generators/storybook-configuration/configuration';
export { hostGenerator } from './src/generators/host/host';
export { cypressComponentConfigGenerator } from './src/generators/cypress-component-configuration/cypress-component-configuration';
export { componentTestGenerator } from './src/generators/component-test/component-test';
export type { SupportedStyles } from './typings/style';

View File

@ -0,0 +1,179 @@
import { nxBaseCypressPreset } from '@nrwl/cypress/plugins/cypress-preset';
import { getCSSModuleLocalIdent } from '@nrwl/web/src/utils/web.config';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import type { Configuration } from 'webpack';
/**
* React nx preset for Cypress Component Testing
*
* This preset contains the base configuration
* for your component tests that nx recommends.
* including a devServer that supports nx workspaces.
* you can easily extend this within your cypress config via spreading the preset
* @example
* export default defineConfig({
* component: {
* ...nxComponentTestingPreset(__dirname)
* // add your own config here
* }
* })
*
* @param pathToConfig will be used to construct the output paths for videos and screenshots
*/
export function nxComponentTestingPreset(pathToConfig: string) {
return {
...nxBaseCypressPreset(pathToConfig),
devServer: {
// cypress uses string union type,
// need to use const to prevent typing to string
framework: 'react',
bundler: 'webpack',
webpackConfig: buildBaseWebpackConfig({
tsConfigPath: 'tsconfig.cy.json',
compiler: 'babel',
}),
} as const,
};
}
// TODO(caleb): use the webpack utils to build the config
// can't seem to get css modules to play nice when using it 🤔
function buildBaseWebpackConfig({
tsConfigPath = 'tsconfig.cy.json',
compiler = 'babel',
}: {
tsConfigPath: string;
compiler: 'swc' | 'babel';
}): Configuration {
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const config: Configuration = {
target: 'web',
resolve: {
extensions,
plugins: [
new TsconfigPathsPlugin({
configFile: tsConfigPath,
extensions,
}) as never,
],
},
mode: 'development',
devtool: false,
output: {
publicPath: '/',
chunkFilename: '[name].bundle.js',
},
module: {
rules: [
{
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
CSS_MODULES_LOADER,
],
},
};
if (compiler === 'swc') {
config.module.rules.push({
test: /\.([jt])sx?$/,
loader: require.resolve('swc-loader'),
exclude: /node_modules/,
options: {
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
loose: true,
},
},
});
}
if (compiler === 'babel') {
config.module.rules.push({
test: /\.(js|jsx|mjs|ts|tsx)$/,
loader: require.resolve('babel-loader'),
options: {
presets: [`@nrwl/react/babel`],
rootMode: 'upward',
babelrc: true,
},
});
}
return config;
}
const loaderModulesOptions = {
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
importLoaders: 1,
};
const commonLoaders = [
{
loader: require.resolve('style-loader'),
},
{
loader: require.resolve('css-loader'),
options: loaderModulesOptions,
},
];
const CSS_MODULES_LOADER = {
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
oneOf: [
{
test: /\.module\.css$/,
use: commonLoaders,
},
{
test: /\.module\.(scss|sass)$/,
use: [
...commonLoaders,
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sassOptions: {
fiber: false,
precision: 8,
},
},
},
],
},
{
test: /\.module\.less$/,
use: [
...commonLoaders,
{
loader: require.resolve('less-loader'),
},
],
},
{
test: /\.module\.styl$/,
use: [
...commonLoaders,
{
loader: require.resolve('stylus-loader'),
},
],
},
],
};

View File

@ -1,3 +1,4 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
getProjects,
readJson,
@ -5,10 +6,12 @@ import {
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import { applicationGenerator } from './application';
import { Schema } from './schema';
import { Linter } from '@nrwl/linter';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('app', () => {
let appTree: Tree;
let schema: Schema = {
@ -22,8 +25,11 @@ describe('app', () => {
strict: true,
standaloneConfig: false,
};
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
appTree = createTreeWithEmptyWorkspace();
});
@ -108,26 +114,27 @@ describe('app', () => {
'../../.eslintrc.json',
]);
expect(appTree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
expect(appTree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy();
const tsconfigE2E = readJson(appTree, 'apps/my-app-e2e/tsconfig.json');
expect(tsconfigE2E).toMatchInlineSnapshot(`
Object {
"compilerOptions": Object {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": Array [
"cypress",
"node",
],
},
"extends": "../../tsconfig.base.json",
"include": Array [
"src/**/*.ts",
"src/**/*.js",
],
}
`);
Object {
"compilerOptions": Object {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": Array [
"cypress",
"node",
],
},
"extends": "../../tsconfig.base.json",
"include": Array [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`);
});
it('should extend from root tsconfig.base.json', async () => {
@ -368,18 +375,18 @@ Object {
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app').targets.test).toBeUndefined();
expect(workspaceJson.get('my-app').targets.lint).toMatchInlineSnapshot(`
Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app/**/*.{ts,tsx,js,jsx}",
],
},
"outputs": Array [
"{options.outputFile}",
],
}
`);
Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app/**/*.{ts,tsx,js,jsx}",
],
},
"outputs": Array [
"{options.outputFile}",
],
}
`);
});
});

View File

@ -1,10 +1,10 @@
import { Tree } from '@nrwl/devkit';
import componentCypressSpecGenerator from './component-cypress-spec';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from '../library/library';
import { Linter } from '@nrwl/linter';
import applicationGenerator from '../application/application';
import { formatFile } from '../../utils/format-file';
import applicationGenerator from '../application/application';
import libraryGenerator from '../library/library';
import componentCypressSpecGenerator from './component-cypress-spec';
describe('react:component-cypress-spec', () => {
let appTree: Tree;
@ -155,6 +155,34 @@ describe('react:component-cypress-spec', () => {
unitTestRunner: 'none',
standaloneConfig: false,
});
// since other-e2e isn't a real cypress project we mock the v10 cypress config
appTree.write('apps/other-e2e/cypress.config.ts', `export default {}`);
await componentCypressSpecGenerator(appTree, {
componentPath: `lib/test-ui-lib.tsx`,
project: 'test-ui-lib',
cypressProject: 'other-e2e',
});
expect(
appTree.exists('apps/other-e2e/src/e2e/test-ui-lib/test-ui-lib.cy.ts')
).toBeTruthy();
expect(
appTree.exists('apps/test-ui-lib/src/e2e/test-ui-lib/test-ui-lib.cy.ts')
).toBeFalsy();
});
it('should generate a .spec.ts file with cypress.json', async () => {
appTree = await createTestUILib('test-ui-lib');
await applicationGenerator(appTree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
name: `other-e2e`,
skipFormat: true,
style: 'css',
unitTestRunner: 'none',
standaloneConfig: false,
});
appTree.delete(`apps/other-e2e/cypress.config.ts`);
appTree.write(`apps/other-e2e/cypress.json`, '{}');
await componentCypressSpecGenerator(appTree, {
componentPath: `lib/test-ui-lib.tsx`,
project: 'test-ui-lib',

View File

@ -1,10 +1,3 @@
import {
findExportDeclarationsForJsx,
getComponentNode,
getComponentPropsInterface,
} from '../../utils/ast-utils';
import * as ts from 'typescript';
import {
convertNxGenerator,
generateFiles,
@ -12,6 +5,13 @@ import {
joinPathFragments,
Tree,
} from '@nrwl/devkit';
import { basename, join } from 'path';
import * as ts from 'typescript';
import {
findExportDeclarationsForJsx,
getComponentNode,
getComponentPropsInterface,
} from '../../utils/ast-utils';
export interface CreateComponentSpecFileSchema {
project: string;
@ -51,9 +51,14 @@ export function createComponentSpecFile(
) {
const e2eProjectName = cypressProject || `${project}-e2e`;
const projects = getProjects(tree);
const e2eLibIntegrationFolderPath = `${
projects.get(e2eProjectName).sourceRoot
}/integration`;
const e2eProject = projects.get(e2eProjectName);
// cypress >= v10 will have a cypress.config.ts < v10 will have a cypress.json
const isCypressV10 = tree.exists(join(e2eProject.root, 'cypress.config.ts'));
const e2eLibIntegrationFolderPath = join(
e2eProject.sourceRoot,
isCypressV10 ? 'e2e' : 'integration'
);
const proj = projects.get(project);
const componentFilePath = joinPathFragments(proj.sourceRoot, componentPath);
@ -135,6 +140,9 @@ function findPropsAndGenerateFileForCypress(
});
}
const isCypressV10 = basename(e2eLibIntegrationFolderPath) === 'e2e';
const cyFilePrefix = isCypressV10 ? 'cy' : 'spec';
generateFiles(
tree,
joinPathFragments(__dirname, './files'),
@ -148,7 +156,7 @@ function findPropsAndGenerateFileForCypress(
componentName,
componentSelector: (cmpDeclaration as any).name.text,
props,
fileExt: js ? 'js' : 'ts',
fileExt: js ? `${cyFilePrefix}.js` : `${cyFilePrefix}.ts`,
}
);
}

View File

@ -11,30 +11,14 @@ import * as ts from 'typescript';
import {
findExportDeclarationsForJsx,
getComponentNode,
getComponentPropsInterface,
} from '../../utils/ast-utils';
import { getDefaultsForComponent } from '../../utils/component-props';
export interface CreateComponentStoriesFileSchema {
project: string;
componentPath: string;
}
// TODO: candidate to refactor with the angular component story
export function getArgsDefaultValue(property: ts.SyntaxKind): string {
const typeNameToDefault: Record<number, any> = {
[ts.SyntaxKind.StringKeyword]: "''",
[ts.SyntaxKind.NumberKeyword]: 0,
[ts.SyntaxKind.BooleanKeyword]: false,
};
const resolvedValue = typeNameToDefault[property];
if (typeof resolvedValue === undefined) {
return "''";
} else {
return resolvedValue;
}
}
export function createComponentStoriesFile(
host: Tree,
{ project, componentPath }: CreateComponentStoriesFileSchema
@ -123,37 +107,10 @@ export function findPropsAndGenerateFile(
fileExt: string,
fromNodeArray?: boolean
) {
const propsInterface = getComponentPropsInterface(sourceFile, cmpDeclaration);
let propsTypeName: string = null;
let props: {
name: string;
defaultValue: any;
}[] = [];
let argTypes: {
name: string;
type: string;
actionText: string;
}[] = [];
if (propsInterface) {
propsTypeName = propsInterface.name.text;
props = propsInterface.members.map((member: ts.PropertySignature) => {
if (member.type.kind === ts.SyntaxKind.FunctionType) {
argTypes.push({
name: (member.name as ts.Identifier).text,
type: 'action',
actionText: `${(member.name as ts.Identifier).text} executed!`,
});
} else {
return {
name: (member.name as ts.Identifier).text,
defaultValue: getArgsDefaultValue(member.type.kind),
};
}
});
props = props.filter((p) => p && p.defaultValue !== undefined);
}
const { propsTypeName, props, argTypes } = getDefaultsForComponent(
sourceFile,
cmpDeclaration
);
generateFiles(
host,

View File

@ -0,0 +1,232 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`componentTestGenerator multiple components per file should handle default export 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import AnotherCmp, { AnotherCmpProps, AnotherCmp2 } from './some-lib'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
describe(AnotherCmp2.name, () => {
it('renders', () => {
mount(<AnotherCmp2 />)
})
})
"
`;
exports[`componentTestGenerator multiple components per file should handle named exports 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import { AnotherCmpProps, AnotherCmp, AnotherCmp2 } from './some-lib'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
describe(AnotherCmp2.name, () => {
it('renders', () => {
mount(<AnotherCmp2 />)
})
})
"
`;
exports[`componentTestGenerator multiple components per file should handle no props 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import SomeLib, { SomeLibProps, AnotherCmp } from './some-lib'
describe(SomeLib.name, () => {
let props: SomeLibProps;
beforeEach(() => {
props = {
}
})
it('renders', () => {
mount(<SomeLib {...props}/>)
})
})
describe(AnotherCmp.name, () => {
it('renders', () => {
mount(<AnotherCmp />)
})
})
"
`;
exports[`componentTestGenerator multiple components per file should handle props 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import SomeLib, { SomeLibProps, AnotherCmpProps, AnotherCmp } from './some-lib'
describe(SomeLib.name, () => {
let props: SomeLibProps;
beforeEach(() => {
props = {
}
})
it('renders', () => {
mount(<SomeLib {...props}/>)
})
})
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
"
`;
exports[`componentTestGenerator single component per file should handle default export 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import AnotherCmp, { AnotherCmpProps } from './some-lib'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
"
`;
exports[`componentTestGenerator single component per file should handle named exports 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import { AnotherCmpProps, AnotherCmp } from './some-lib'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
"
`;
exports[`componentTestGenerator single component per file should handle no props 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import SomeLib, { SomeLibProps } from './some-lib'
describe(SomeLib.name, () => {
let props: SomeLibProps;
beforeEach(() => {
props = {
}
})
it('renders', () => {
mount(<SomeLib {...props}/>)
})
})
"
`;
exports[`componentTestGenerator single component per file should handle props 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import { AnotherCmpProps, AnotherCmp } from './some-lib'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
text: '',
count: 0,
isOkay: false,
handleClick: undefined
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
"
`;

View File

@ -0,0 +1,396 @@
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import libraryGenerator from '../library/library';
import { componentTestGenerator } from './component-test';
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe(componentTestGenerator.name, () => {
let tree: Tree;
let mockedAssertMinimumCypressVersion: jest.Mock<
ReturnType<typeof assertMinimumCypressVersion>
> = assertMinimumCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should create component test for tsx files', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
});
it('should create component test for js files', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
js: true,
});
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'lib/some-lib.js',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.js')).toBeTruthy();
});
it('should not throw if path is invalid', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
expect(() => {
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'lib/blah/abc-123.blah',
});
}).not.toThrow();
});
it('should handle being provided the full path to the component', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
});
describe('multiple components per file', () => {
it('should handle props', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
${tree.read('libs/some-lib/src/lib/some-lib.tsx')}
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle no props', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
${tree.read('libs/some-lib/src/lib/some-lib.tsx')}
export function AnotherCmp() {
return <button>AnotherCmp</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle default export', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export default function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
export function AnotherCmp2() {
return <button>AnotherCmp</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle named exports', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
export function AnotherCmp2() {
return <button>AnotherCmp2</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
});
describe('single component per file', () => {
it('should handle props', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle no props', async () => {
// this is the default behavior of the library component generator
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle default export', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export default function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
it('should handle named exports', async () => {
mockedAssertMinimumCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
tree.write(
'libs/some-lib/src/lib/some-lib.tsx',
`
/* eslint-disable-next-line */
export interface AnotherCmpProps {
handleClick: () => void;
text: string;
count: number;
isOkay: boolean;
}
export function AnotherCmp(props: AnotherCmpProps) {
return <button onClick="{handleClick}">{props.text}</button>;
}
`
);
componentTestGenerator(tree, {
project: 'some-lib',
componentPath: 'libs/some-lib/src/lib/some-lib.tsx',
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
expect(
tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,98 @@
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
generateFiles,
joinPathFragments,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { basename, dirname, extname, relative } from 'path';
import * as ts from 'typescript';
import {
findExportDeclarationsForJsx,
getComponentNode,
} from '../../utils/ast-utils';
import { getDefaultsForComponent } from '../../utils/component-props';
import { ComponentTestSchema } from './schema';
export function componentTestGenerator(
tree: Tree,
options: ComponentTestSchema
) {
assertMinimumCypressVersion(10);
const projectConfig = readProjectConfiguration(tree, options.project);
const normalizedPath = options.componentPath.startsWith(
projectConfig.sourceRoot
)
? relative(projectConfig.sourceRoot, options.componentPath)
: options.componentPath;
const componentPath = joinPathFragments(
projectConfig.sourceRoot,
normalizedPath
);
if (tree.exists(componentPath)) {
generateSpecsForComponents(tree, componentPath);
}
}
function generateSpecsForComponents(tree: Tree, filePath: string) {
const sourceFile = ts.createSourceFile(
filePath,
tree.read(filePath, 'utf-8'),
ts.ScriptTarget.Latest,
true
);
const cmpNodes = findExportDeclarationsForJsx(sourceFile);
const componentDir = dirname(filePath);
const ext = extname(filePath);
const fileName = basename(filePath, ext);
const defaultExport = getComponentNode(sourceFile);
if (cmpNodes?.length) {
const components = cmpNodes.map((cmp) => {
const defaults = getDefaultsForComponent(sourceFile, cmp);
const isDefaultExport = defaultExport
? (defaultExport as any).name.text === (cmp as any).name.text
: false;
return {
isDefaultExport,
props: [...defaults.props, ...defaults.argTypes],
name: (cmp as any).name.text as string,
typeName: defaults.propsTypeName,
};
});
const namedImports = components
.reduce((imports, cmp) => {
if (cmp.typeName) {
imports.push(cmp.typeName);
}
if (cmp.isDefaultExport) {
return imports;
}
imports.push(cmp.name);
return imports;
}, [])
.join(', ');
const namedImportStatement =
namedImports.length > 0 ? `, { ${namedImports} }` : '';
generateFiles(tree, joinPathFragments(__dirname, 'files'), componentDir, {
fileName,
components,
importStatement: defaultExport
? `import ${
(defaultExport as any).name.text
}${namedImportStatement} from './${fileName}'`
: `import { ${namedImports} } from './${fileName}'`,
ext,
});
}
}
export default componentTestGenerator;

View File

@ -0,0 +1,22 @@
import * as React from 'react'
import { mount } from 'cypress/react'
<%- importStatement %>
<% for (let cmp of components) { %>
describe(<%= cmp.name %>.name, () => {<% if (cmp.typeName) { %>
let props: <%= cmp.typeName%>;
beforeEach(() => {
props = {<% for (let prop of cmp.props) { %>
<%- prop.name %>: <% if(prop.type === 'action') { %>undefined<% } else { %><%- prop.defaultValue %>,<% } %><% }%>
}
})
it('renders', () => {
mount(<<%= cmp.name %> {...props}/>)
})<% } else { %>
it('renders', () => {
mount(<<%= cmp.name %> />)
})<% } %>
})
<% } %>

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxReactCypressComponentTest",
"title": "Add Cypress component test",
"description": "Add a Cypress component test for a component.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/react:component-test --project=my-react-project --component-path=src/lib/fancy-component.tsx",
"description": "Create a cypress component test for FancyComponent"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project the component is apart of",
"$default": {
"$source": "projectName"
},
"x-prompt": "What project is this component apart of?"
},
"componentPath": {
"type": "string",
"description": "Path to component, from the project source root",
"x-prompt": "What is the path to the component?"
}
},
"required": ["project", "componentPath"]
}

View File

@ -0,0 +1,4 @@
export interface ComponentTestSchema {
project: string;
componentPath: string;
}

View File

@ -1,13 +1,20 @@
import { createApp, createLib } from '../../utils/testing-generators';
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import { logger, readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createApp, createLib } from '../../utils/testing-generators';
import { componentGenerator } from './component';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('component', () => {
let appTree: Tree;
let projectName: string;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
projectName = 'my-lib';
appTree = createTreeWithEmptyWorkspace();
await createApp(appTree, 'my-app', false);

View File

@ -1,11 +1,3 @@
import * as ts from 'typescript';
import { Schema } from './schema';
import {
reactRouterDomVersion,
typesReactRouterDomVersion,
} from '../../utils/versions';
import { assertValidStyle } from '../../utils/assertion';
import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies';
import {
addDependenciesToPackageJson,
applyChangesToString,
@ -21,7 +13,15 @@ import {
Tree,
} from '@nrwl/devkit';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import * as ts from 'typescript';
import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies';
import { assertValidStyle } from '../../utils/assertion';
import { addImport } from '../../utils/ast-utils';
import {
reactRouterDomVersion,
typesReactRouterDomVersion,
} from '../../utils/versions';
import { Schema } from './schema';
interface NormalizedSchema extends Schema {
projectSourceRoot: string;

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`React:CypressComponentTestConfiguration should generate tests for existing js components 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import SomeCmp from './some-cmp'
describe(SomeCmp.name, () => {
it('renders', () => {
mount(<SomeCmp />)
})
})
"
`;
exports[`React:CypressComponentTestConfiguration should generate tests for existing js components 2`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import AnotherCmp from './another-cmp'
describe(AnotherCmp.name, () => {
it('renders', () => {
mount(<AnotherCmp />)
})
})
"
`;
exports[`React:CypressComponentTestConfiguration should generate tests for existing tsx components 1`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import SomeLib, { SomeLibProps } from './some-lib'
describe(SomeLib.name, () => {
let props: SomeLibProps;
beforeEach(() => {
props = {
}
})
it('renders', () => {
mount(<SomeLib {...props}/>)
})
})
"
`;
exports[`React:CypressComponentTestConfiguration should generate tests for existing tsx components 2`] = `
"import * as React from 'react'
import { mount } from 'cypress/react'
import AnotherCmp, { AnotherCmpProps } from './another-cmp'
describe(AnotherCmp.name, () => {
let props: AnotherCmpProps;
beforeEach(() => {
props = {
}
})
it('renders', () => {
mount(<AnotherCmp {...props}/>)
})
})
"
`;

View File

@ -0,0 +1,153 @@
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import { readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import componentGenerator from '../component/component';
import libraryGenerator from '../library/library';
import { cypressComponentConfigGenerator } from './cypress-component-configuration';
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('React:CypressComponentTestConfiguration', () => {
let tree: Tree;
let mockedAssertCypressVersion: jest.Mock<
ReturnType<typeof assertMinimumCypressVersion>
> = assertMinimumCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should generate cypress component test config', async () => {
mockedAssertCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
await cypressComponentConfigGenerator(tree, {
project: 'some-lib',
generateTests: false,
});
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
expect(config).toContain(
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
);
expect(config).toContain('component: nxComponentTestingPreset(__dirname),');
const cyTsConfig = readJson(tree, 'libs/some-lib/tsconfig.cy.json');
expect(cyTsConfig.include).toEqual([
'cypress.config.ts',
'**/*.cy.ts',
'**/*.cy.tsx',
'**/*.cy.js',
'**/*.cy.jsx',
'**/*.d.ts',
]);
const libTsConfig = readJson(tree, 'libs/some-lib/tsconfig.lib.json');
expect(libTsConfig.exclude).toEqual(
expect.arrayContaining([
'cypress/**/*',
'cypress.config.ts',
'**/*.cy.ts',
'**/*.cy.js',
'**/*.cy.tsx',
'**/*.cy.jsx',
])
);
const baseTsConfig = readJson(tree, 'libs/some-lib/tsconfig.json');
expect(baseTsConfig.references).toEqual(
expect.arrayContaining([{ path: './tsconfig.cy.json' }])
);
});
it('should generate tests for existing tsx components', async () => {
mockedAssertCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'jest',
component: true,
});
await componentGenerator(tree, {
name: 'another-cmp',
project: 'some-lib',
style: 'scss',
});
await cypressComponentConfigGenerator(tree, {
project: 'some-lib',
generateTests: true,
});
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
const compTest = tree.read(
'libs/some-lib/src/lib/some-lib.cy.tsx',
'utf-8'
);
expect(compTest).toMatchSnapshot();
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
const compTestNested = tree.read(
'libs/some-lib/src/lib/another-cmp/another-cmp.cy.tsx',
'utf-8'
);
expect(compTestNested).toMatchSnapshot();
expect(
tree.exists('libs/some-lib/src/lib/another-cmp/another-cmp.spec.cy.tsx')
).toBeFalsy();
});
it('should generate tests for existing js components', async () => {
mockedAssertCypressVersion.mockReturnValue();
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'jest',
js: true,
});
await componentGenerator(tree, {
name: 'some-cmp',
flat: true,
project: 'some-lib',
style: 'scss',
js: true,
});
await componentGenerator(tree, {
name: 'another-cmp',
project: 'some-lib',
style: 'scss',
js: true,
});
await cypressComponentConfigGenerator(tree, {
project: 'some-lib',
generateTests: true,
});
expect(tree.exists('libs/some-lib/src/lib/some-cmp.cy.js')).toBeTruthy();
const compTest = tree.read('libs/some-lib/src/lib/some-cmp.cy.js', 'utf-8');
expect(compTest).toMatchSnapshot();
expect(
tree.exists('libs/some-lib/src/lib/another-cmp/another-cmp.cy.js')
).toBeTruthy();
const compTestNested = tree.read(
'libs/some-lib/src/lib/another-cmp/another-cmp.cy.js',
'utf-8'
);
expect(compTestNested).toMatchSnapshot();
expect(
tree.exists('libs/some-lib/src/lib/another-cmp/another-cmp.spec.cy.js')
).toBeFalsy();
});
});

View File

@ -0,0 +1,152 @@
import { cypressComponentProject } from '@nrwl/cypress';
import {
formatFiles,
generateFiles,
joinPathFragments,
ProjectConfiguration,
readProjectConfiguration,
Tree,
updateJson,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import * as ts from 'typescript';
import { getComponentNode } from '../../utils/ast-utils';
import componentTestGenerator from '../component-test/component-test';
import { CypressComponentConfigurationSchema } from './schema';
const allowedFileExt = new RegExp(/\.[jt]sx?/g);
const isSpecFile = new RegExp(/(spec|test)\./g);
/**
* This is for using cypresses own Component testing, if you want to use test
* storybook components then use componentCypressGenerator instead.
*
*/
export async function cypressComponentConfigGenerator(
tree: Tree,
options: CypressComponentConfigurationSchema
) {
const projectConfig = readProjectConfiguration(tree, options.project);
const installTask = await cypressComponentProject(tree, {
project: options.project,
skipFormat: true,
});
addFiles(tree, projectConfig, options);
updateTsConfig(tree, projectConfig);
if (options.skipFormat) {
await formatFiles(tree);
}
return () => {
installTask();
};
}
function addFiles(
tree: Tree,
projectConfig: ProjectConfiguration,
options: CypressComponentConfigurationSchema
) {
const cypressConfigPath = joinPathFragments(
projectConfig.root,
'cypress.config.ts'
);
if (tree.exists(cypressConfigPath)) {
tree.delete(cypressConfigPath);
}
generateFiles(
tree,
joinPathFragments(__dirname, 'files'),
projectConfig.root,
{
tpl: '',
}
);
if (options.generateTests) {
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
if (isComponent(tree, filePath)) {
componentTestGenerator(tree, {
project: options.project,
componentPath: filePath,
});
}
});
}
}
function updateTsConfig(tree: Tree, projectConfig: ProjectConfiguration) {
const tsConfigPath = joinPathFragments(
projectConfig.root,
projectConfig.projectType === 'library'
? 'tsconfig.lib.json'
: 'tsconfig.app.json'
);
if (tree.exists(tsConfigPath)) {
updateJson(tree, tsConfigPath, (json) => {
const excluded = new Set([
...(json.exclude || []),
'cypress/**/*',
'cypress.config.ts',
'**/*.cy.ts',
'**/*.cy.js',
'**/*.cy.tsx',
'**/*.cy.jsx',
]);
json.exclude = Array.from(excluded);
return json;
});
}
const projectBaseTsConfig = joinPathFragments(
projectConfig.root,
'tsconfig.json'
);
if (tree.exists(projectBaseTsConfig)) {
updateJson(tree, projectBaseTsConfig, (json) => {
if (json.references) {
const hasCyTsConfig = json.references.some(
(r) => r.path === './tsconfig.cy.json'
);
if (!hasCyTsConfig) {
json.references.push({ path: './tsconfig.cy.json' });
}
} else {
const excluded = new Set([
...(json.exclude || []),
'cypress/**/*',
'cypress.config.ts',
'**/*.cy.ts',
'**/*.cy.js',
'**/*.cy.tsx',
'**/*.cy.jsx',
]);
json.exclude = Array.from(excluded);
}
return json;
});
}
}
function isComponent(tree: Tree, filePath: string): boolean {
if (isSpecFile.test(filePath) || !allowedFileExt.test(filePath)) {
return false;
}
const content = tree.read(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(
filePath,
content,
ts.ScriptTarget.Latest,
true
);
const cmpDeclaration = getComponentNode(sourceFile);
return !!cmpDeclaration;
}
export default cypressComponentConfigGenerator;

View File

@ -0,0 +1,6 @@
import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__dirname),
});

View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxReactCypressComponentTestConfiguration",
"title": "Add Cypress component testing",
"description": "Add a Cypress component testing configuration to an existing project.",
"type": "object",
"examples": [
{
"command": "nx g @nrwl/react:cypress-component-configuration --project=my-react-project",
"description": "Add component testing to your react project"
},
{
"command": "nx g @nrwl/react:cypress-component-configuration --project=my-react-project --generate-tests",
"description": "Add component testing to your react project and generate component tests for your existing components"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing configuration to",
"$default": {
"$source": "projectName"
},
"x-prompt": "What project should we add Cypress component testing to?"
},
"generateTests": {
"type": "boolean",
"description": "Generate default component tests for existing components in the project",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false
}
},
"required": ["project"]
}

View File

@ -0,0 +1,5 @@
export interface CypressComponentConfigurationSchema {
project: string;
generateTests: boolean;
skipFormat?: boolean;
}

View File

@ -1,3 +1,4 @@
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import {
getProjects,
readJson,
@ -6,14 +7,18 @@ import {
updateJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from './library';
import { Linter } from '@nrwl/linter';
import { Schema } from './schema';
import applicationGenerator from '../application/application';
import libraryGenerator from './library';
import { Schema } from './schema';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
describe('lib', () => {
let appTree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
let defaultSchema: Schema = {
name: 'myLib',
linter: Linter.EsLint,
@ -27,6 +32,7 @@ describe('lib', () => {
};
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
appTree = createTreeWithEmptyWorkspace();
});

View File

@ -1,5 +1,5 @@
import { SupportedStyles } from '../../../typings/style';
import { Linter } from '@nrwl/linter';
import { SupportedStyles } from '../../../typings/style';
export interface Schema {
name: string;

Some files were not shown because too many files have changed in this diff Show More