feat(testing): add support for cypress v14 (#30618)
## Current Behavior Cypress v14 is not supported. ## Expected Behavior Cypress v14 is supported. ## Related Issue(s) Fixes #30097
This commit is contained in:
parent
3b91e0b32d
commit
5feafd64d4
@ -1372,6 +1372,56 @@
|
||||
}
|
||||
},
|
||||
"migrations": {
|
||||
"/nx-api/cypress/migrations/set-inject-document-domain": {
|
||||
"description": "Replaces the `experimentalSkipDomainInjection` configuration option with the new `injectDocumentDomain` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/set-inject-document-domain.json",
|
||||
"hidden": false,
|
||||
"name": "set-inject-document-domain",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "/nx-api/cypress/migrations/set-inject-document-domain",
|
||||
"type": "migration"
|
||||
},
|
||||
"/nx-api/cypress/migrations/remove-experimental-fetch-polyfill": {
|
||||
"description": "Removes the `experimentalFetchPolyfill` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/remove-experimental-fetch-polyfill.json",
|
||||
"hidden": false,
|
||||
"name": "remove-experimental-fetch-polyfill",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "/nx-api/cypress/migrations/remove-experimental-fetch-polyfill",
|
||||
"type": "migration"
|
||||
},
|
||||
"/nx-api/cypress/migrations/replace-experimental-just-in-time-compile": {
|
||||
"description": "Replaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/replace-experimental-just-in-time-compile.json",
|
||||
"hidden": false,
|
||||
"name": "replace-experimental-just-in-time-compile",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "/nx-api/cypress/migrations/replace-experimental-just-in-time-compile",
|
||||
"type": "migration"
|
||||
},
|
||||
"/nx-api/cypress/migrations/update-component-testing-mount-imports": {
|
||||
"description": "Updates the module specifier for the Component Testing `mount` function.",
|
||||
"file": "generated/packages/cypress/migrations/update-component-testing-mount-imports.json",
|
||||
"hidden": false,
|
||||
"name": "update-component-testing-mount-imports",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "/nx-api/cypress/migrations/update-component-testing-mount-imports",
|
||||
"type": "migration"
|
||||
},
|
||||
"/nx-api/cypress/migrations/20.8.0-package-updates": {
|
||||
"description": "",
|
||||
"file": "generated/packages/cypress/migrations/20.8.0-package-updates.json",
|
||||
"hidden": false,
|
||||
"name": "20.8.0-package-updates",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "/nx-api/cypress/migrations/20.8.0-package-updates",
|
||||
"type": "migration"
|
||||
},
|
||||
"/nx-api/cypress/migrations/update-19-6-0-update-ci-webserver-for-vite": {
|
||||
"description": "Update ciWebServerCommand to use static serve for the application.",
|
||||
"file": "generated/packages/cypress/migrations/update-19-6-0-update-ci-webserver-for-vite.json",
|
||||
|
||||
@ -1364,6 +1364,56 @@
|
||||
}
|
||||
],
|
||||
"migrations": [
|
||||
{
|
||||
"description": "Replaces the `experimentalSkipDomainInjection` configuration option with the new `injectDocumentDomain` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/set-inject-document-domain.json",
|
||||
"hidden": false,
|
||||
"name": "set-inject-document-domain",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "cypress/migrations/set-inject-document-domain",
|
||||
"type": "migration"
|
||||
},
|
||||
{
|
||||
"description": "Removes the `experimentalFetchPolyfill` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/remove-experimental-fetch-polyfill.json",
|
||||
"hidden": false,
|
||||
"name": "remove-experimental-fetch-polyfill",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "cypress/migrations/remove-experimental-fetch-polyfill",
|
||||
"type": "migration"
|
||||
},
|
||||
{
|
||||
"description": "Replaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option.",
|
||||
"file": "generated/packages/cypress/migrations/replace-experimental-just-in-time-compile.json",
|
||||
"hidden": false,
|
||||
"name": "replace-experimental-just-in-time-compile",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "cypress/migrations/replace-experimental-just-in-time-compile",
|
||||
"type": "migration"
|
||||
},
|
||||
{
|
||||
"description": "Updates the module specifier for the Component Testing `mount` function.",
|
||||
"file": "generated/packages/cypress/migrations/update-component-testing-mount-imports.json",
|
||||
"hidden": false,
|
||||
"name": "update-component-testing-mount-imports",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "cypress/migrations/update-component-testing-mount-imports",
|
||||
"type": "migration"
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"file": "generated/packages/cypress/migrations/20.8.0-package-updates.json",
|
||||
"hidden": false,
|
||||
"name": "20.8.0-package-updates",
|
||||
"version": "20.8.0-beta.0",
|
||||
"originalFilePath": "/packages/cypress",
|
||||
"path": "cypress/migrations/20.8.0-package-updates",
|
||||
"type": "migration"
|
||||
},
|
||||
{
|
||||
"description": "Update ciWebServerCommand to use static serve for the application.",
|
||||
"file": "generated/packages/cypress/migrations/update-19-6-0-update-ci-webserver-for-vite.json",
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "20.8.0-package-updates",
|
||||
"version": "20.8.0-beta.0",
|
||||
"x-prompt": "Do you want to update the Cypress version to v14?",
|
||||
"requires": { "cypress": ">=13.0.0 <14.0.0" },
|
||||
"packages": {
|
||||
"cypress": { "version": "^14.2.1", "alwaysAddToPackageJson": false },
|
||||
"@cypress/vite-dev-server": {
|
||||
"version": "^6.0.3",
|
||||
"alwaysAddToPackageJson": false
|
||||
},
|
||||
"@cypress/webpack-dev-server": {
|
||||
"version": "^4.0.2",
|
||||
"alwaysAddToPackageJson": false
|
||||
}
|
||||
},
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"hidden": false,
|
||||
"implementation": "",
|
||||
"path": "/packages/cypress",
|
||||
"schema": null,
|
||||
"type": "migration"
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "remove-experimental-fetch-polyfill",
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": { "cypress": ">=14.0.0" },
|
||||
"description": "Removes the `experimentalFetchPolyfill` configuration option.",
|
||||
"implementation": "/packages/cypress/src/migrations/update-20-8-0/remove-experimental-fetch-polyfill.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/cypress",
|
||||
"schema": null,
|
||||
"type": "migration",
|
||||
"examplesFile": "#### Remove `experimentalFetchPolyfill` Configuration Option\n\nRemoves the `experimentalFetchPolyfill` configuration option that was removed in Cypress v14. Read more at the [migration notes](<https://docs.cypress.io/app/references/changelog#:~:text=The%20experimentalFetchPolyfill%20configuration%20option%20was,cy.intercept()%20for%20handling%20fetch%20requests>).\n\n#### Examples\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1-e2e/cypress.config.ts\" %}\nimport { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n e2e: {\n ...nxE2EPreset(__filename, {\n cypressDir: 'src',\n bundler: 'vite',\n webServerCommands: {\n default: 'pnpm exec nx run app1:dev',\n production: 'pnpm exec nx run app1:dev',\n },\n ciWebServerCommand: 'pnpm exec nx run app1:dev',\n ciBaseUrl: 'http://localhost:4200',\n }),\n baseUrl: 'http://localhost:4200',\n experimentalFetchPolyfill: true,\n },\n});\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1-e2e/cypress.config.ts\" %}\nimport { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n e2e: {\n ...nxE2EPreset(__filename, {\n cypressDir: 'src',\n bundler: 'vite',\n webServerCommands: {\n default: 'pnpm exec nx run app1:dev',\n production: 'pnpm exec nx run app1:dev',\n },\n ciWebServerCommand: 'pnpm exec nx run app1:dev',\n ciBaseUrl: 'http://localhost:4200',\n }),\n baseUrl: 'http://localhost:4200',\n },\n});\n```\n\n{% /tab %}\n\n{% /tabs %}\n"
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "replace-experimental-just-in-time-compile",
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": { "cypress": ">=14.0.0" },
|
||||
"description": "Replaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option.",
|
||||
"implementation": "/packages/cypress/src/migrations/update-20-8-0/replace-experimental-just-in-time-compile.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/cypress",
|
||||
"schema": null,
|
||||
"type": "migration",
|
||||
"examplesFile": "#### Replace the `experimentalJustInTimeCompile` Configuration Option with `justInTimeCompile`\n\nReplaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option. Read more at the [migration notes](https://docs.cypress.io/app/references/migration-guide#CT-Just-in-Time-Compile-changes).\n\n#### Examples\n\nIf the `experimentalJustInTimeCompile` configuration option is present and set to `true`, the migration will remove it. This is to account for the fact that JIT compilation is the default behavior in Cypress v14.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'angular',\n bundler: 'webpack',\n },\n experimentalJustInTimeCompile: true,\n },\n});\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'angular',\n bundler: 'webpack',\n },\n },\n});\n```\n\n{% /tab %}\n{% /tabs %}\n\nIf the `experimentalJustInTimeCompile` configuration option is set to `false` and it is using webpack, the migration will rename it to `justInTimeCompile`.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'angular',\n bundler: 'webpack',\n },\n experimentalJustInTimeCompile: false,\n },\n});\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" highlightLines=[9] %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'angular',\n bundler: 'webpack',\n },\n justInTimeCompile: false,\n },\n});\n```\n\n{% /tab %}\n{% /tabs %}\n\nIf the `experimentalJustInTimeCompile` configuration is set to any value and it is using Vite, the migration will remove it. This is to account for the fact that JIT compilation no longer applies to Vite.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'react',\n bundler: 'vite',\n },\n experimentalJustInTimeCompile: false,\n },\n});\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1/cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n component: {\n devServer: {\n framework: 'react',\n bundler: 'vite',\n },\n },\n});\n```\n\n{% /tab %}\n{% /tabs %}\n"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "update-component-testing-mount-imports",
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": { "cypress": ">=14.0.0" },
|
||||
"description": "Updates the module specifier for the Component Testing `mount` function.",
|
||||
"implementation": "/packages/cypress/src/migrations/update-20-8-0/update-component-testing-mount-imports.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/cypress",
|
||||
"schema": null,
|
||||
"type": "migration",
|
||||
"examplesFile": "#### Update Component Testing `mount` Imports\n\nUpdates the relevant module specifiers when importing the `mount` function and using the Angular or React frameworks. Read more at the [Angular migration notes](https://docs.cypress.io/app/references/migration-guide#Angular-1720-CT-no-longer-supported) and the [React migration notes](https://docs.cypress.io/app/references/migration-guide#React-18-CT-no-longer-supported).\n\n#### Examples\n\nIf using the Angular framework with a version greater than or equal to v17.2.0 and importing the `mount` function from the `cypress/angular-signals` module, the migration will update the import to use the `cypress/angular` module.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" %}\nimport { mount } from 'cypress/angular-signals';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" highlightLines=[1] %}\nimport { mount } from 'cypress/angular';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n{% /tabs %}\n\nIf using the Angular framework with a version lower than v17.2.0 and importing the `mount` function from the `cypress/angular` module, the migration will install the `@cypress/angular@2` package and update the import to use the `@cypress/angular` module.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"package.json\" %}\n{\n \"name\": \"@my-repo/source\",\n \"dependencies\": {\n ...\n \"cypress\": \"^14.2.1\"\n }\n}\n```\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" %}\nimport { mount } from 'cypress/angular';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```json {% fileName=\"package.json\" highlightLines=[6] %}\n{\n \"name\": \"@my-repo/source\",\n \"dependencies\": {\n ...\n \"cypress\": \"^14.2.1\",\n \"@cypress/angular\": \"^2.1.0\"\n }\n}\n```\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" highlightLines=[1] %}\nimport { mount } from '@cypress/angular';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n{% /tabs %}\n\nIf using the React framework and importing the `mount` function from the `cypress/react18` module, the migration will update the import to use the `cypress/react` module.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" %}\nimport { mount } from 'cypress/react18';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```ts {% fileName=\"apps/app1/cypress/support/component.ts\" highlightLines=[1] %}\nimport { mount } from 'cypress/react';\nimport './commands';\n\ndeclare global {\n namespace Cypress {\n interface Chainable<Subject> {\n mount: typeof mount;\n }\n }\n}\n\nCypress.Commands.add('mount', mount);\n```\n\n{% /tab %}\n{% /tabs %}\n"
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { Tree, updateProjectConfiguration, writeJson } from '@nx/devkit';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import * as devkit from '@nx/devkit';
|
||||
import {
|
||||
NxJsonConfiguration,
|
||||
@ -7,7 +6,9 @@ import {
|
||||
readJson,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '@nx/eslint';
|
||||
@ -25,7 +26,10 @@ import { generateTestApplication } from '../utils/testing';
|
||||
import type { Schema } from './schema';
|
||||
|
||||
// need to mock cypress otherwise it'll use installed version in this repo's package.json
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
jest.mock('enquirer');
|
||||
jest.mock('@nx/devkit', () => {
|
||||
const original = jest.requireActual('@nx/devkit');
|
||||
@ -42,8 +46,8 @@ jest.mock('@nx/devkit', () => {
|
||||
describe('app', () => {
|
||||
let appTree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(null);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '@nx/eslint';
|
||||
@ -10,7 +10,7 @@ import { generateTestLibrary } from '../utils/testing';
|
||||
import { componentTestGenerator } from './component-test';
|
||||
import { EOL } from 'node:os';
|
||||
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions');
|
||||
|
||||
describe('Angular Cypress Component Test Generator', () => {
|
||||
let tree: Tree;
|
||||
|
||||
@ -19,8 +19,8 @@ export async function componentTestGenerator(
|
||||
) {
|
||||
ensurePackage('@nx/cypress', nxVersion);
|
||||
const { assertMinimumCypressVersion } = <
|
||||
typeof import('@nx/cypress/src/utils/cypress-version')
|
||||
>require('@nx/cypress/src/utils/cypress-version');
|
||||
typeof import('@nx/cypress/src/utils/versions')
|
||||
>require('@nx/cypress/src/utils/versions');
|
||||
assertMinimumCypressVersion(10);
|
||||
const { root } = readProjectConfiguration(tree, options.project);
|
||||
const componentDirPath = joinPathFragments(root, options.componentDir);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
DependencyType,
|
||||
joinPathFragments,
|
||||
@ -23,7 +23,10 @@ import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-
|
||||
import { generateTestApplication, generateTestLibrary } from '../utils/testing';
|
||||
import { cypressComponentConfiguration } from './cypress-component-configuration';
|
||||
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
// nested code imports graph from the repo, which might have innacurate graph version
|
||||
jest.mock('nx/src/project-graph/project-graph', () => ({
|
||||
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
|
||||
@ -33,8 +36,8 @@ jest.mock('nx/src/project-graph/project-graph', () => ({
|
||||
describe('Cypress Component Testing Configuration', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
// TODO(@leosvelperez): Turn this to adding the plugin
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
// mock so we can test multiple versions
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual<any>('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
// mock bc the nxE2EPreset uses fs for path normalization
|
||||
jest.mock('fs', () => {
|
||||
return {
|
||||
@ -12,7 +15,7 @@ jest.mock('fs', () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { formatFiles, ProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import {
|
||||
joinPathFragments,
|
||||
@ -30,9 +33,10 @@ const mockedLogger = { warn: jest.fn() };
|
||||
describe('e2e migrator', () => {
|
||||
let tree: Tree;
|
||||
let migrator: E2eMigrator;
|
||||
let mockedInstalledCypressVersion = installedCypressVersion as jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
>;
|
||||
let mockedInstalledCypressVersion =
|
||||
getInstalledCypressMajorVersion as jest.Mock<
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
>;
|
||||
|
||||
function addProject(
|
||||
name: string,
|
||||
|
||||
@ -262,9 +262,9 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
|
||||
} else if (this.isCypressE2eProject()) {
|
||||
ensurePackage('@nx/cypress', nxVersion);
|
||||
const {
|
||||
installedCypressVersion,
|
||||
} = require('@nx/cypress/src/utils/cypress-version');
|
||||
this.cypressInstalledVersion = installedCypressVersion();
|
||||
getInstalledCypressMajorVersion,
|
||||
} = require('@nx/cypress/src/utils/versions');
|
||||
this.cypressInstalledVersion = getInstalledCypressMajorVersion(this.tree);
|
||||
this.project = {
|
||||
...this.project,
|
||||
name,
|
||||
|
||||
@ -11,6 +11,42 @@
|
||||
"version": "19.6.0-beta.4",
|
||||
"description": "Update ciWebServerCommand to use static serve for the application.",
|
||||
"implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-static-serve"
|
||||
},
|
||||
"set-inject-document-domain": {
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": {
|
||||
"cypress": ">=14.0.0"
|
||||
},
|
||||
"description": "Replaces the `experimentalSkipDomainInjection` configuration option with the new `injectDocumentDomain` configuration option.",
|
||||
"implementation": "./src/migrations/update-20-8-0/set-inject-document-domain"
|
||||
},
|
||||
"remove-experimental-fetch-polyfill": {
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": {
|
||||
"cypress": ">=14.0.0"
|
||||
},
|
||||
"description": "Removes the `experimentalFetchPolyfill` configuration option.",
|
||||
"implementation": "./src/migrations/update-20-8-0/remove-experimental-fetch-polyfill"
|
||||
},
|
||||
"replace-experimental-just-in-time-compile": {
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": {
|
||||
"cypress": ">=14.0.0"
|
||||
},
|
||||
"description": "Replaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option.",
|
||||
"implementation": "./src/migrations/update-20-8-0/replace-experimental-just-in-time-compile"
|
||||
},
|
||||
"update-component-testing-mount-imports": {
|
||||
"cli": "nx",
|
||||
"version": "20.8.0-beta.0",
|
||||
"requires": {
|
||||
"cypress": ">=14.0.0"
|
||||
},
|
||||
"description": "Updates the module specifier for the Component Testing `mount` function.",
|
||||
"implementation": "./src/migrations/update-20-8-0/update-component-testing-mount-imports"
|
||||
}
|
||||
},
|
||||
"packageJsonUpdates": {
|
||||
@ -59,6 +95,27 @@
|
||||
"alwaysAddToPackageJson": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"20.8.0": {
|
||||
"version": "20.8.0-beta.0",
|
||||
"x-prompt": "Do you want to update the Cypress version to v14?",
|
||||
"requires": {
|
||||
"cypress": ">=13.0.0 <14.0.0"
|
||||
},
|
||||
"packages": {
|
||||
"cypress": {
|
||||
"version": "^14.2.1",
|
||||
"alwaysAddToPackageJson": false
|
||||
},
|
||||
"@cypress/vite-dev-server": {
|
||||
"version": "^6.0.3",
|
||||
"alwaysAddToPackageJson": false
|
||||
},
|
||||
"@cypress/webpack-dev-server": {
|
||||
"version": "^4.0.2",
|
||||
"alwaysAddToPackageJson": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,10 +41,11 @@
|
||||
"@nx/js": "file:../js",
|
||||
"@phenomnomnominal/tsquery": "~5.0.1",
|
||||
"detect-port": "^1.5.1",
|
||||
"semver": "^7.6.3",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cypress": ">= 3 < 14"
|
||||
"cypress": ">= 3 < 15"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"cypress": {
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { getTempTailwindPath } from '../../utils/ct-helpers';
|
||||
import { ExecutorContext, stripIndents } from '@nx/devkit';
|
||||
import * as detectPort from 'detect-port';
|
||||
import * as executorUtils from 'nx/src/command-line/run/executor-utils';
|
||||
import * as path from 'path';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { getTempTailwindPath } from '../../utils/ct-helpers';
|
||||
import { getInstalledCypressMajorVersion } from '../../utils/versions';
|
||||
import cypressExecutor, { CypressExecutorOptions } from './cypress.impl';
|
||||
|
||||
jest.mock('@nx/devkit');
|
||||
let devkit = require('@nx/devkit');
|
||||
jest.mock('detect-port', () => jest.fn().mockResolvedValue(4200));
|
||||
import * as detectPort from 'detect-port';
|
||||
jest.mock('../../utils/cypress-version');
|
||||
jest.mock('../../utils/versions', () => ({
|
||||
...jest.requireActual('../../utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../utils/ct-helpers');
|
||||
const Cypress = require('cypress');
|
||||
|
||||
@ -29,8 +32,8 @@ describe('Cypress builder', () => {
|
||||
};
|
||||
let mockContext: ExecutorContext;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as any;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as any;
|
||||
mockContext = {
|
||||
root: '/root',
|
||||
workspace: { projects: {} },
|
||||
|
||||
@ -2,7 +2,7 @@ import { ExecutorContext, logger, stripIndents } from '@nx/devkit';
|
||||
import { existsSync, readdirSync, unlinkSync } from 'fs';
|
||||
import { basename, dirname } from 'path';
|
||||
import { getTempTailwindPath } from '../../utils/ct-helpers';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '../../utils/versions';
|
||||
import { startDevServer } from '../../utils/start-dev-server';
|
||||
|
||||
const Cypress = require('cypress'); // @NOTE: Importing via ES6 messes the whole test dependencies.
|
||||
@ -53,7 +53,8 @@ export default async function cypressExecutor(
|
||||
options: CypressExecutorOptions,
|
||||
context: ExecutorContext
|
||||
) {
|
||||
options = normalizeOptions(options, context);
|
||||
const installedCypressMajorVersion = getInstalledCypressMajorVersion();
|
||||
options = normalizeOptions(options, context, installedCypressMajorVersion);
|
||||
// this is used by cypress component testing presets to build the executor contexts with the correct configuration options.
|
||||
process.env.NX_CYPRESS_TARGET_CONFIGURATION = context.configurationName;
|
||||
let success;
|
||||
@ -61,10 +62,14 @@ export default async function cypressExecutor(
|
||||
const generatorInstance = startDevServer(options, context);
|
||||
for await (const devServerValues of generatorInstance) {
|
||||
try {
|
||||
success = await runCypress(devServerValues.baseUrl, {
|
||||
...options,
|
||||
portLockFilePath: devServerValues.portLockFilePath,
|
||||
});
|
||||
success = await runCypress(
|
||||
devServerValues.baseUrl,
|
||||
{
|
||||
...options,
|
||||
portLockFilePath: devServerValues.portLockFilePath,
|
||||
},
|
||||
installedCypressMajorVersion
|
||||
);
|
||||
if (!options.watch) {
|
||||
generatorInstance.return();
|
||||
break;
|
||||
@ -81,7 +86,8 @@ export default async function cypressExecutor(
|
||||
|
||||
function normalizeOptions(
|
||||
options: CypressExecutorOptions,
|
||||
context: ExecutorContext
|
||||
context: ExecutorContext,
|
||||
installedCypressMajorVersion: number | null
|
||||
): NormalizedCypressExecutorOptions {
|
||||
options.env = options.env || {};
|
||||
if (options.testingType === 'component') {
|
||||
@ -90,19 +96,22 @@ function normalizeOptions(
|
||||
options.ctTailwindPath = getTempTailwindPath(context);
|
||||
}
|
||||
}
|
||||
checkSupportedBrowser(options);
|
||||
warnDeprecatedHeadless(options);
|
||||
warnDeprecatedCypressVersion();
|
||||
checkSupportedBrowser(options, installedCypressMajorVersion);
|
||||
warnDeprecatedHeadless(options, installedCypressMajorVersion);
|
||||
warnDeprecatedCypressVersion(installedCypressMajorVersion);
|
||||
return options;
|
||||
}
|
||||
|
||||
function checkSupportedBrowser({ browser }: CypressExecutorOptions) {
|
||||
function checkSupportedBrowser(
|
||||
{ browser }: CypressExecutorOptions,
|
||||
installedCypressMajorVersion: number | null
|
||||
) {
|
||||
// Browser was not passed in as an option, cypress will use whatever default it has set and we dont need to check it
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (installedCypressVersion() >= 4 && browser == 'canary') {
|
||||
if (installedCypressMajorVersion >= 4 && browser == 'canary') {
|
||||
logger.warn(stripIndents`
|
||||
Warning:
|
||||
You are using a browser that is not supported by cypress v4+.
|
||||
@ -115,7 +124,7 @@ function checkSupportedBrowser({ browser }: CypressExecutorOptions) {
|
||||
|
||||
const supportedV3Browsers = ['electron', 'chrome', 'canary', 'chromium'];
|
||||
if (
|
||||
installedCypressVersion() <= 3 &&
|
||||
installedCypressMajorVersion <= 3 &&
|
||||
!supportedV3Browsers.includes(browser)
|
||||
) {
|
||||
logger.warn(stripIndents`
|
||||
@ -126,8 +135,11 @@ function checkSupportedBrowser({ browser }: CypressExecutorOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function warnDeprecatedHeadless({ headless }: CypressExecutorOptions) {
|
||||
if (installedCypressVersion() < 8 || headless === undefined) {
|
||||
function warnDeprecatedHeadless(
|
||||
{ headless }: CypressExecutorOptions,
|
||||
installedCypressMajorVersion: number | null
|
||||
) {
|
||||
if (installedCypressMajorVersion < 8 || headless === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -140,8 +152,10 @@ function warnDeprecatedHeadless({ headless }: CypressExecutorOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function warnDeprecatedCypressVersion() {
|
||||
if (installedCypressVersion() < 10) {
|
||||
function warnDeprecatedCypressVersion(
|
||||
installedCypressMajorVersion: number | null
|
||||
) {
|
||||
if (installedCypressMajorVersion < 10) {
|
||||
logger.warn(stripIndents`
|
||||
NOTE:
|
||||
Support for Cypress versions < 10 is deprecated. Please upgrade to at least Cypress version 10.
|
||||
@ -157,9 +171,9 @@ A generator to migrate from v8 to v10 is provided. See https://nx.dev/cypress/v1
|
||||
*/
|
||||
async function runCypress(
|
||||
baseUrl: string,
|
||||
opts: NormalizedCypressExecutorOptions
|
||||
opts: NormalizedCypressExecutorOptions,
|
||||
installedCypressMajorVersion: number | null
|
||||
) {
|
||||
const cypressVersion = installedCypressVersion();
|
||||
// Cypress expects the folder where a cypress config is present
|
||||
const projectFolderPath = dirname(opts.cypressConfig);
|
||||
const options: any = {
|
||||
@ -200,7 +214,7 @@ async function runCypress(
|
||||
options.ciBuildId = opts.ciBuildId?.toString();
|
||||
options.group = opts.group;
|
||||
// renamed in cy 10
|
||||
if (cypressVersion >= 10) {
|
||||
if (installedCypressMajorVersion >= 10) {
|
||||
options.config ??= {};
|
||||
options.config[opts.testingType] = {
|
||||
excludeSpecPattern: opts.ignoreTestFiles,
|
||||
|
||||
@ -12,11 +12,15 @@ import {
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '../../utils/versions';
|
||||
import { componentConfigurationGenerator } from './component-configuration';
|
||||
import { cypressInitGenerator } from '../init/init';
|
||||
|
||||
jest.mock('../../utils/cypress-version');
|
||||
jest.mock('../../utils/versions', () => ({
|
||||
...jest.requireActual('../../utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
let projectConfig: ProjectConfiguration = {
|
||||
projectType: 'library',
|
||||
sourceRoot: 'libs/cool-lib/src',
|
||||
@ -39,8 +43,8 @@ let projectConfig: ProjectConfiguration = {
|
||||
describe('Cypress Component Configuration', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
|
||||
@ -2,31 +2,27 @@ import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
ProjectConfiguration,
|
||||
readJson,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
updateNxJson,
|
||||
runTasksInSerial,
|
||||
GeneratorCallback,
|
||||
readJson,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
|
||||
import {
|
||||
cypressVersion,
|
||||
cypressViteDevServerVersion,
|
||||
cypressWebpackVersion,
|
||||
htmlWebpackPluginVersion,
|
||||
getInstalledCypressMajorVersion,
|
||||
versions,
|
||||
} from '../../utils/versions';
|
||||
import { CypressComponentConfigurationSchema } from './schema';
|
||||
import { addBaseCypressSetup } from '../base-setup/base-setup';
|
||||
import init from '../init/init';
|
||||
import { CypressComponentConfigurationSchema } from './schema';
|
||||
|
||||
type NormalizeCTOptions = ReturnType<typeof normalizeOptions>;
|
||||
|
||||
@ -49,12 +45,14 @@ export async function componentConfigurationGeneratorInternal(
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const opts = normalizeOptions(tree, options);
|
||||
|
||||
tasks.push(
|
||||
await init(tree, {
|
||||
...opts,
|
||||
skipFormat: true,
|
||||
})
|
||||
);
|
||||
if (!getInstalledCypressMajorVersion(tree)) {
|
||||
tasks.push(
|
||||
await init(tree, {
|
||||
...opts,
|
||||
skipFormat: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const nxJson = readNxJson(tree);
|
||||
const hasPlugin = nxJson.plugins?.some((p) =>
|
||||
@ -86,7 +84,7 @@ function normalizeOptions(
|
||||
tree: Tree,
|
||||
options: CypressComponentConfigurationSchema
|
||||
) {
|
||||
const cyVersion = installedCypressVersion();
|
||||
const cyVersion = getInstalledCypressMajorVersion(tree);
|
||||
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/v11-migration-guide'
|
||||
@ -107,18 +105,21 @@ function normalizeOptions(
|
||||
}
|
||||
|
||||
function updateDeps(tree: Tree, opts: NormalizeCTOptions) {
|
||||
const pkgVersions = versions(tree);
|
||||
|
||||
const devDeps = {
|
||||
cypress: cypressVersion,
|
||||
cypress: pkgVersions.cypressVersion,
|
||||
};
|
||||
|
||||
if (opts.bundler === 'vite') {
|
||||
devDeps['@cypress/vite-dev-server'] = cypressViteDevServerVersion;
|
||||
devDeps['@cypress/vite-dev-server'] =
|
||||
pkgVersions.cypressViteDevServerVersion;
|
||||
} else {
|
||||
devDeps['@cypress/webpack-dev-server'] = cypressWebpackVersion;
|
||||
devDeps['html-webpack-plugin'] = htmlWebpackPluginVersion;
|
||||
devDeps['@cypress/webpack-dev-server'] = pkgVersions.cypressWebpackVersion;
|
||||
devDeps['html-webpack-plugin'] = pkgVersions.htmlWebpackPluginVersion;
|
||||
}
|
||||
|
||||
return addDependenciesToPackageJson(tree, {}, devDeps);
|
||||
return addDependenciesToPackageJson(tree, {}, devDeps, undefined, true);
|
||||
}
|
||||
|
||||
function addProjectFiles(
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Cypress e2e configuration v10+ should not override eslint settings if preset 1`] = `
|
||||
{
|
||||
"extends": [
|
||||
"plugin:cypress/recommended",
|
||||
"../../.eslintrc.json",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*",
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
],
|
||||
"files": [
|
||||
"*.ts",
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"prefix": "cy-port-test",
|
||||
"style": "kebab-case",
|
||||
"type": "element",
|
||||
},
|
||||
],
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"prefix": "cyPortTest",
|
||||
"style": "camelCase",
|
||||
"type": "attribute",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@nx/angular-template",
|
||||
],
|
||||
"files": [
|
||||
"*.html",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.cy.{ts,js,tsx,jsx}",
|
||||
"cypress/**/*.{ts,js,tsx,jsx}",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -12,51 +12,35 @@ import {
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import cypressE2EConfigurationGenerator from './configuration';
|
||||
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { cypressInitGenerator } from '../init/init';
|
||||
|
||||
jest.mock('../../utils/cypress-version');
|
||||
|
||||
describe('Cypress e2e configuration', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
tree.write('.eslintrc.json', '{}'); // we are explicitly checking for existance of config type
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('v10+', () => {
|
||||
beforeAll(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
it('should add web server commands to the cypress config when the @nx/cypress/plugin is present', async () => {
|
||||
await cypressInitGenerator(tree, {
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
it('should add web server commands to the cypress config when the @nx/cypress/plugin is present', async () => {
|
||||
await cypressInitGenerator(tree, {
|
||||
addPlugin: true,
|
||||
});
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
webServerCommands: {
|
||||
default: 'nx run my-app:serve',
|
||||
production: 'nx run my-app:serve:production',
|
||||
},
|
||||
ciWebServerCommand: 'nx run my-app:serve-static',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
webServerCommands: {
|
||||
default: 'nx run my-app:serve',
|
||||
production: 'nx run my-app:serve:production',
|
||||
},
|
||||
ciWebServerCommand: 'nx run my-app:serve-static',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
@ -75,12 +59,11 @@ describe('Cypress e2e configuration', () => {
|
||||
});
|
||||
"
|
||||
`);
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-app').targets.e2e
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-app').targets.e2e
|
||||
).toBeUndefined();
|
||||
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
@ -103,18 +86,19 @@ describe('Cypress e2e configuration', () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
});
|
||||
|
||||
it('should add e2e target to existing app when not using plugin', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
addPlugin: false,
|
||||
});
|
||||
|
||||
it('should add e2e target to existing app', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
@ -122,22 +106,31 @@ describe('Cypress e2e configuration', () => {
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
webServerCommands: {
|
||||
default: 'nx run my-app:serve',
|
||||
production: 'nx run my-app:serve:production',
|
||||
},
|
||||
ciWebServerCommand: 'nx run my-app:serve-static',
|
||||
}),
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-app').targets.e2e
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(readProjectConfiguration(tree, 'my-app').targets.e2e)
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"devServerTarget": "my-app:serve-static",
|
||||
},
|
||||
"production": {
|
||||
"devServerTarget": "my-app:serve:production",
|
||||
},
|
||||
},
|
||||
"executor": "@nx/cypress:cypress",
|
||||
"options": {
|
||||
"cypressConfig": "apps/my-app/cypress.config.ts",
|
||||
"devServerTarget": "my-app:serve",
|
||||
"testingType": "e2e",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
@ -160,20 +153,20 @@ describe('Cypress e2e configuration', () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
});
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
});
|
||||
|
||||
it('should add e2e target to existing lib', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'cypress',
|
||||
devServerTarget: 'my-app:serve',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
it('should add e2e target to existing lib', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'cypress',
|
||||
devServerTarget: 'my-app:serve',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
@ -191,19 +184,19 @@ describe('Cypress e2e configuration', () => {
|
||||
});
|
||||
"
|
||||
`);
|
||||
assertCypressFiles(tree, 'libs/my-lib/cypress');
|
||||
});
|
||||
assertCypressFiles(tree, 'libs/my-lib/cypress');
|
||||
});
|
||||
|
||||
it('should use --baseUrl', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
it('should use --baseUrl', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'apps/my-app/src');
|
||||
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
@ -222,73 +215,73 @@ describe('Cypress e2e configuration', () => {
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not overwrite existing e2e target', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
const pc = readProjectConfiguration(tree, 'my-app');
|
||||
pc.targets.e2e = {};
|
||||
updateProjectConfiguration(tree, 'my-app', pc);
|
||||
await expect(async () => {
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
addPlugin: true,
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
it('should not overwrite existing e2e target', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
const pc = readProjectConfiguration(tree, 'my-app');
|
||||
pc.targets.e2e = {};
|
||||
updateProjectConfiguration(tree, 'my-app', pc);
|
||||
await expect(async () => {
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
addPlugin: true,
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Project my-app already has an e2e target.
|
||||
Rename or remove the existing e2e target."
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should customize directory name', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
tree.write(
|
||||
'apps/my-app/tsconfig.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
compilerOptions: {
|
||||
target: 'es2022',
|
||||
useDefineForClassFields: false,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
strict: true,
|
||||
noImplicitOverride: true,
|
||||
noPropertyAccessFromIndexSignature: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
files: [],
|
||||
include: [],
|
||||
references: [
|
||||
{
|
||||
path: './tsconfig.app.json',
|
||||
},
|
||||
{
|
||||
path: './tsconfig.spec.json',
|
||||
},
|
||||
{
|
||||
path: './tsconfig.editor.json',
|
||||
},
|
||||
],
|
||||
extends: '../../tsconfig.base.json',
|
||||
angularCompilerOptions: {
|
||||
enableI18nLegacyMessageIdFormat: false,
|
||||
strictInjectionParameters: true,
|
||||
strictInputAccessModifiers: true,
|
||||
strictTemplates: true,
|
||||
},
|
||||
it('should customize directory name', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
tree.write(
|
||||
'apps/my-app/tsconfig.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
compilerOptions: {
|
||||
target: 'es2022',
|
||||
useDefineForClassFields: false,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
strict: true,
|
||||
noImplicitOverride: true,
|
||||
noPropertyAccessFromIndexSignature: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
directory: 'e2e/something',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'apps/my-app/e2e/something');
|
||||
expect(readJson(tree, 'apps/my-app/e2e/something/tsconfig.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
files: [],
|
||||
include: [],
|
||||
references: [
|
||||
{
|
||||
path: './tsconfig.app.json',
|
||||
},
|
||||
{
|
||||
path: './tsconfig.spec.json',
|
||||
},
|
||||
{
|
||||
path: './tsconfig.editor.json',
|
||||
},
|
||||
],
|
||||
extends: '../../tsconfig.base.json',
|
||||
angularCompilerOptions: {
|
||||
enableI18nLegacyMessageIdFormat: false,
|
||||
strictInjectionParameters: true,
|
||||
strictInputAccessModifiers: true,
|
||||
strictTemplates: true,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
directory: 'e2e/something',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'apps/my-app/e2e/something');
|
||||
expect(readJson(tree, 'apps/my-app/e2e/something/tsconfig.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
@ -311,111 +304,165 @@ describe('Cypress e2e configuration', () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json').references).toEqual(
|
||||
expect.arrayContaining([{ path: './e2e/something/tsconfig.json' }])
|
||||
);
|
||||
});
|
||||
expect(readJson(tree, 'apps/my-app/tsconfig.json').references).toEqual(
|
||||
expect.arrayContaining([{ path: './e2e/something/tsconfig.json' }])
|
||||
);
|
||||
});
|
||||
|
||||
it('should use js instead of ts files with --js', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'src/e2e',
|
||||
js: true,
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'libs/my-lib/src/e2e', 'js');
|
||||
it('should use js instead of ts files with --js', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'src/e2e',
|
||||
js: true,
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
assertCypressFiles(tree, 'libs/my-lib/src/e2e', 'js');
|
||||
});
|
||||
|
||||
it('should not override eslint settings if preset', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
const ngEsLintContents = {
|
||||
extends: ['../../.eslintrc.json'],
|
||||
ignorePatterns: ['!**/*'],
|
||||
overrides: [
|
||||
it('should not override eslint settings if preset', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
const ngEsLintContents = {
|
||||
extends: ['../../.eslintrc.json'],
|
||||
ignorePatterns: ['!**/*'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'cyPortTest',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'cy-port-test',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
extends: [
|
||||
'plugin:@nx/angular',
|
||||
'plugin:@angular-eslint/template/process-inline-templates',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
extends: ['plugin:@nx/angular-template'],
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
tree.write(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
JSON.stringify(ngEsLintContents, null, 2)
|
||||
);
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'cypress',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(readJson(tree, 'libs/my-lib/.eslintrc.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"extends": [
|
||||
"plugin:cypress/recommended",
|
||||
"../../.eslintrc.json",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*",
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
],
|
||||
"files": [
|
||||
"*.ts",
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'cyPortTest',
|
||||
style: 'camelCase',
|
||||
"prefix": "cy-port-test",
|
||||
"style": "kebab-case",
|
||||
"type": "element",
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'cy-port-test',
|
||||
style: 'kebab-case',
|
||||
"prefix": "cyPortTest",
|
||||
"style": "camelCase",
|
||||
"type": "attribute",
|
||||
},
|
||||
],
|
||||
},
|
||||
extends: [
|
||||
'plugin:@nx/angular',
|
||||
'plugin:@angular-eslint/template/process-inline-templates',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
extends: ['plugin:@nx/angular-template'],
|
||||
rules: {},
|
||||
"extends": [
|
||||
"plugin:@nx/angular-template",
|
||||
],
|
||||
"files": [
|
||||
"*.html",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.cy.{ts,js,tsx,jsx}",
|
||||
"cypress/**/*.{ts,js,tsx,jsx}",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
],
|
||||
};
|
||||
tree.write(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
JSON.stringify(ngEsLintContents, null, 2)
|
||||
);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
directory: 'cypress',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(readJson(tree, 'libs/my-lib/.eslintrc.json')).toMatchSnapshot();
|
||||
it('should add serve-static target to CI configuration', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
const pc = readProjectConfiguration(tree, 'my-app');
|
||||
pc.targets['serve-static'] = {
|
||||
executor: 'some-file-server',
|
||||
};
|
||||
|
||||
updateProjectConfiguration(tree, 'my-lib', pc);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
devServerTarget: 'my-app:serve',
|
||||
directory: 'cypress',
|
||||
addPlugin: false,
|
||||
});
|
||||
|
||||
it('should add serve-static target to CI configuration', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
const pc = readProjectConfiguration(tree, 'my-app');
|
||||
pc.targets['serve-static'] = {
|
||||
executor: 'some-file-server',
|
||||
};
|
||||
|
||||
updateProjectConfiguration(tree, 'my-lib', pc);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
devServerTarget: 'my-app:serve',
|
||||
directory: 'cypress',
|
||||
addPlugin: false,
|
||||
});
|
||||
assertCypressFiles(tree, 'libs/my-lib/cypress');
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-lib').targets['e2e'].configurations
|
||||
.ci
|
||||
).toMatchInlineSnapshot(`
|
||||
assertCypressFiles(tree, 'libs/my-lib/cypress');
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-lib').targets['e2e'].configurations.ci
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"devServerTarget": "my-app:serve-static",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set --port', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
port: 0,
|
||||
addPlugin: false,
|
||||
});
|
||||
|
||||
it('should set --port', async () => {
|
||||
addProject(tree, { name: 'my-app', type: 'apps' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-app',
|
||||
port: 0,
|
||||
addPlugin: false,
|
||||
});
|
||||
|
||||
expect(readProjectConfiguration(tree, 'my-app').targets['e2e'].options)
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(readProjectConfiguration(tree, 'my-app').targets['e2e'].options)
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"cypressConfig": "apps/my-app/cypress.config.ts",
|
||||
"devServerTarget": "my-app:serve",
|
||||
@ -423,29 +470,29 @@ describe('Cypress e2e configuration', () => {
|
||||
"testingType": "e2e",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add e2e to an existing config', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
it('should add e2e to an existing config', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
|
||||
tree.write(
|
||||
'libs/my-lib/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
tree.write(
|
||||
'libs/my-lib/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
`
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
|
||||
@ -461,35 +508,33 @@ export default defineConfig({
|
||||
});
|
||||
"
|
||||
`);
|
||||
// these files are only added when there isn't already a cypress config
|
||||
expect(
|
||||
tree.exists('libs/my-lib/cypress/fixtures/example.json')
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
tree.exists('libs/my-lib/cypress/support/commands.ts')
|
||||
).toBeFalsy();
|
||||
});
|
||||
// these files are only added when there isn't already a cypress config
|
||||
expect(
|
||||
tree.exists('libs/my-lib/cypress/fixtures/example.json')
|
||||
).toBeFalsy();
|
||||
expect(tree.exists('libs/my-lib/cypress/support/commands.ts')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not throw if e2e is already defined', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
it('should not throw if e2e is already defined', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
|
||||
tree.write(
|
||||
'libs/my-lib/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
tree.write(
|
||||
'libs/my-lib/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {exists: true},
|
||||
});
|
||||
`
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
);
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
@ -497,19 +542,19 @@ export default defineConfig({
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support --js option with CommonJS format', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
it('should support --js option with CommonJS format', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { nxE2EPreset } = require('@nx/cypress/plugins/cypress-preset');
|
||||
const { defineConfig } = require('cypress');
|
||||
|
||||
@ -523,25 +568,25 @@ export default defineConfig({
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support --js option with ESM format', async () => {
|
||||
// When type is "module", Node will treat .js files as ESM format.
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.type = 'module';
|
||||
return json;
|
||||
});
|
||||
|
||||
it('should support --js option with ESM format', async () => {
|
||||
// When type is "module", Node will treat .js files as ESM format.
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.type = 'module';
|
||||
return json;
|
||||
});
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
@ -555,43 +600,43 @@ export default defineConfig({
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
describe('TS Solution Setup', () => {
|
||||
beforeEach(() => {
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.workspaces = ['packages/*', 'apps/*'];
|
||||
return json;
|
||||
});
|
||||
writeJson(tree, 'tsconfig.base.json', {
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
declaration: true,
|
||||
},
|
||||
});
|
||||
writeJson(tree, 'tsconfig.json', {
|
||||
extends: './tsconfig.base.json',
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('TS Solution Setup', () => {
|
||||
beforeEach(() => {
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.workspaces = ['packages/*', 'apps/*'];
|
||||
return json;
|
||||
});
|
||||
writeJson(tree, 'tsconfig.base.json', {
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
declaration: true,
|
||||
},
|
||||
});
|
||||
writeJson(tree, 'tsconfig.json', {
|
||||
extends: './tsconfig.base.json',
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
it('should handle existing tsconfig.json files', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
writeJson(tree, 'libs/my-lib/tsconfig.json', {
|
||||
include: [],
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
|
||||
it('should handle existing tsconfig.json files', async () => {
|
||||
addProject(tree, { name: 'my-lib', type: 'libs' });
|
||||
writeJson(tree, 'libs/my-lib/tsconfig.json', {
|
||||
include: [],
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
await cypressE2EConfigurationGenerator(tree, {
|
||||
project: 'my-lib',
|
||||
baseUrl: 'http://localhost:4200',
|
||||
js: true,
|
||||
});
|
||||
|
||||
expect(tree.read('libs/my-lib/tsconfig.json', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/tsconfig.json', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"{
|
||||
"include": [],
|
||||
"files": [],
|
||||
@ -603,8 +648,8 @@ export default defineConfig({
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(tree.read('libs/my-lib/src/tsconfig.json', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(tree.read('libs/my-lib/src/tsconfig.json', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
@ -625,7 +670,6 @@ export default defineConfig({
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { resolveImportPath } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||
import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt';
|
||||
import { Linter, LinterType } from '@nx/eslint';
|
||||
import {
|
||||
getRelativePathToRootTsConfig,
|
||||
@ -35,11 +36,12 @@ import { PackageJson } from 'nx/src/utils/package-json';
|
||||
import { join } from 'path';
|
||||
import { addLinterToCyProject } from '../../utils/add-linter';
|
||||
import { addDefaultE2EConfig } from '../../utils/config';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { typesNodeVersion, viteVersion } from '../../utils/versions';
|
||||
import {
|
||||
getInstalledCypressMajorVersion,
|
||||
versions,
|
||||
} from '../../utils/versions';
|
||||
import { addBaseCypressSetup } from '../base-setup/base-setup';
|
||||
import cypressInitGenerator, { addPlugin } from '../init/init';
|
||||
import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt';
|
||||
|
||||
export interface CypressE2EConfigSchema {
|
||||
project: string;
|
||||
@ -82,7 +84,7 @@ export async function configurationGeneratorInternal(
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
|
||||
const projectGraph = await createProjectGraphAsync();
|
||||
if (!installedCypressVersion()) {
|
||||
if (!getInstalledCypressMajorVersion(tree)) {
|
||||
tasks.push(await jsInitGenerator(tree, { ...options, skipFormat: true }));
|
||||
tasks.push(
|
||||
await cypressInitGenerator(tree, {
|
||||
@ -174,15 +176,23 @@ export async function configurationGeneratorInternal(
|
||||
}
|
||||
|
||||
function ensureDependencies(tree: Tree, options: NormalizedSchema) {
|
||||
const pkgVersions = versions(tree);
|
||||
|
||||
const devDependencies: Record<string, string> = {
|
||||
'@types/node': typesNodeVersion,
|
||||
'@types/node': pkgVersions.typesNodeVersion,
|
||||
};
|
||||
|
||||
if (options.bundler === 'vite') {
|
||||
devDependencies['vite'] = viteVersion;
|
||||
devDependencies['vite'] = pkgVersions.viteVersion;
|
||||
}
|
||||
|
||||
return addDependenciesToPackageJson(tree, {}, devDependencies);
|
||||
return addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
devDependencies,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) {
|
||||
@ -280,7 +290,7 @@ async function addFiles(
|
||||
hasPlugin: boolean
|
||||
) {
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
const cyVersion = installedCypressVersion();
|
||||
const cyVersion = getInstalledCypressMajorVersion(tree);
|
||||
const filesToUse = cyVersion && cyVersion < 10 ? 'v9' : 'v10';
|
||||
|
||||
const hasTsConfig = tree.exists(
|
||||
@ -400,7 +410,7 @@ function addTarget(
|
||||
projectGraph: ProjectGraph
|
||||
) {
|
||||
const projectConfig = readProjectConfiguration(tree, opts.project);
|
||||
const cyVersion = installedCypressVersion();
|
||||
const cyVersion = getInstalledCypressMajorVersion(tree);
|
||||
projectConfig.targets ??= {};
|
||||
projectConfig.targets.e2e = {
|
||||
executor: '@nx/cypress:cypress',
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '../../utils/versions';
|
||||
import { configurationGenerator } from '../configuration/configuration';
|
||||
import {
|
||||
createSupportFileImport,
|
||||
@ -21,13 +21,16 @@ import {
|
||||
} from './conversion.util';
|
||||
import { migrateCypressProject } from './migrate-to-cypress-11';
|
||||
|
||||
jest.mock('../../utils/cypress-version');
|
||||
jest.mock('../../utils/versions', () => ({
|
||||
...jest.requireActual('../../utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('convertToCypressTen', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
import {
|
||||
assertMinimumCypressVersion,
|
||||
installedCypressVersion,
|
||||
} from '../../utils/cypress-version';
|
||||
import {
|
||||
formatFiles,
|
||||
installPackagesTask,
|
||||
@ -16,6 +12,10 @@ import {
|
||||
} from '@nx/devkit';
|
||||
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
|
||||
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
|
||||
import {
|
||||
assertMinimumCypressVersion,
|
||||
getInstalledCypressMajorVersion,
|
||||
} from '../../utils/versions';
|
||||
import {
|
||||
addConfigToTsConfig,
|
||||
createNewCypressConfig,
|
||||
@ -119,7 +119,7 @@ https://nx.dev/cypress/v10-migration-guide
|
||||
|
||||
export async function migrateCypressProject(tree: Tree) {
|
||||
assertMinimumCypressVersion(8);
|
||||
if (installedCypressVersion() >= 10) {
|
||||
if (getInstalledCypressMajorVersion(tree) >= 10) {
|
||||
logger.info('NX This workspace is already using Cypress v10+');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { readJson, updateJson, type Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import * as cypressVersionUtils from '../../utils/cypress-version';
|
||||
import * as cypressVersions from '../../utils/versions';
|
||||
import migration from './update-cypress-version-13-6-6';
|
||||
|
||||
describe('update-cypress-version migration', () => {
|
||||
@ -14,7 +14,7 @@ describe('update-cypress-version migration', () => {
|
||||
});
|
||||
const major = parseInt(version.split('.')[0].replace('^', ''), 10);
|
||||
jest
|
||||
.spyOn(cypressVersionUtils, 'installedCypressVersion')
|
||||
.spyOn(cypressVersions, 'getInstalledCypressMajorVersion')
|
||||
.mockReturnValue(major);
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ import {
|
||||
formatFiles,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '../../utils/versions';
|
||||
|
||||
export default async function (tree: Tree) {
|
||||
if (installedCypressVersion() < 13) {
|
||||
if (getInstalledCypressMajorVersion(tree) < 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
#### Remove `experimentalFetchPolyfill` Configuration Option
|
||||
|
||||
Removes the `experimentalFetchPolyfill` configuration option that was removed in Cypress v14. Read more at the [migration notes](<https://docs.cypress.io/app/references/changelog#:~:text=The%20experimentalFetchPolyfill%20configuration%20option%20was,cy.intercept()%20for%20handling%20fetch%20requests>).
|
||||
|
||||
#### Examples
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalFetchPolyfill: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
@ -0,0 +1,159 @@
|
||||
import { addProjectConfiguration, type Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import migration from './remove-experimental-fetch-polyfill';
|
||||
|
||||
describe('remove-experimental-fetch-polyfill', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should do nothing when there are no projects with cypress config', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing when the cypress config cannot be parsed as expected', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write('apps/app1-e2e/cypress.config.ts', `export const foo = 'bar';`);
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export const foo = 'bar';
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle when the cypress config path in the executor is not valid', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1-e2e/non-existent-cypress.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should remove the experimentalFetchPolyfill property even if defined multiple times', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
experimentalFetchPolyfill: true,
|
||||
},
|
||||
e2e: {
|
||||
experimentalFetchPolyfill: true,
|
||||
},
|
||||
experimentalFetchPolyfill: true,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
e2e: {},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle cypress config files in projects using the "@nx/cypress:cypress" executor', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1-e2e/cypress.custom-config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.custom-config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalFetchPolyfill: true,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.custom-config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,79 @@
|
||||
import { formatFiles, type Tree } from '@nx/devkit';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import type { Printer } from 'typescript';
|
||||
import { resolveCypressConfigObject } from '../../utils/config';
|
||||
import { cypressProjectConfigs } from '../../utils/migrations';
|
||||
|
||||
let printer: Printer;
|
||||
let ts: typeof import('typescript');
|
||||
|
||||
// https://docs.cypress.io/app/references/changelog#:~:text=The%20experimentalFetchPolyfill%20configuration%20option%20was,cy.intercept()%20for%20handling%20fetch%20requests
|
||||
export default async function (tree: Tree) {
|
||||
for await (const { cypressConfigPath } of cypressProjectConfigs(tree)) {
|
||||
if (!tree.exists(cypressConfigPath)) {
|
||||
// cypress config file doesn't exist, so skip
|
||||
continue;
|
||||
}
|
||||
|
||||
ts ??= ensureTypescript();
|
||||
printer ??= ts.createPrinter();
|
||||
|
||||
const cypressConfig = tree.read(cypressConfigPath, 'utf-8');
|
||||
const updatedConfig = removeExperimentalFetchPolyfill(cypressConfig);
|
||||
|
||||
tree.write(cypressConfigPath, updatedConfig);
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
function removeExperimentalFetchPolyfill(cypressConfig: string): string {
|
||||
const config = resolveCypressConfigObject(cypressConfig);
|
||||
|
||||
if (!config) {
|
||||
// couldn't find the config object, leave as is
|
||||
return cypressConfig;
|
||||
}
|
||||
|
||||
const sourceFile = tsquery.ast(cypressConfig);
|
||||
|
||||
const updatedConfig = ts.factory.updateObjectLiteralExpression(
|
||||
config,
|
||||
config.properties
|
||||
// remove the experimentalFetchPolyfill property from the top level config object
|
||||
.filter(
|
||||
(p) =>
|
||||
!ts.isPropertyAssignment(p) ||
|
||||
p.name.getText() !== 'experimentalFetchPolyfill'
|
||||
)
|
||||
.map((p) => {
|
||||
if (
|
||||
ts.isPropertyAssignment(p) &&
|
||||
['component', 'e2e'].includes(p.name.getText()) &&
|
||||
ts.isObjectLiteralExpression(p.initializer)
|
||||
) {
|
||||
// remove the experimentalFetchPolyfill property from the component or e2e config object
|
||||
return ts.factory.updatePropertyAssignment(
|
||||
p,
|
||||
p.name,
|
||||
ts.factory.updateObjectLiteralExpression(
|
||||
p.initializer,
|
||||
p.initializer.properties.filter(
|
||||
(ip) =>
|
||||
!ts.isPropertyAssignment(ip) ||
|
||||
ip.name.getText() !== 'experimentalFetchPolyfill'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return p;
|
||||
})
|
||||
);
|
||||
|
||||
return cypressConfig.replace(
|
||||
config.getText(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, updatedConfig, sourceFile)
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
#### Replace the `experimentalJustInTimeCompile` Configuration Option with `justInTimeCompile`
|
||||
|
||||
Replaces the `experimentalJustInTimeCompile` configuration option with the new `justInTimeCompile` configuration option. Read more at the [migration notes](https://docs.cypress.io/app/references/migration-guide#CT-Just-in-Time-Compile-changes).
|
||||
|
||||
#### Examples
|
||||
|
||||
If the `experimentalJustInTimeCompile` configuration option is present and set to `true`, the migration will remove it. This is to account for the fact that JIT compilation is the default behavior in Cypress v14.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
experimentalJustInTimeCompile: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
If the `experimentalJustInTimeCompile` configuration option is set to `false` and it is using webpack, the migration will rename it to `justInTimeCompile`.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" highlightLines=[9] %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
justInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
If the `experimentalJustInTimeCompile` configuration is set to any value and it is using Vite, the migration will remove it. This is to account for the fact that JIT compilation no longer applies to Vite.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'vite',
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress.config.ts" %}
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
@ -0,0 +1,319 @@
|
||||
import { addProjectConfiguration, type Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import migration from './replace-experimental-just-in-time-compile';
|
||||
|
||||
describe('replace-experimental-just-in-time-compile', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should do nothing when there are no projects with cypress config', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.exists('apps/app1/cypress.config.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing when the cypress config cannot be parsed as expected', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write('apps/app1/cypress.config.ts', `export const foo = 'bar';`);
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.read('apps/app1/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export const foo = 'bar';
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle when the cypress config path in the executor is not valid', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
ct: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1/non-existent-cypress.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle cypress config files in projects using the "@nx/cypress:cypress" executor', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
ct: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1/cypress.custom-config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.custom-config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.custom-config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should remove the experimentalJustInTimeCompile property from the top-level config when using vite', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should remove the experimentalJustInTimeCompile property from the component config when using vite', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should remove the experimentalJustInTimeCompile property from both the top-level config and the component config when using vite', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should remove the experimentalJustInTimeCompile property from the top-level config when set to true and it is using webpack', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
experimentalJustInTimeCompile: true,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should rename the experimentalJustInTimeCompile property to justInTimeCompile in the top-level config when set to false and it is using webpack', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
justInTimeCompile: false,
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove the experimentalJustInTimeCompile property from the component config when set to true and it is using webpack', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
experimentalJustInTimeCompile: true,
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
const config = tree.read('apps/app1/cypress.config.ts', 'utf-8');
|
||||
expect(config).not.toContain('experimentalJustInTimeCompile');
|
||||
expect(config).not.toContain('justInTimeCompile');
|
||||
});
|
||||
|
||||
it('should rename the experimentalJustInTimeCompile property to justInTimeCompile in the component config when set to false and it is using webpack', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
experimentalJustInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
justInTimeCompile: false,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,159 @@
|
||||
import { formatFiles, workspaceRoot, type Tree } from '@nx/devkit';
|
||||
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import { join } from 'node:path';
|
||||
import type {
|
||||
ObjectLiteralExpression,
|
||||
Printer,
|
||||
PropertyAssignment,
|
||||
} from 'typescript';
|
||||
import { resolveCypressConfigObject } from '../../utils/config';
|
||||
import {
|
||||
cypressProjectConfigs,
|
||||
getObjectProperty,
|
||||
removeObjectProperty,
|
||||
updateObjectProperty,
|
||||
} from '../../utils/migrations';
|
||||
|
||||
let printer: Printer;
|
||||
let ts: typeof import('typescript');
|
||||
|
||||
// https://docs.cypress.io/app/references/migration-guide#CT-Just-in-Time-Compile-changes
|
||||
export default async function (tree: Tree) {
|
||||
for await (const { cypressConfigPath } of cypressProjectConfigs(tree)) {
|
||||
if (!tree.exists(cypressConfigPath)) {
|
||||
// cypress config file doesn't exist, so skip
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedConfig = await updateCtJustInTimeCompile(
|
||||
tree,
|
||||
cypressConfigPath
|
||||
);
|
||||
|
||||
tree.write(cypressConfigPath, updatedConfig);
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
async function updateCtJustInTimeCompile(
|
||||
tree: Tree,
|
||||
cypressConfigPath: string
|
||||
): Promise<string> {
|
||||
const cypressConfig = tree.read(cypressConfigPath, 'utf-8');
|
||||
const config = resolveCypressConfigObject(cypressConfig);
|
||||
|
||||
if (!config) {
|
||||
// couldn't find the config object, leave as is
|
||||
return cypressConfig;
|
||||
}
|
||||
|
||||
if (!getObjectProperty(config, 'component')) {
|
||||
// no component config, leave as is
|
||||
return cypressConfig;
|
||||
}
|
||||
|
||||
ts ??= ensureTypescript();
|
||||
printer ??= ts.createPrinter();
|
||||
|
||||
const sourceFile = tsquery.ast(cypressConfig);
|
||||
let updatedConfig = config;
|
||||
|
||||
const bundler = await resolveBundler(updatedConfig, cypressConfigPath);
|
||||
const isViteBundler = bundler === 'vite';
|
||||
|
||||
const existingJustInTimeCompileProperty = getObjectProperty(
|
||||
updatedConfig,
|
||||
'experimentalJustInTimeCompile'
|
||||
);
|
||||
if (existingJustInTimeCompileProperty) {
|
||||
if (
|
||||
isViteBundler ||
|
||||
existingJustInTimeCompileProperty.initializer.kind ===
|
||||
ts.SyntaxKind.TrueKeyword
|
||||
) {
|
||||
// if it's using vite or it's set to true (the new default value), remove it
|
||||
updatedConfig = removeObjectProperty(
|
||||
updatedConfig,
|
||||
existingJustInTimeCompileProperty
|
||||
);
|
||||
} else {
|
||||
// rename to justInTimeCompile
|
||||
updatedConfig = updateObjectProperty(
|
||||
updatedConfig,
|
||||
existingJustInTimeCompileProperty,
|
||||
{ newName: 'justInTimeCompile' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const componentProperty = getObjectProperty(updatedConfig, 'component');
|
||||
if (
|
||||
componentProperty &&
|
||||
ts.isObjectLiteralExpression(componentProperty.initializer)
|
||||
) {
|
||||
const componentConfigObject = componentProperty.initializer;
|
||||
const existingJustInTimeCompileProperty = getObjectProperty(
|
||||
componentConfigObject,
|
||||
'experimentalJustInTimeCompile'
|
||||
);
|
||||
if (existingJustInTimeCompileProperty) {
|
||||
if (
|
||||
isViteBundler ||
|
||||
existingJustInTimeCompileProperty.initializer.kind ===
|
||||
ts.SyntaxKind.TrueKeyword
|
||||
) {
|
||||
// if it's using vite or it's set to true (the new default value), remove it
|
||||
updatedConfig = updateObjectProperty(updatedConfig, componentProperty, {
|
||||
newValue: removeObjectProperty(
|
||||
componentConfigObject,
|
||||
existingJustInTimeCompileProperty
|
||||
),
|
||||
});
|
||||
} else {
|
||||
// rename to justInTimeCompile
|
||||
updatedConfig = updateObjectProperty(updatedConfig, componentProperty, {
|
||||
newValue: updateObjectProperty(
|
||||
componentConfigObject,
|
||||
existingJustInTimeCompileProperty,
|
||||
{ newName: 'justInTimeCompile' }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cypressConfig.replace(
|
||||
config.getText(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, updatedConfig, sourceFile)
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveBundler(
|
||||
config: ObjectLiteralExpression,
|
||||
cypressConfigPath: string
|
||||
): Promise<string | null> {
|
||||
const bundlerProperty = tsquery.query<PropertyAssignment>(
|
||||
config,
|
||||
'PropertyAssignment:has(Identifier[name=component]) PropertyAssignment:has(Identifier[name=devServer]) PropertyAssignment:has(Identifier[name=bundler])'
|
||||
)[0];
|
||||
|
||||
if (bundlerProperty) {
|
||||
return ts.isStringLiteral(bundlerProperty.initializer)
|
||||
? bundlerProperty.initializer.getText().replace(/['"`]/g, '')
|
||||
: null;
|
||||
}
|
||||
|
||||
try {
|
||||
// we can't statically resolve the bundler from the config, so we load the config
|
||||
const cypressConfig = await loadConfigFile(
|
||||
join(workspaceRoot, cypressConfigPath)
|
||||
);
|
||||
|
||||
return cypressConfig.component?.devServer?.bundler;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
#### Set `injectDocumentDomain` Configuration Option
|
||||
|
||||
Replaces the removed `experimentalSkipDomainInjection` configuration option with the new `injectDocumentDomain` configuration option when needed. Skipping domain injection is the default behavior in Cypress v14 and therefore, it is required to use the `cy.origin()` command when navigating between domains. The `injectDocumentDomain` option was introduced to ease the transition to v14, but it is deprecated and will be removed in Cypress v15. Read more at the [migration notes](https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin).
|
||||
|
||||
#### Examples
|
||||
|
||||
If the `experimentalSkipDomainInjection` configuration option is present, the migration will remove it. This is to account for the fact that skipping domain injection is the default behavior in Cypress v14.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: ['https://example.com'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
If the `experimentalSkipDomainInjection` configuration option is present and set to an empty array (no domain injection is skipped), the migration will remove it and will set the `injectDocumentDomain` option to `true`.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: [],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" highlightLines=["17-19"] %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use `cy.origin()` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
If the `experimentalSkipDomainInjection` configuration option is not present (no domain injection is skipped), the migration will set the `injectDocumentDomain` option to `true`.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1-e2e/cypress.config.ts" highlightLines=["17-19"] %}
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use `cy.origin()` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
@ -0,0 +1,712 @@
|
||||
import { addProjectConfiguration, type Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import migration from './set-inject-document-domain';
|
||||
|
||||
describe('set-inject-document-domain', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should do nothing when there are no projects with cypress config', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing when the cypress config cannot be parsed as expected', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write('apps/app1-e2e/cypress.config.ts', `export const foo = 'bar';`);
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export const foo = 'bar';
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle when the cypress config path in the executor is not valid', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1-e2e/non-existent-cypress.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should set the injectDocumentDomain property to true in the top-level config when there is no e2e or component config', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should replace the experimentalSkipDomainInjection property in the top-level config with injectDocumentDomain when it is set to an empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: [],
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should replace the experimentalSkipDomainInjection property in the top-level config with injectDocumentDomain when it is set to an empty array and there is an e2e or component config without experimentalSkipDomainInjection', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
experimentalSkipDomainInjection: [],
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove the experimentalSkipDomainInjection property from the top-level config when it is set to a non-empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: ['https://example.com'],
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set the injectDocumentDomain property to true in the e2e config when defined and experimentalSkipDomainInjection is not set', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set the injectDocumentDomain property to true in the e2e config when it is not an object literal', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should replace the experimentalSkipDomainInjection property in the e2e config with injectDocumentDomain when it is set to an empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: [],
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove the experimentalSkipDomainInjection property from the e2e config when it is set to a non-empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
experimentalSkipDomainInjection: ['https://example.com'],
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set the injectDocumentDomain property to true in the component config when defined and experimentalSkipDomainInjection is not set', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set the injectDocumentDomain property to true in the component config when it is not an object literal', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should replace the experimentalSkipDomainInjection property in the component config with injectDocumentDomain when it is set to an empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
experimentalSkipDomainInjection: [],
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove the experimentalSkipDomainInjection property in the component config when it is set to a non-empty array', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
experimentalSkipDomainInjection: ['https://example.com'],
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
...nxComponentTestingPreset(__filename, { bundler: 'vite' }),
|
||||
},
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle cypress config files in projects using the "@nx/cypress:cypress" executor', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1-e2e/cypress.custom-config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1-e2e/cypress.custom-config.ts',
|
||||
`import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1-e2e/cypress.custom-config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
bundler: 'vite',
|
||||
webServerCommands: {
|
||||
default: 'pnpm exec nx run app1:dev',
|
||||
production: 'pnpm exec nx run app1:dev',
|
||||
},
|
||||
ciWebServerCommand: 'pnpm exec nx run app1:dev',
|
||||
ciBaseUrl: 'http://localhost:4200',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
// Please ensure you use \`cy.origin()\` when navigating between domains and remove this option.
|
||||
// See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
injectDocumentDomain: true,
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,251 @@
|
||||
import { formatFiles, type Tree } from '@nx/devkit';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import type {
|
||||
Expression,
|
||||
ObjectLiteralExpression,
|
||||
Printer,
|
||||
PropertyAssignment,
|
||||
} from 'typescript';
|
||||
import { resolveCypressConfigObject } from '../../utils/config';
|
||||
import {
|
||||
cypressProjectConfigs,
|
||||
getObjectProperty,
|
||||
removeObjectProperty,
|
||||
updateObjectProperty,
|
||||
} from '../../utils/migrations';
|
||||
|
||||
let printer: Printer;
|
||||
let ts: typeof import('typescript');
|
||||
|
||||
// https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin
|
||||
// https://docs.cypress.io/app/references/changelog#:~:text=The%20experimentalSkipDomainInjection%20configuration%20has%20been,injectDocumentDomain%20configuration
|
||||
export default async function (tree: Tree) {
|
||||
for await (const { cypressConfigPath } of cypressProjectConfigs(tree)) {
|
||||
if (!tree.exists(cypressConfigPath)) {
|
||||
// cypress config file doesn't exist, so skip
|
||||
continue;
|
||||
}
|
||||
|
||||
ts ??= ensureTypescript();
|
||||
printer ??= ts.createPrinter();
|
||||
|
||||
const cypressConfig = tree.read(cypressConfigPath, 'utf-8');
|
||||
const updatedConfig = setInjectDocumentDomain(cypressConfig);
|
||||
|
||||
tree.write(cypressConfigPath, updatedConfig);
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
function setInjectDocumentDomain(cypressConfig: string): string {
|
||||
const config = resolveCypressConfigObject(cypressConfig);
|
||||
|
||||
if (!config) {
|
||||
// couldn't find the config object, leave as is
|
||||
return cypressConfig;
|
||||
}
|
||||
|
||||
const sourceFile = tsquery.ast(cypressConfig);
|
||||
let e2eProperty = getObjectProperty(config, 'e2e');
|
||||
let componentProperty = getObjectProperty(config, 'component');
|
||||
let updatedConfig = config;
|
||||
|
||||
const topLevelExperimentalSkipDomainInjectionProperty = getObjectProperty(
|
||||
updatedConfig,
|
||||
'experimentalSkipDomainInjection'
|
||||
);
|
||||
const topLevelSkipDomainState: 'not-set' | 'skipping' | 'not-skipping' =
|
||||
!topLevelExperimentalSkipDomainInjectionProperty
|
||||
? 'not-set'
|
||||
: !ts.isArrayLiteralExpression(
|
||||
topLevelExperimentalSkipDomainInjectionProperty.initializer
|
||||
) ||
|
||||
topLevelExperimentalSkipDomainInjectionProperty.initializer.elements
|
||||
.length > 0
|
||||
? 'skipping'
|
||||
: 'not-skipping';
|
||||
|
||||
let e2eSkipDomainState: 'not-set' | 'skipping' | 'not-skipping' = 'not-set';
|
||||
if (e2eProperty) {
|
||||
let experimentalSkipDomainInjectionProperty: PropertyAssignment | undefined;
|
||||
let isObjectLiteral = false;
|
||||
if (ts.isObjectLiteralExpression(e2eProperty.initializer)) {
|
||||
experimentalSkipDomainInjectionProperty = getObjectProperty(
|
||||
e2eProperty.initializer,
|
||||
'experimentalSkipDomainInjection'
|
||||
);
|
||||
isObjectLiteral = true;
|
||||
}
|
||||
|
||||
if (experimentalSkipDomainInjectionProperty) {
|
||||
e2eSkipDomainState =
|
||||
!ts.isArrayLiteralExpression(
|
||||
experimentalSkipDomainInjectionProperty.initializer
|
||||
) ||
|
||||
experimentalSkipDomainInjectionProperty.initializer.elements.length > 0
|
||||
? 'skipping'
|
||||
: 'not-skipping';
|
||||
}
|
||||
|
||||
if (
|
||||
e2eSkipDomainState === 'not-set' &&
|
||||
topLevelSkipDomainState === 'not-set'
|
||||
) {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, e2eProperty, {
|
||||
newValue: setInjectDocumentDomainInObject(e2eProperty.initializer),
|
||||
});
|
||||
} else if (e2eSkipDomainState === 'not-skipping') {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, e2eProperty, {
|
||||
newValue: replaceExperimentalSkipDomainInjectionInObject(
|
||||
e2eProperty.initializer
|
||||
),
|
||||
});
|
||||
} else if (e2eSkipDomainState === 'skipping') {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, e2eProperty, {
|
||||
newValue: removeObjectProperty(
|
||||
// we only determine that it's skipping if it's an object literal
|
||||
e2eProperty.initializer as ObjectLiteralExpression,
|
||||
getObjectProperty(
|
||||
e2eProperty.initializer as ObjectLiteralExpression,
|
||||
'experimentalSkipDomainInjection'
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let componentSkipDomainState: 'not-set' | 'skipping' | 'not-skipping' =
|
||||
'not-set';
|
||||
if (componentProperty) {
|
||||
let experimentalSkipDomainInjectionProperty: PropertyAssignment | undefined;
|
||||
let isObjectLiteral = false;
|
||||
if (ts.isObjectLiteralExpression(componentProperty.initializer)) {
|
||||
experimentalSkipDomainInjectionProperty = getObjectProperty(
|
||||
componentProperty.initializer,
|
||||
'experimentalSkipDomainInjection'
|
||||
);
|
||||
isObjectLiteral = true;
|
||||
}
|
||||
|
||||
if (experimentalSkipDomainInjectionProperty) {
|
||||
componentSkipDomainState =
|
||||
!ts.isArrayLiteralExpression(
|
||||
experimentalSkipDomainInjectionProperty.initializer
|
||||
) ||
|
||||
experimentalSkipDomainInjectionProperty.initializer.elements.length > 0
|
||||
? 'skipping'
|
||||
: 'not-skipping';
|
||||
}
|
||||
|
||||
if (
|
||||
componentSkipDomainState === 'not-set' &&
|
||||
topLevelSkipDomainState === 'not-set'
|
||||
) {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, componentProperty, {
|
||||
newValue: setInjectDocumentDomainInObject(
|
||||
componentProperty.initializer
|
||||
),
|
||||
});
|
||||
} else if (componentSkipDomainState === 'not-skipping') {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, componentProperty, {
|
||||
newValue: replaceExperimentalSkipDomainInjectionInObject(
|
||||
componentProperty.initializer
|
||||
),
|
||||
});
|
||||
} else if (componentSkipDomainState === 'skipping') {
|
||||
updatedConfig = updateObjectProperty(updatedConfig, componentProperty, {
|
||||
newValue: removeObjectProperty(
|
||||
// we only determine that it's skipping if it's an object literal
|
||||
componentProperty.initializer as ObjectLiteralExpression,
|
||||
getObjectProperty(
|
||||
componentProperty.initializer as ObjectLiteralExpression,
|
||||
'experimentalSkipDomainInjection'
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
topLevelSkipDomainState === 'not-set' &&
|
||||
!e2eProperty &&
|
||||
!componentProperty
|
||||
) {
|
||||
updatedConfig = setInjectDocumentDomainInObject(updatedConfig);
|
||||
} else if (topLevelSkipDomainState === 'not-skipping') {
|
||||
updatedConfig =
|
||||
replaceExperimentalSkipDomainInjectionInObject(updatedConfig);
|
||||
} else if (topLevelSkipDomainState === 'skipping') {
|
||||
updatedConfig = removeObjectProperty(
|
||||
updatedConfig,
|
||||
topLevelExperimentalSkipDomainInjectionProperty
|
||||
);
|
||||
}
|
||||
|
||||
return cypressConfig.replace(
|
||||
config.getText(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, updatedConfig, sourceFile)
|
||||
);
|
||||
}
|
||||
|
||||
function setInjectDocumentDomainInObject(
|
||||
config: Expression
|
||||
): ObjectLiteralExpression {
|
||||
let configToUpdate: ObjectLiteralExpression;
|
||||
if (ts.isObjectLiteralExpression(config)) {
|
||||
configToUpdate = config;
|
||||
} else {
|
||||
// spread the current expression into a new object literal
|
||||
configToUpdate = ts.factory.createObjectLiteralExpression([
|
||||
ts.factory.createSpreadAssignment(config),
|
||||
]);
|
||||
}
|
||||
|
||||
return ts.factory.updateObjectLiteralExpression(
|
||||
configToUpdate,
|
||||
ts.factory.createNodeArray([
|
||||
...configToUpdate.properties,
|
||||
getInjectDocumentDomainPropertyAssignment(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function replaceExperimentalSkipDomainInjectionInObject(
|
||||
config: Expression
|
||||
): ObjectLiteralExpression {
|
||||
let configToUpdate: ObjectLiteralExpression;
|
||||
if (ts.isObjectLiteralExpression(config)) {
|
||||
configToUpdate = config;
|
||||
} else {
|
||||
// spread the current expression into a new object literal
|
||||
configToUpdate = ts.factory.createObjectLiteralExpression([
|
||||
ts.factory.createSpreadAssignment(config),
|
||||
]);
|
||||
}
|
||||
|
||||
return ts.factory.updateObjectLiteralExpression(
|
||||
configToUpdate,
|
||||
configToUpdate.properties.map((property) =>
|
||||
property.name?.getText() === 'experimentalSkipDomainInjection'
|
||||
? getInjectDocumentDomainPropertyAssignment()
|
||||
: property
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getInjectDocumentDomainPropertyAssignment(): PropertyAssignment {
|
||||
return ts.addSyntheticLeadingComment(
|
||||
ts.addSyntheticLeadingComment(
|
||||
ts.factory.createPropertyAssignment(
|
||||
ts.factory.createIdentifier('injectDocumentDomain'),
|
||||
ts.factory.createTrue()
|
||||
),
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' Please ensure you use `cy.origin()` when navigating between domains and remove this option.'
|
||||
),
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' See https://docs.cypress.io/app/references/migration-guide#Changes-to-cyorigin'
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
#### Update Component Testing `mount` Imports
|
||||
|
||||
Updates the relevant module specifiers when importing the `mount` function and using the Angular or React frameworks. Read more at the [Angular migration notes](https://docs.cypress.io/app/references/migration-guide#Angular-1720-CT-no-longer-supported) and the [React migration notes](https://docs.cypress.io/app/references/migration-guide#React-18-CT-no-longer-supported).
|
||||
|
||||
#### Examples
|
||||
|
||||
If using the Angular framework with a version greater than or equal to v17.2.0 and importing the `mount` function from the `cypress/angular-signals` module, the migration will update the import to use the `cypress/angular` module.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" %}
|
||||
import { mount } from 'cypress/angular-signals';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" highlightLines=[1] %}
|
||||
import { mount } from 'cypress/angular';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
If using the Angular framework with a version lower than v17.2.0 and importing the `mount` function from the `cypress/angular` module, the migration will install the `@cypress/angular@2` package and update the import to use the `@cypress/angular` module.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```json {% fileName="package.json" %}
|
||||
{
|
||||
"name": "@my-repo/source",
|
||||
"dependencies": {
|
||||
...
|
||||
"cypress": "^14.2.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" %}
|
||||
import { mount } from 'cypress/angular';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```json {% fileName="package.json" highlightLines=[6] %}
|
||||
{
|
||||
"name": "@my-repo/source",
|
||||
"dependencies": {
|
||||
...
|
||||
"cypress": "^14.2.1",
|
||||
"@cypress/angular": "^2.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" highlightLines=[1] %}
|
||||
import { mount } from '@cypress/angular';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
If using the React framework and importing the `mount` function from the `cypress/react18` module, the migration will update the import to use the `cypress/react` module.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Before" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" %}
|
||||
import { mount } from 'cypress/react18';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="After" %}
|
||||
|
||||
```ts {% fileName="apps/app1/cypress/support/component.ts" highlightLines=[1] %}
|
||||
import { mount } from 'cypress/react';
|
||||
import './commands';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
@ -0,0 +1,443 @@
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
readJson,
|
||||
type ProjectConfiguration,
|
||||
type ProjectGraph,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import migration from './update-component-testing-mount-imports';
|
||||
|
||||
let projectGraph: ProjectGraph;
|
||||
jest.mock('@nx/devkit', () => ({
|
||||
...jest.requireActual('@nx/devkit'),
|
||||
createProjectGraphAsync: jest.fn(() => Promise.resolve(projectGraph)),
|
||||
}));
|
||||
|
||||
describe('update-component-testing-mount-imports', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should do nothing when there are no projects with cypress config', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.exists('apps/app1-e2e/cypress.config.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing when the cypress config cannot be parsed as expected', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write('apps/app1-e2e/cypress.config.ts', `export const foo = 'bar';`);
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
expect(tree.read('apps/app1-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export const foo = 'bar';
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle when the cypress config path in the executor is not valid', async () => {
|
||||
addProjectConfiguration(tree, 'app1-e2e', {
|
||||
root: 'apps/app1-e2e',
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/app1-e2e/non-existent-cypress.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(migration(tree)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should convert import from cypress/react18 to cypress/react when the framework information is directly set in the config', async () => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/App.cy.tsx',
|
||||
`import { mount } from 'cypress/react18';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/App.cy.tsx', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it.each(['react', 'next', 'remix'])(
|
||||
'should convert import from cypress/react18 to cypress/react when the framework information is set in the Nx preset import for %s',
|
||||
async (framework: string) => {
|
||||
addProjectConfiguration(tree, 'app1', {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
});
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/${framework}/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/App.cy.tsx',
|
||||
`import { mount } from 'cypress/react18';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/App.cy.tsx', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['react', 'next'])(
|
||||
'should convert import from cypress/react18 to cypress/react when the framework cannot be determined from statically analysing the config but the project depends on %s',
|
||||
async (pkg: string) => {
|
||||
const project: ProjectConfiguration = {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
};
|
||||
projectGraph = {
|
||||
nodes: { app1: { name: 'app1', type: 'app', data: project } },
|
||||
dependencies: {
|
||||
app1: [{ target: `npm:${pkg}`, type: 'static', source: 'app1' }],
|
||||
},
|
||||
externalNodes: {
|
||||
[`npm:${pkg}`]: {
|
||||
name: `npm:${pkg}`,
|
||||
type: 'npm',
|
||||
data: { packageName: pkg, version: '19.0.0' },
|
||||
},
|
||||
},
|
||||
};
|
||||
addProjectConfiguration(tree, 'app1', project);
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { somePreset } from '@org/some-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: somePreset(__filename),
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/App.cy.tsx',
|
||||
`import { mount } from 'cypress/react18';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/App.cy.tsx', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(<App />);
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
}
|
||||
);
|
||||
|
||||
it('should convert import from cypress/angular-signals to cypress/angular when the framework information is directly set in the config', async () => {
|
||||
const project: ProjectConfiguration = {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
};
|
||||
projectGraph = {
|
||||
nodes: { app1: { name: 'app1', type: 'app', data: project } },
|
||||
dependencies: {
|
||||
app1: [{ target: 'npm:@angular/core', type: 'static', source: 'app1' }],
|
||||
},
|
||||
externalNodes: {
|
||||
'npm:@angular/core': {
|
||||
name: 'npm:@angular/core',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: '@angular/core',
|
||||
version: '19.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
addProjectConfiguration(tree, 'app1', project);
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/app.component.cy.ts',
|
||||
`import { mount } from 'cypress/angular-signals';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {})
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/app.component.cy.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/angular';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {});
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should convert import from cypress/angular-signals to cypress/angular when the framework is set in the Nx preset import', async () => {
|
||||
const project: ProjectConfiguration = {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
};
|
||||
projectGraph = {
|
||||
nodes: { app1: { name: 'app1', type: 'app', data: project } },
|
||||
dependencies: {
|
||||
app1: [{ target: 'npm:@angular/core', type: 'static', source: 'app1' }],
|
||||
},
|
||||
externalNodes: {
|
||||
'npm:@angular/core': {
|
||||
name: 'npm:@angular/core',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: '@angular/core',
|
||||
version: '19.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
addProjectConfiguration(tree, 'app1', project);
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
|
||||
import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/app.component.cy.ts',
|
||||
`import { mount } from 'cypress/angular-signals';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {})
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/app.component.cy.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/angular';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {});
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should convert import from cypress/angular-signals to cypress/angular when the framework cannot be determined from statically analysing the config but the project depends on angular', async () => {
|
||||
const project: ProjectConfiguration = {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
};
|
||||
projectGraph = {
|
||||
nodes: { app1: { name: 'app1', type: 'app', data: project } },
|
||||
dependencies: {
|
||||
app1: [{ target: 'npm:@angular/core', type: 'static', source: 'app1' }],
|
||||
},
|
||||
externalNodes: {
|
||||
'npm:@angular/core': {
|
||||
name: 'npm:@angular/core',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: '@angular/core',
|
||||
version: '19.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
addProjectConfiguration(tree, 'app1', project);
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { somePreset } from '@org/some-preset';
|
||||
import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: somePreset(__filename),
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/app.component.cy.ts',
|
||||
`import { mount } from 'cypress/angular-signals';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {})
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/app.component.cy.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/angular';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {});
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should convert import from cypress/angular to @cypress/angular and install the package if it is a version lower than v17.2.0', async () => {
|
||||
const project: ProjectConfiguration = {
|
||||
root: 'apps/app1',
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
};
|
||||
projectGraph = {
|
||||
nodes: { app1: { name: 'app1', type: 'app', data: project } },
|
||||
dependencies: {
|
||||
app1: [{ target: 'npm:@angular/core', type: 'static', source: 'app1' }],
|
||||
},
|
||||
externalNodes: {
|
||||
'npm:@angular/core': {
|
||||
name: 'npm:@angular/core',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: '@angular/core',
|
||||
version: '17.1.3',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
addProjectConfiguration(tree, 'app1', project);
|
||||
tree.write(
|
||||
'apps/app1/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'angular',
|
||||
bundler: 'webpack',
|
||||
},
|
||||
},
|
||||
});`
|
||||
);
|
||||
tree.write(
|
||||
'apps/app1/src/app/app.component.cy.ts',
|
||||
`import { mount } from 'cypress/angular';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {})
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
await migration(tree);
|
||||
|
||||
expect(tree.read('apps/app1/src/app/app.component.cy.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from '@cypress/angular';
|
||||
describe('App', () => {
|
||||
it('should render', () => {
|
||||
mount(AppComponent, {});
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
expect(
|
||||
readJson(tree, 'package.json').devDependencies['@cypress/angular']
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,299 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
createProjectGraphAsync,
|
||||
formatFiles,
|
||||
visitNotIgnoredFiles,
|
||||
type ProjectConfiguration,
|
||||
type ProjectGraph,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import { lt, valid } from 'semver';
|
||||
import type {
|
||||
ImportDeclaration,
|
||||
ObjectLiteralExpression,
|
||||
Printer,
|
||||
PropertyAssignment,
|
||||
} from 'typescript';
|
||||
import { resolveCypressConfigObject } from '../../utils/config';
|
||||
import {
|
||||
cypressProjectConfigs,
|
||||
getObjectProperty,
|
||||
} from '../../utils/migrations';
|
||||
|
||||
let printer: Printer;
|
||||
let ts: typeof import('typescript');
|
||||
|
||||
export default async function (tree: Tree) {
|
||||
const projectGraph = await createProjectGraphAsync();
|
||||
|
||||
for await (const {
|
||||
cypressConfigPath,
|
||||
projectName,
|
||||
projectConfig,
|
||||
} of cypressProjectConfigs(tree)) {
|
||||
if (!tree.exists(cypressConfigPath)) {
|
||||
// cypress config file doesn't exist, so skip
|
||||
continue;
|
||||
}
|
||||
|
||||
ts ??= ensureTypescript();
|
||||
printer ??= ts.createPrinter();
|
||||
|
||||
const migrationInfo = parseMigrationInfo(
|
||||
tree,
|
||||
cypressConfigPath,
|
||||
projectName,
|
||||
projectGraph
|
||||
);
|
||||
|
||||
if (!migrationInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (migrationInfo.framework === 'angular') {
|
||||
migrateAngularFramework(
|
||||
tree,
|
||||
projectConfig,
|
||||
migrationInfo.isLegacyVersion
|
||||
);
|
||||
} else if (migrationInfo.framework === 'react') {
|
||||
migrateReactFramework(tree, projectConfig);
|
||||
}
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
function parseMigrationInfo(
|
||||
tree: Tree,
|
||||
cypressConfigPath: string,
|
||||
projectName: string,
|
||||
projectGraph: ProjectGraph
|
||||
): {
|
||||
framework?: 'angular' | 'react';
|
||||
isLegacyVersion?: boolean;
|
||||
} | null {
|
||||
const cypressConfig = tree.read(cypressConfigPath, 'utf-8');
|
||||
const config = resolveCypressConfigObject(cypressConfig);
|
||||
|
||||
if (!config) {
|
||||
// couldn't find the config object, leave as is
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!getObjectProperty(config, 'component')) {
|
||||
// no component config, leave as is
|
||||
return null;
|
||||
}
|
||||
|
||||
const framework = resolveFramework(
|
||||
cypressConfig,
|
||||
config,
|
||||
projectName,
|
||||
projectGraph
|
||||
);
|
||||
|
||||
if (framework === 'react') {
|
||||
return { framework: 'react' };
|
||||
}
|
||||
|
||||
if (framework === 'angular') {
|
||||
const angularCoreDep = projectGraph.dependencies[projectName].find((d) =>
|
||||
// account for possible different versions of angular core
|
||||
d.target.startsWith('npm:@angular/core')
|
||||
);
|
||||
if (angularCoreDep) {
|
||||
const angularVersion =
|
||||
projectGraph.externalNodes?.[angularCoreDep.target]?.data?.version;
|
||||
if (valid(angularVersion) && lt(angularVersion, '17.2.0')) {
|
||||
return {
|
||||
framework: 'angular',
|
||||
isLegacyVersion: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
framework: 'angular',
|
||||
isLegacyVersion: false,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveFramework(
|
||||
cypressConfig: string,
|
||||
config: ObjectLiteralExpression,
|
||||
projectName: string,
|
||||
projectGraph: ProjectGraph
|
||||
): string | null {
|
||||
const frameworkProperty = tsquery.query<PropertyAssignment>(
|
||||
config,
|
||||
'PropertyAssignment:has(Identifier[name=component]) PropertyAssignment:has(Identifier[name=devServer]) PropertyAssignment:has(Identifier[name=framework])'
|
||||
)[0];
|
||||
|
||||
if (frameworkProperty) {
|
||||
return ts.isStringLiteral(frameworkProperty.initializer)
|
||||
? frameworkProperty.initializer.getText().replace(/['"`]/g, '')
|
||||
: null;
|
||||
}
|
||||
|
||||
// component might be assigned to an Nx preset function call, so we try to
|
||||
// infer the framework from the Nx preset import
|
||||
const sourceFile = tsquery.ast(cypressConfig);
|
||||
const nxPresetModuleSpecifiers = [
|
||||
'@nx/angular/plugins/component-testing',
|
||||
'@nx/react/plugins/component-testing',
|
||||
'@nx/next/plugins/component-testing',
|
||||
'@nx/remix/plugins/component-testing',
|
||||
];
|
||||
const nxPresetImportModuleSpecifier = sourceFile.statements
|
||||
.find(
|
||||
(s): s is ImportDeclaration =>
|
||||
ts.isImportDeclaration(s) &&
|
||||
nxPresetModuleSpecifiers.includes(
|
||||
s.moduleSpecifier.getText().replace(/['"`]/g, '')
|
||||
)
|
||||
)
|
||||
?.moduleSpecifier.getText()
|
||||
.replace(/['"`]/g, '');
|
||||
if (nxPresetImportModuleSpecifier) {
|
||||
const plugin = nxPresetImportModuleSpecifier.split('/').at(1);
|
||||
|
||||
return plugin === 'angular' ? 'angular' : 'react';
|
||||
}
|
||||
|
||||
// it might be set to something else, so we fall back to checking the
|
||||
// project dependencies
|
||||
if (
|
||||
projectGraph.dependencies[projectName]?.some((d) =>
|
||||
d.target.startsWith('npm:@angular/core')
|
||||
)
|
||||
) {
|
||||
return 'angular';
|
||||
}
|
||||
|
||||
if (
|
||||
projectGraph.dependencies[projectName]?.some(
|
||||
(d) => d.target.startsWith('npm:react') || d.target.startsWith('npm:next')
|
||||
)
|
||||
) {
|
||||
return 'react';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://docs.cypress.io/app/references/migration-guide#Angular-1720-CT-no-longer-supported
|
||||
function migrateAngularFramework(
|
||||
tree: Tree,
|
||||
projectConfig: ProjectConfiguration,
|
||||
isLegacyVersion: boolean
|
||||
) {
|
||||
visitNotIgnoredFiles(tree, projectConfig.root, (filePath) => {
|
||||
if (!isJsTsFile(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = tree.read(filePath, 'utf-8');
|
||||
|
||||
let updatedFileContent: string;
|
||||
if (isLegacyVersion) {
|
||||
let needPackage = false;
|
||||
|
||||
updatedFileContent = tsquery.replace(
|
||||
content,
|
||||
'ImportDeclaration',
|
||||
importTransformerFactory(
|
||||
content,
|
||||
'cypress/angular',
|
||||
'@cypress/angular',
|
||||
() => {
|
||||
needPackage = true;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (needPackage) {
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ '@cypress/angular': '^2.1.0' },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
updatedFileContent = tsquery.replace(
|
||||
content,
|
||||
'ImportDeclaration',
|
||||
importTransformerFactory(
|
||||
content,
|
||||
'cypress/angular-signals',
|
||||
'cypress/angular'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
tree.write(filePath, updatedFileContent);
|
||||
});
|
||||
}
|
||||
|
||||
// https://docs.cypress.io/app/references/migration-guide#React-18-CT-no-longer-supported
|
||||
function migrateReactFramework(
|
||||
tree: Tree,
|
||||
projectConfig: ProjectConfiguration
|
||||
) {
|
||||
visitNotIgnoredFiles(tree, projectConfig.root, (filePath) => {
|
||||
if (!isJsTsFile(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = tree.read(filePath, 'utf-8');
|
||||
const updatedContent = tsquery.replace(
|
||||
content,
|
||||
'ImportDeclaration',
|
||||
importTransformerFactory(content, 'cypress/react18', 'cypress/react')
|
||||
);
|
||||
|
||||
tree.write(filePath, updatedContent);
|
||||
});
|
||||
}
|
||||
|
||||
function importTransformerFactory(
|
||||
fileContent: string,
|
||||
sourceModuleSpecifier: string,
|
||||
targetModuleSpecifier: string,
|
||||
matchImportCallback?: () => void
|
||||
): Parameters<typeof tsquery.replace>[2] {
|
||||
return (node: ImportDeclaration) => {
|
||||
if (
|
||||
node.moduleSpecifier.getText().replace(/['"`]/g, '') ===
|
||||
sourceModuleSpecifier
|
||||
) {
|
||||
matchImportCallback?.();
|
||||
const updatedImport = ts.factory.updateImportDeclaration(
|
||||
node,
|
||||
node.modifiers,
|
||||
node.importClause,
|
||||
ts.factory.createStringLiteral(targetModuleSpecifier),
|
||||
node.attributes
|
||||
);
|
||||
|
||||
return printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
updatedImport,
|
||||
tsquery.ast(fileContent)
|
||||
);
|
||||
}
|
||||
|
||||
return node.getText();
|
||||
};
|
||||
}
|
||||
|
||||
function isJsTsFile(filePath: string) {
|
||||
return /\.[cm]?[jt]sx?$/.test(filePath);
|
||||
}
|
||||
@ -7,8 +7,10 @@ import {
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { Linter, LinterType, lintProjectGenerator } from '@nx/eslint';
|
||||
import { installedCypressVersion } from './cypress-version';
|
||||
import { eslintPluginCypressVersion } from './versions';
|
||||
import {
|
||||
javaScriptOverride,
|
||||
typeScriptOverride,
|
||||
} from '@nx/eslint/src/generators/init/global-eslint-config';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
addOverrideToLintConfig,
|
||||
@ -18,11 +20,8 @@ import {
|
||||
isEslintConfigSupported,
|
||||
replaceOverridesInLintConfig,
|
||||
} from '@nx/eslint/src/generators/utils/eslint-file';
|
||||
import {
|
||||
javaScriptOverride,
|
||||
typeScriptOverride,
|
||||
} from '@nx/eslint/src/generators/init/global-eslint-config';
|
||||
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
|
||||
import { getInstalledCypressMajorVersion, versions } from './versions';
|
||||
|
||||
export interface CyLinterOptions {
|
||||
project: string;
|
||||
@ -77,15 +76,17 @@ export async function addLinterToCyProject(
|
||||
|
||||
options.overwriteExisting = options.overwriteExisting || !eslintFile;
|
||||
|
||||
tasks.push(
|
||||
!options.skipPackageJson
|
||||
? addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ 'eslint-plugin-cypress': eslintPluginCypressVersion }
|
||||
)
|
||||
: () => {}
|
||||
);
|
||||
if (!options.skipPackageJson) {
|
||||
const pkgVersions = versions(tree);
|
||||
|
||||
tasks.push(
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ 'eslint-plugin-cypress': pkgVersions.eslintPluginCypressVersion }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isEslintConfigSupported(tree, projectConfig.root) ||
|
||||
@ -119,7 +120,7 @@ export async function addLinterToCyProject(
|
||||
);
|
||||
tasks.push(addExtendsTask);
|
||||
}
|
||||
const cyVersion = installedCypressVersion();
|
||||
const cyVersion = getInstalledCypressMajorVersion(tree);
|
||||
/**
|
||||
* We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests.
|
||||
* That however leads to issues with the CommonJS Cypress plugin file.
|
||||
|
||||
@ -2,7 +2,9 @@ import {
|
||||
addDefaultCTConfig,
|
||||
addDefaultE2EConfig,
|
||||
addMountDefinition,
|
||||
resolveCypressConfigObject,
|
||||
} from './config';
|
||||
|
||||
describe('Cypress Config parser', () => {
|
||||
it('should add CT config to existing e2e config', async () => {
|
||||
const actual = await addDefaultCTConfig(
|
||||
@ -261,3 +263,133 @@ Cypress.Commands.add('mount', customMount);
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveCypressConfigObject', () => {
|
||||
it('should handle "export default defineConfig()"', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle "export default {}"', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`export default {
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle "export default <variable>" when <variable> is defined in the file and is an object literal', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`const config = {
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle "module.exports = defineConfig()"', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`const { defineConfig } = require('cypress');
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle "module.exports = {}"', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`module.exports = {
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle "module.exports = <variable>" when <variable> is defined in the file and is an object literal', async () => {
|
||||
const config = resolveCypressConfigObject(
|
||||
`const config = {
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
`
|
||||
);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.getText()).toMatchInlineSnapshot(`
|
||||
"{
|
||||
e2e: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { glob, joinPathFragments, type Tree } from '@nx/devkit';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import type {
|
||||
BinaryExpression,
|
||||
ExportAssignment,
|
||||
Expression,
|
||||
ExpressionStatement,
|
||||
InterfaceDeclaration,
|
||||
MethodSignature,
|
||||
ObjectLiteralExpression,
|
||||
PropertyAssignment,
|
||||
PropertySignature,
|
||||
SourceFile,
|
||||
} from 'typescript';
|
||||
import type {
|
||||
NxComponentTestingOptions,
|
||||
@ -184,3 +190,88 @@ export function getProjectCypressConfigPath(
|
||||
|
||||
return cypressConfigPaths[0];
|
||||
}
|
||||
|
||||
export function resolveCypressConfigObject(
|
||||
cypressConfigContents: string
|
||||
): ObjectLiteralExpression | null {
|
||||
const ts = ensureTypescript();
|
||||
|
||||
const { tsquery } = <typeof import('@phenomnomnominal/tsquery')>(
|
||||
require('@phenomnomnominal/tsquery')
|
||||
);
|
||||
const sourceFile = tsquery.ast(cypressConfigContents);
|
||||
|
||||
const exportDefaultStatement = sourceFile.statements.find(
|
||||
(statement): statement is ExportAssignment =>
|
||||
ts.isExportAssignment(statement)
|
||||
);
|
||||
|
||||
if (exportDefaultStatement) {
|
||||
return resolveCypressConfigObjectFromExportExpression(
|
||||
exportDefaultStatement.expression,
|
||||
sourceFile
|
||||
);
|
||||
}
|
||||
|
||||
const moduleExportsStatement = sourceFile.statements.find(
|
||||
(
|
||||
statement
|
||||
): statement is ExpressionStatement & { expression: BinaryExpression } =>
|
||||
ts.isExpressionStatement(statement) &&
|
||||
ts.isBinaryExpression(statement.expression) &&
|
||||
statement.expression.left.getText() === 'module.exports'
|
||||
);
|
||||
|
||||
if (moduleExportsStatement) {
|
||||
return resolveCypressConfigObjectFromExportExpression(
|
||||
moduleExportsStatement.expression.right,
|
||||
sourceFile
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveCypressConfigObjectFromExportExpression(
|
||||
exportExpression: Expression,
|
||||
sourceFile: SourceFile
|
||||
): ObjectLiteralExpression | null {
|
||||
const ts = ensureTypescript();
|
||||
|
||||
if (ts.isObjectLiteralExpression(exportExpression)) {
|
||||
return exportExpression;
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(exportExpression)) {
|
||||
// try to locate the identifier in the source file
|
||||
const variableStatements = sourceFile.statements.filter((statement) =>
|
||||
ts.isVariableStatement(statement)
|
||||
);
|
||||
|
||||
for (const variableStatement of variableStatements) {
|
||||
for (const declaration of variableStatement.declarationList
|
||||
.declarations) {
|
||||
if (
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
declaration.name.getText() === exportExpression.getText() &&
|
||||
ts.isObjectLiteralExpression(declaration.initializer)
|
||||
) {
|
||||
return declaration.initializer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isCallExpression(exportExpression) &&
|
||||
ts.isIdentifier(exportExpression.expression) &&
|
||||
exportExpression.expression.getText() === 'defineConfig' &&
|
||||
ts.isObjectLiteralExpression(exportExpression.arguments[0])
|
||||
) {
|
||||
return exportExpression.arguments[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
let cypressPackageJson;
|
||||
let loadedCypress = false;
|
||||
|
||||
/**
|
||||
* @deprecated use the `getInstalledCypressMajorVersion` exported from
|
||||
* `@nx/cypress/src/utils/versions` instead. It will be removed in v22.
|
||||
*/
|
||||
export function installedCypressVersion() {
|
||||
if (!loadedCypress) {
|
||||
try {
|
||||
@ -21,6 +25,8 @@ export function installedCypressVersion() {
|
||||
|
||||
/**
|
||||
* will not throw if cypress is not installed
|
||||
* @deprecated use the `assertMinimumCypressVersion` exported from
|
||||
* `@nx/cypress/src/utils/versions` instead. It will be removed in v22.
|
||||
*/
|
||||
export function assertMinimumCypressVersion(minVersion: number) {
|
||||
const version = installedCypressVersion();
|
||||
|
||||
124
packages/cypress/src/utils/migrations.ts
Normal file
124
packages/cypress/src/utils/migrations.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import {
|
||||
getProjects,
|
||||
globAsync,
|
||||
type ProjectConfiguration,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { posix } from 'path';
|
||||
import type {
|
||||
Expression,
|
||||
ObjectLiteralExpression,
|
||||
PropertyAssignment,
|
||||
} from 'typescript';
|
||||
import type { CypressExecutorOptions } from '../executors/cypress/cypress.impl';
|
||||
import { CYPRESS_CONFIG_FILE_NAME_PATTERN } from './config';
|
||||
|
||||
let ts: typeof import('typescript');
|
||||
|
||||
export async function* cypressProjectConfigs(tree: Tree): AsyncGenerator<{
|
||||
projectName: string;
|
||||
projectConfig: ProjectConfiguration;
|
||||
cypressConfigPath: string;
|
||||
}> {
|
||||
const projects = getProjects(tree);
|
||||
|
||||
for (const [projectName, projectConfig] of projects) {
|
||||
const targetWithExecutor = Object.values(projectConfig.targets ?? {}).find(
|
||||
(target) => target.executor === '@nx/cypress:cypress'
|
||||
);
|
||||
if (targetWithExecutor) {
|
||||
for (const [, options] of allTargetOptions<CypressExecutorOptions>(
|
||||
targetWithExecutor
|
||||
)) {
|
||||
if (options.cypressConfig) {
|
||||
yield {
|
||||
projectName,
|
||||
projectConfig,
|
||||
cypressConfigPath: options.cypressConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// might be using the crystal plugin
|
||||
const result = await globAsync(tree, [
|
||||
posix.join(projectConfig.root, CYPRESS_CONFIG_FILE_NAME_PATTERN),
|
||||
]);
|
||||
if (result.length > 0) {
|
||||
yield {
|
||||
projectName,
|
||||
projectConfig,
|
||||
cypressConfigPath: result[0],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getObjectProperty(
|
||||
config: ObjectLiteralExpression,
|
||||
name: string
|
||||
): PropertyAssignment | undefined {
|
||||
ts ??= ensureTypescript();
|
||||
|
||||
return config.properties.find(
|
||||
(p): p is PropertyAssignment =>
|
||||
ts.isPropertyAssignment(p) && p.name.getText() === name
|
||||
);
|
||||
}
|
||||
|
||||
export function removeObjectProperty(
|
||||
config: ObjectLiteralExpression,
|
||||
property: PropertyAssignment
|
||||
): ObjectLiteralExpression {
|
||||
ts ??= ensureTypescript();
|
||||
|
||||
return ts.factory.updateObjectLiteralExpression(
|
||||
config,
|
||||
config.properties.filter((p) => p !== property)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateObjectProperty(
|
||||
config: ObjectLiteralExpression,
|
||||
property: PropertyAssignment,
|
||||
{ newName, newValue }: { newName?: string; newValue?: Expression }
|
||||
): ObjectLiteralExpression {
|
||||
ts ??= ensureTypescript();
|
||||
|
||||
if (!newName && !newValue) {
|
||||
throw new Error('newName or newValue must be provided');
|
||||
}
|
||||
|
||||
return ts.factory.updateObjectLiteralExpression(
|
||||
config,
|
||||
config.properties.map((p) =>
|
||||
p === property
|
||||
? ts.factory.updatePropertyAssignment(
|
||||
p,
|
||||
newName ? ts.factory.createIdentifier(newName) : p.name,
|
||||
newValue ? newValue : p.initializer
|
||||
)
|
||||
: p
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function* allTargetOptions<T>(
|
||||
target: TargetConfiguration<T>
|
||||
): Iterable<[string | undefined, T]> {
|
||||
if (target.options) {
|
||||
yield [undefined, target.options];
|
||||
}
|
||||
|
||||
if (!target.configurations) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, options] of Object.entries(target.configurations)) {
|
||||
if (options !== undefined) {
|
||||
yield [name, options];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,111 @@
|
||||
import { readJson, type Tree } from '@nx/devkit';
|
||||
import type { PackageJson } from 'nx/src/utils/package-json';
|
||||
import { clean, coerce, major } from 'semver';
|
||||
|
||||
export const nxVersion = require('../../package.json').version;
|
||||
export const eslintPluginCypressVersion = '^3.5.0';
|
||||
export const typesNodeVersion = '18.16.9';
|
||||
export const cypressViteDevServerVersion = '^2.2.1';
|
||||
export const cypressVersion = '^13.13.0';
|
||||
export const cypressWebpackVersion = '^3.8.0';
|
||||
export const viteVersion = '~5.0.0';
|
||||
export const cypressViteDevServerVersion = '^6.0.3';
|
||||
export const cypressVersion = '^14.2.1';
|
||||
export const cypressWebpackVersion = '^4.0.2';
|
||||
export const viteVersion = '^6.0.0';
|
||||
export const htmlWebpackPluginVersion = '^5.5.0';
|
||||
|
||||
const latestVersions: Omit<
|
||||
typeof import('./versions'),
|
||||
'versions' | 'getInstalledCypressMajorVersion' | 'assertMinimumCypressVersion'
|
||||
> = {
|
||||
nxVersion,
|
||||
eslintPluginCypressVersion,
|
||||
typesNodeVersion,
|
||||
cypressViteDevServerVersion,
|
||||
cypressVersion,
|
||||
cypressWebpackVersion,
|
||||
viteVersion,
|
||||
htmlWebpackPluginVersion,
|
||||
};
|
||||
|
||||
export function versions(
|
||||
tree: Tree,
|
||||
cypressMajorVersion = getInstalledCypressMajorVersion(tree)
|
||||
) {
|
||||
if (!cypressMajorVersion) {
|
||||
return latestVersions;
|
||||
}
|
||||
|
||||
if (cypressMajorVersion > 14) {
|
||||
throw new Error(`Unsupported Cypress version: ${cypressVersion}`);
|
||||
}
|
||||
|
||||
if (cypressMajorVersion === 14) {
|
||||
return latestVersions;
|
||||
}
|
||||
|
||||
return {
|
||||
nxVersion,
|
||||
eslintPluginCypressVersion: '^3.5.0',
|
||||
typesNodeVersion: '18.16.9',
|
||||
cypressViteDevServerVersion: '^2.2.1',
|
||||
cypressVersion: '^13.13.0',
|
||||
cypressWebpackVersion: '^3.8.0',
|
||||
viteVersion: '~5.0.0',
|
||||
htmlWebpackPluginVersion: '^5.5.0',
|
||||
};
|
||||
}
|
||||
|
||||
export function getInstalledCypressMajorVersion(tree?: Tree): number | null {
|
||||
try {
|
||||
let version: string | null;
|
||||
|
||||
if (tree) {
|
||||
version = getCypressVersionFromTree(tree);
|
||||
} else {
|
||||
version = getCypressVersionFromFileSystem();
|
||||
}
|
||||
|
||||
return version ? major(version) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMinimumCypressVersion(
|
||||
minVersion: number,
|
||||
tree?: Tree
|
||||
): void {
|
||||
const version = getInstalledCypressMajorVersion(tree);
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCypressVersionFromTree(tree: Tree): string | null {
|
||||
const packageJson = readJson(tree, 'package.json');
|
||||
const installedVersion =
|
||||
packageJson.devDependencies?.cypress ?? packageJson.dependencies?.cypress;
|
||||
|
||||
if (!installedVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (installedVersion === 'latest' || installedVersion === 'next') {
|
||||
return clean(cypressVersion) ?? coerce(cypressVersion)?.version;
|
||||
}
|
||||
|
||||
return clean(installedVersion) ?? coerce(installedVersion)?.version;
|
||||
}
|
||||
|
||||
function getCypressVersionFromFileSystem(): string | null {
|
||||
let packageJson: PackageJson | undefined;
|
||||
try {
|
||||
packageJson = <PackageJson>require('cypress/package.json');
|
||||
} catch {}
|
||||
|
||||
if (!packageJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { readJson, readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { cypressComponentConfiguration } from './cypress-component-configuration';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { setupTailwindGenerator } from '@nx/react';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { libraryGenerator } from '../library/library';
|
||||
import { setupTailwindGenerator } from '@nx/react';
|
||||
import { Linter } from '@nx/eslint';
|
||||
import { cypressComponentConfiguration } from './cypress-component-configuration';
|
||||
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual<any>('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('cypress-component-configuration generator', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressMajorVersion: jest.Mock<
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
// TODO(@leosvelperez): Turn this back to adding the plugin
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
mockedInstalledCypressMajorVersion.mockReturnValue(14);
|
||||
});
|
||||
|
||||
it('should setup nextjs app', async () => {
|
||||
@ -57,7 +66,7 @@ describe('cypress-component-configuration generator', () => {
|
||||
`);
|
||||
expect(tree.read('demo/cypress/support/component.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react18';
|
||||
"import { mount } from 'cypress/react';
|
||||
import './styles.ct.css';
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
@ -104,6 +113,56 @@ describe('cypress-component-configuration generator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should import "mount" from "cypress/react18" when cypress version is lower than v14', async () => {
|
||||
mockedInstalledCypressMajorVersion.mockReturnValue(13);
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'demo',
|
||||
style: 'css',
|
||||
});
|
||||
|
||||
await cypressComponentConfiguration(tree, {
|
||||
generateTests: true,
|
||||
project: 'demo',
|
||||
});
|
||||
|
||||
expect(tree.read('demo/cypress/support/component.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react18';
|
||||
import './styles.ct.css';
|
||||
// ***********************************************************
|
||||
// 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';
|
||||
|
||||
// add component testing only related command here, such as mount
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add styles setup in app', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'demo',
|
||||
@ -131,7 +190,7 @@ describe('cypress-component-configuration generator', () => {
|
||||
it('should setup nextjs lib', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'demo',
|
||||
linter: Linter.EsLint,
|
||||
linter: 'eslint',
|
||||
style: 'css',
|
||||
unitTestRunner: 'jest',
|
||||
component: true,
|
||||
|
||||
@ -91,6 +91,10 @@ async function addFiles(
|
||||
const { addMountDefinition, addDefaultCTConfig } = await import(
|
||||
'@nx/cypress/src/utils/config'
|
||||
);
|
||||
const { getInstalledCypressMajorVersion } = await import(
|
||||
'@nx/cypress/src/utils/versions'
|
||||
);
|
||||
const installedCypressMajorVersion = getInstalledCypressMajorVersion(tree);
|
||||
|
||||
const ctFile = joinPathFragments(
|
||||
projectConfig.root,
|
||||
@ -102,9 +106,11 @@ async function addFiles(
|
||||
const updatedCommandFile = await addMountDefinition(
|
||||
tree.read(ctFile, 'utf-8')
|
||||
);
|
||||
const moduleSpecifier =
|
||||
installedCypressMajorVersion >= 14 ? 'cypress/react' : 'cypress/react18';
|
||||
tree.write(
|
||||
ctFile,
|
||||
`import { mount } from 'cypress/react18';\nimport './styles.ct.css';\n${updatedCommandFile}`
|
||||
`import { mount } from '${moduleSpecifier}';\nimport './styles.ct.css';\n${updatedCommandFile}`
|
||||
);
|
||||
|
||||
const cyFile = joinPathFragments(projectConfig.root, 'cypress.config.ts');
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
@ -13,12 +13,15 @@ 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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('next library', () => {
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
it('should use @nx/next images.d.ts file', async () => {
|
||||
const baseOptions: Schema = {
|
||||
directory: '',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '@nx/eslint';
|
||||
@ -8,7 +8,10 @@ import { applicationGenerator } from './application';
|
||||
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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
describe('react app generator (legacy)', () => {
|
||||
let appTree: Tree;
|
||||
let schema: Schema = {
|
||||
@ -22,8 +25,8 @@ describe('react app generator (legacy)', () => {
|
||||
addPlugin: false,
|
||||
};
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
detectPackageManager,
|
||||
getPackageManagerCommand,
|
||||
@ -20,7 +20,10 @@ import { Schema } from './schema';
|
||||
const { load } = require('@zkochan/js-yaml');
|
||||
// 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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
let projectGraph: ProjectGraph;
|
||||
jest.mock('@nx/devkit', () => {
|
||||
@ -49,8 +52,8 @@ describe('app', () => {
|
||||
addPlugin: true,
|
||||
};
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
beforeEach(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
appTree = createTreeWithEmptyWorkspace();
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '@nx/eslint';
|
||||
import libraryGenerator from '../library/library';
|
||||
import { componentTestGenerator } from './component-test';
|
||||
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions');
|
||||
describe(componentTestGenerator.name, () => {
|
||||
let tree: Tree;
|
||||
let mockedAssertMinimumCypressVersion: jest.Mock<
|
||||
|
||||
@ -23,7 +23,7 @@ export async function componentTestGenerator(
|
||||
) {
|
||||
ensurePackage('@nx/cypress', nxVersion);
|
||||
const { assertMinimumCypressVersion } = await import(
|
||||
'@nx/cypress/src/utils/cypress-version'
|
||||
'@nx/cypress/src/utils/versions'
|
||||
);
|
||||
assertMinimumCypressVersion(10);
|
||||
// normalize any windows paths
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
logger,
|
||||
readJson,
|
||||
@ -14,14 +14,17 @@ 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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('component', () => {
|
||||
let appTree: Tree;
|
||||
let projectName: string;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
DependencyType,
|
||||
ProjectGraph,
|
||||
@ -7,6 +7,11 @@ import {
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '@nx/eslint';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { componentGenerator } from '../component/component';
|
||||
import { libraryGenerator } from '../library/library';
|
||||
import { cypressComponentConfigGenerator } from './cypress-component-configuration';
|
||||
|
||||
let projectGraph: ProjectGraph;
|
||||
jest.mock('@nx/devkit', () => ({
|
||||
@ -16,14 +21,10 @@ jest.mock('@nx/devkit', () => ({
|
||||
.fn()
|
||||
.mockImplementation(async () => projectGraph),
|
||||
}));
|
||||
|
||||
import { Linter } from '@nx/eslint';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { componentGenerator } from '../component/component';
|
||||
import { libraryGenerator } from '../library/library';
|
||||
import { cypressComponentConfigGenerator } from './cypress-component-configuration';
|
||||
|
||||
jest.mock('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual<any>('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
// nested code imports graph from the repo, which might have innacurate graph version
|
||||
jest.mock('nx/src/project-graph/project-graph', () => ({
|
||||
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
|
||||
@ -32,20 +33,12 @@ jest.mock('nx/src/project-graph/project-graph', () => ({
|
||||
|
||||
describe('React:CypressComponentTestConfiguration', () => {
|
||||
let tree: Tree;
|
||||
let mockedAssertCypressVersion: jest.Mock<
|
||||
ReturnType<typeof assertMinimumCypressVersion>
|
||||
> = assertMinimumCypressVersion as never;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
// TODO(@jaysoo): Turn this back to adding the plugin
|
||||
let originalEnv: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.NX_ADD_PLUGINS;
|
||||
process.env.NX_ADD_PLUGINS = 'false';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NX_ADD_PLUGINS = originalEnv;
|
||||
});
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
@ -53,6 +46,14 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
nodes: {},
|
||||
dependencies: {},
|
||||
};
|
||||
|
||||
originalEnv = process.env.NX_ADD_PLUGINS;
|
||||
process.env.NX_ADD_PLUGINS = 'false';
|
||||
mockedInstalledCypressVersion.mockReturnValue(14);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NX_ADD_PLUGINS = originalEnv;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@ -60,8 +61,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should generate cypress config with vite', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -116,8 +115,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should generate cypress component test config with --build-target', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -184,7 +181,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should generate cypress component test config with project graph', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -250,7 +246,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should generate cypress component test config with webpack', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -315,7 +310,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
});
|
||||
it('should generate tests for existing tsx components', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -360,7 +354,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('should generate tests for existing js components', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -415,7 +408,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should throw error when an invalid --build-target is provided', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -470,8 +462,6 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
|
||||
it('should setup cypress config files correctly', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
@ -531,6 +521,95 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
"
|
||||
`);
|
||||
expect(tree.read('some-lib/cypress/support/component.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react';
|
||||
// ***********************************************************
|
||||
// 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';
|
||||
|
||||
// add component testing only related command here, such as mount
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should import "mount" from "cypress/react18" when cypress version is lower than v14', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(13);
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: true,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
directory: 'my-app',
|
||||
bundler: 'vite',
|
||||
});
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
directory: 'some-lib',
|
||||
skipFormat: true,
|
||||
skipTsConfig: false,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
component: true,
|
||||
});
|
||||
|
||||
projectGraph = {
|
||||
nodes: {
|
||||
'my-app': {
|
||||
name: 'my-app',
|
||||
type: 'app',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'my-app'),
|
||||
} as any,
|
||||
},
|
||||
'some-lib': {
|
||||
name: 'some-lib',
|
||||
type: 'lib',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'some-lib'),
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
'my-app': [
|
||||
{ type: DependencyType.static, source: 'my-app', target: 'some-lib' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await cypressComponentConfigGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
generateTests: false,
|
||||
buildTarget: 'my-app:build',
|
||||
});
|
||||
|
||||
expect(tree.read('some-lib/cypress/support/component.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { mount } from 'cypress/react18';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { FoundTarget } from '@nx/cypress/src/utils/find-target-options';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
joinPathFragments,
|
||||
@ -7,10 +8,9 @@ import {
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nx/devkit';
|
||||
import { nxVersion } from 'nx/src/utils/versions';
|
||||
import { getActualBundler, isComponent } from '../../../utils/ct-utils';
|
||||
import { componentTestGenerator } from '../../component-test/component-test';
|
||||
import type { CypressComponentConfigurationSchema } from '../schema';
|
||||
import { getActualBundler, isComponent } from '../../../utils/ct-utils';
|
||||
import { FoundTarget } from '@nx/cypress/src/utils/find-target-options';
|
||||
|
||||
export async function addFiles(
|
||||
tree: Tree,
|
||||
@ -18,11 +18,13 @@ export async function addFiles(
|
||||
options: CypressComponentConfigurationSchema,
|
||||
found: FoundTarget
|
||||
) {
|
||||
// must dyanmicaly import to prevent packages not using cypress from erroring out
|
||||
// must dynamicaly import to prevent packages not using cypress from erroring out
|
||||
// when importing react
|
||||
const { addMountDefinition, addDefaultCTConfig } = await import(
|
||||
'@nx/cypress/src/utils/config'
|
||||
const { addMountDefinition } = await import('@nx/cypress/src/utils/config');
|
||||
const { getInstalledCypressMajorVersion } = await import(
|
||||
'@nx/cypress/src/utils/versions'
|
||||
);
|
||||
const installedCypressMajorVersion = getInstalledCypressMajorVersion(tree);
|
||||
|
||||
// Specifically undefined to allow Remix workaround of passing an empty string
|
||||
const actualBundler = await getActualBundler(tree, options, found);
|
||||
@ -46,9 +48,11 @@ export async function addFiles(
|
||||
const updatedCommandFile = await addMountDefinition(
|
||||
tree.read(commandFile, 'utf-8')
|
||||
);
|
||||
const moduleSpecifier =
|
||||
installedCypressMajorVersion >= 14 ? 'cypress/react' : 'cypress/react18';
|
||||
tree.write(
|
||||
commandFile,
|
||||
`import { mount } from 'cypress/react18';\n${updatedCommandFile}`
|
||||
`import { mount } from '${moduleSpecifier}';\n${updatedCommandFile}`
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
getProjects,
|
||||
readJson,
|
||||
@ -18,12 +18,15 @@ import { Schema } from './schema';
|
||||
const { load } = require('@zkochan/js-yaml');
|
||||
// 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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
describe('lib', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
let defaultSchema: Schema = {
|
||||
directory: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import { getProjects, readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { applicationGenerator } from './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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
jest.mock('@nx/devkit', () => {
|
||||
return {
|
||||
...jest.requireActual('@nx/devkit'),
|
||||
@ -17,8 +20,8 @@ jest.mock('@nx/devkit', () => {
|
||||
describe('web app generator (legacy)', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
let originalEnv: string;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/src/utils/versions';
|
||||
import {
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
@ -18,7 +18,10 @@ import { Schema } from './schema';
|
||||
import { PackageManagerCommands } from 'nx/src/utils/package-manager';
|
||||
// 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('@nx/cypress/src/utils/cypress-version');
|
||||
jest.mock('@nx/cypress/src/utils/versions', () => ({
|
||||
...jest.requireActual('@nx/cypress/src/utils/versions'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
jest.mock('@nx/devkit', () => {
|
||||
return {
|
||||
...jest.requireActual('@nx/devkit'),
|
||||
@ -29,8 +32,8 @@ jest.mock('@nx/devkit', () => {
|
||||
describe('app', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
beforeEach(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
jest
|
||||
|
||||
@ -46,9 +46,11 @@ function check() {
|
||||
// which is @angular/core/testing. and the tests check for this
|
||||
'packages/cypress/src/migrations/update-15-1-0/cypress-11.spec.ts',
|
||||
'packages/cypress/src/migrations/update-15-1-0/cypress-11.ts',
|
||||
// this migration looks for projects depending on @angular/core, it doesn't require it
|
||||
// these migrations looks for projects depending on @angular/core, it doesn't require it
|
||||
'packages/cypress/src/migrations/update-16-4-0/warn-incompatible-angular-cypress.spec.ts',
|
||||
'packages/cypress/src/migrations/update-16-4-0/warn-incompatible-angular-cypress.ts',
|
||||
'packages/cypress/src/migrations/update-20-8-0/update-component-testing-mount-imports.spec.ts',
|
||||
'packages/cypress/src/migrations/update-20-8-0/update-component-testing-mount-imports.ts',
|
||||
];
|
||||
|
||||
const files = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user