@nx/cypress 23.3.0-pr.37068.f95e11e → 23.3.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/generators/component-configuration/component-configuration.js +23 -5
- package/dist/src/migrations/update-23-1-0/disable-webpack-ct-just-in-time-compile.js +2 -38
- package/dist/src/migrations/update-23-3-0/adapt-query-command-overwrites.md +74 -0
- package/dist/src/migrations/update-23-3-0/ai-instructions-for-cypress-16.md +220 -0
- package/dist/src/migrations/update-23-3-0/update-angular-zoneless-mount-import.d.ts +5 -0
- package/dist/src/migrations/update-23-3-0/update-angular-zoneless-mount-import.js +108 -0
- package/dist/src/migrations/update-23-3-0/update-angular-zoneless-mount-import.md +19 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-config-options.d.ts +5 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-config-options.js +228 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-config-options.md +36 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-query-command-overwrites.d.ts +10 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-query-command-overwrites.js +120 -0
- package/dist/src/migrations/update-23-3-0/update-cypress-16-query-command-overwrites.md +34 -0
- package/dist/src/migrations/update-23-3-0/upgrade-to-cypress-16.d.ts +8 -0
- package/dist/src/migrations/update-23-3-0/upgrade-to-cypress-16.js +231 -0
- package/dist/src/migrations/update-23-3-0/upgrade-to-cypress-16.md +27 -0
- package/dist/src/plugins/plugin.js +7 -5
- package/dist/src/utils/config.d.ts +8 -1
- package/dist/src/utils/config.js +33 -21
- package/dist/src/utils/migrations.d.ts +15 -1
- package/dist/src/utils/migrations.js +193 -18
- package/dist/src/utils/versions.d.ts +7 -5
- package/dist/src/utils/versions.js +58 -5
- package/migrations.json +47 -0
- package/package.json +7 -7
- package/readme-template.md +18 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](<>)
|
|
13
13
|
[](http://commitizen.github.io/cz-cli/)
|
|
14
14
|
[](https://go.nx.dev/community)
|
|
15
|
-
[](https://nx.dev/nx-cloud)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -23,7 +23,16 @@ async function componentConfigurationGeneratorInternal(tree, options) {
|
|
|
23
23
|
(0, internal_2.assertNotUsingTsSolutionSetup)(tree, 'cypress', 'component-configuration');
|
|
24
24
|
const tasks = [];
|
|
25
25
|
const opts = normalizeOptions(tree, options);
|
|
26
|
-
if (
|
|
26
|
+
if (opts.bundler === 'vite') {
|
|
27
|
+
(0, versions_1.assertViteSupportsInstalledCypress)(tree);
|
|
28
|
+
}
|
|
29
|
+
// Before init, which pins the latest cypress when none is declared. The
|
|
30
|
+
// set added here can be an older one that runs on the installed Vite.
|
|
31
|
+
const isCypressInstalled = !!(0, versions_1.getInstalledCypressMajorVersion)(tree);
|
|
32
|
+
if (!opts.skipPackageJson) {
|
|
33
|
+
tasks.push(updateDeps(tree, opts));
|
|
34
|
+
}
|
|
35
|
+
if (!isCypressInstalled) {
|
|
27
36
|
tasks.push(await (0, init_1.default)(tree, {
|
|
28
37
|
...opts,
|
|
29
38
|
skipFormat: true,
|
|
@@ -34,9 +43,6 @@ async function componentConfigurationGeneratorInternal(tree, options) {
|
|
|
34
43
|
? p === '@nx/cypress/plugin'
|
|
35
44
|
: p.plugin === '@nx/cypress/plugin');
|
|
36
45
|
const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, opts.project);
|
|
37
|
-
if (!opts.skipPackageJson) {
|
|
38
|
-
tasks.push(updateDeps(tree, opts));
|
|
39
|
-
}
|
|
40
46
|
addProjectFiles(tree, projectConfig, opts);
|
|
41
47
|
if (!hasPlugin || opts.addExplicitTargets) {
|
|
42
48
|
(0, deprecation_1.warnCypressExecutorGenerating)();
|
|
@@ -61,15 +67,27 @@ function normalizeOptions(tree, options) {
|
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
function updateDeps(tree, opts) {
|
|
64
|
-
const pkgVersions = (0, versions_1.
|
|
70
|
+
const pkgVersions = (0, versions_1.componentTestingVersions)(tree, opts.bundler);
|
|
65
71
|
const devDeps = {
|
|
66
72
|
cypress: pkgVersions.cypressVersion,
|
|
67
73
|
};
|
|
74
|
+
if (!(0, versions_1.getInstalledCypressVersion)(tree)) {
|
|
75
|
+
// Same as init: the cypress postinstall downloads the binary it needs.
|
|
76
|
+
(0, internal_1.acknowledgeBuildScripts)(tree, (0, devkit_1.detectPackageManager)(tree.root), {
|
|
77
|
+
cypress: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
68
80
|
if (opts.bundler === 'vite') {
|
|
69
81
|
devDeps['@cypress/vite-dev-server'] =
|
|
70
82
|
pkgVersions.cypressViteDevServerVersion;
|
|
71
83
|
}
|
|
72
84
|
else {
|
|
85
|
+
// @cypress/webpack-dev-server depends on tsx, which depends on esbuild,
|
|
86
|
+
// whose install script only validates the prebuilt binary that ships as an
|
|
87
|
+
// optional dependency.
|
|
88
|
+
(0, internal_1.acknowledgeBuildScripts)(tree, (0, devkit_1.detectPackageManager)(tree.root), {
|
|
89
|
+
esbuild: false,
|
|
90
|
+
});
|
|
73
91
|
devDeps['@cypress/webpack-dev-server'] = pkgVersions.cypressWebpackVersion;
|
|
74
92
|
devDeps['html-webpack-plugin'] = pkgVersions.htmlWebpackPluginVersion;
|
|
75
93
|
}
|
|
@@ -46,7 +46,8 @@ function isWebpackComponentTesting(component) {
|
|
|
46
46
|
if (usesNxComponentTestingPreset(component)) {
|
|
47
47
|
// @nx/remix is the only vite-based Nx CT preset; resolve the import bound
|
|
48
48
|
// to the call so an unrelated remix reference elsewhere can't misclassify.
|
|
49
|
-
return getComponentTestingPresetImport(component) !==
|
|
49
|
+
return ((0, migrations_1.getComponentTestingPresetImport)(component.getSourceFile()) !==
|
|
50
|
+
NX_VITE_CT_PRESET);
|
|
50
51
|
}
|
|
51
52
|
// Hand-written config: migrate only when it has an inline devServer framework.
|
|
52
53
|
return hasInlineDevServerFramework(component);
|
|
@@ -71,43 +72,6 @@ function getComponentProperty(component, name) {
|
|
|
71
72
|
ts ??= (0, internal_1.ensureTypescript)();
|
|
72
73
|
return (0, tsquery_1.query)(component, 'PropertyAssignment').filter((property) => ts.isIdentifier(property.name) && property.name.text === name);
|
|
73
74
|
}
|
|
74
|
-
// Resolves the module specifier that binds the `nxComponentTestingPreset`
|
|
75
|
-
// identifier used inside `component`, covering both ESM `import` and CJS
|
|
76
|
-
// `require` cypress configs. Returns null when no such binding is found.
|
|
77
|
-
function getComponentTestingPresetImport(component) {
|
|
78
|
-
ts ??= (0, internal_1.ensureTypescript)();
|
|
79
|
-
for (const statement of component.getSourceFile().statements) {
|
|
80
|
-
if (ts.isImportDeclaration(statement) && statement.importClause) {
|
|
81
|
-
const namedBindings = statement.importClause.namedBindings;
|
|
82
|
-
if (namedBindings &&
|
|
83
|
-
ts.isNamedImports(namedBindings) &&
|
|
84
|
-
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
85
|
-
namedBindings.elements.some((element) => element.name.text === 'nxComponentTestingPreset')) {
|
|
86
|
-
return statement.moduleSpecifier.text;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (ts.isVariableStatement(statement)) {
|
|
90
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
91
|
-
const initializer = declaration.initializer;
|
|
92
|
-
if (!initializer ||
|
|
93
|
-
!ts.isCallExpression(initializer) ||
|
|
94
|
-
!ts.isIdentifier(initializer.expression) ||
|
|
95
|
-
initializer.expression.text !== 'require' ||
|
|
96
|
-
!ts.isObjectBindingPattern(declaration.name)) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const moduleSpecifier = initializer.arguments[0];
|
|
100
|
-
if (moduleSpecifier &&
|
|
101
|
-
ts.isStringLiteral(moduleSpecifier) &&
|
|
102
|
-
declaration.name.elements.some((element) => ts.isIdentifier(element.name) &&
|
|
103
|
-
element.name.text === 'nxComponentTestingPreset')) {
|
|
104
|
-
return moduleSpecifier.text;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
75
|
function setsJustInTimeCompile(component) {
|
|
112
76
|
return getComponentProperty(component, 'justInTimeCompile').length > 0;
|
|
113
77
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Cypress 16 Query Overwrite Migration Instructions for LLM
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Cypress 16 turned `cy.getCookie()`, `cy.getCookies()`, `cy.getAllCookies()`, `cy.getAllLocalStorage()` and `cy.getAllSessionStorage()` into queries. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`. A deterministic pre-pass already renamed `Cypress.Commands.overwrite()` to `Cypress.Commands.overwriteQuery()` for those five names. Your job is to make each renamed callback follow the query contract. Do not touch overwrites of other commands.
|
|
6
|
+
|
|
7
|
+
## Pre-Migration Checklist
|
|
8
|
+
|
|
9
|
+
Confirm both conditions before changing anything. If either fails, make no changes and stop.
|
|
10
|
+
|
|
11
|
+
1. `<files_changed>` or `<advisory_context>` lists at least one file with a renamed `Cypress.Commands.overwriteQuery()` call. Search the workspace for `Cypress.Commands.overwriteQuery(` if the wrapper sections are absent.
|
|
12
|
+
2. The workspace is on Cypress 16 or later (`cypress` in `package.json`).
|
|
13
|
+
|
|
14
|
+
## Step 1: Verify the pre-pass
|
|
15
|
+
|
|
16
|
+
Open each listed file and confirm the call reads `Cypress.Commands.overwriteQuery('<name>', ...)`. Do not re-apply the rename.
|
|
17
|
+
|
|
18
|
+
## Step 2: Adapt the callback to the query contract
|
|
19
|
+
|
|
20
|
+
`overwriteQuery` calls the callback with the command as `this`, the original query function as the first argument, and then the arguments the test passed. The callback must return a function that takes the subject. Cypress calls that returned function, possibly many times while retrying, to compute the result. It must not return a chainable, call `cy.*` commands or use `.then()`.
|
|
21
|
+
|
|
22
|
+
The original query reads `this` as well, so every callback must be a `function` expression that calls `originalFn.call(this, ...)`. An arrow callback, or a plain `originalFn(...)` call, fails to typecheck (TS2684, `QueryFnWithOriginalFn` declares `this: Command`) and throws at runtime. Convert arrow callbacks even when they only forward their arguments:
|
|
23
|
+
|
|
24
|
+
**Before:**
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
Cypress.Commands.overwriteQuery('getCookie', (originalFn, name, options) => {
|
|
28
|
+
return originalFn(name, { ...options, log: false });
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**After:**
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
Cypress.Commands.overwriteQuery(
|
|
36
|
+
'getCookie',
|
|
37
|
+
function (originalFn, name, options) {
|
|
38
|
+
return originalFn.call(this, name, { ...options, log: false });
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rewrite callbacks that post-process the result. Get the inner function from `originalFn`, then return a function that calls it with the subject and transforms the value:
|
|
44
|
+
|
|
45
|
+
**Before:**
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
Cypress.Commands.overwrite('getCookies', (originalFn, options) => {
|
|
49
|
+
return originalFn(options).then((cookies) =>
|
|
50
|
+
cookies.filter((cookie) => cookie.name.startsWith('app.'))
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**After:**
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
Cypress.Commands.overwriteQuery('getCookies', function (originalFn, options) {
|
|
59
|
+
const innerFn = originalFn.call(this, options);
|
|
60
|
+
return (subject) =>
|
|
61
|
+
innerFn(subject).filter((cookie) => cookie.name.startsWith('app.'));
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Callbacks that call other `cy.*` commands, use `cy.wrap()`, or run asynchronous work cannot become queries. Move that logic into a separate custom command added with `Cypress.Commands.add()` and update the specs that relied on the overwritten behavior.
|
|
66
|
+
|
|
67
|
+
## Post-Migration Validation
|
|
68
|
+
|
|
69
|
+
1. Run the Cypress projects that own the changed files, for example `npx nx run <project>:e2e` or `npx nx run <project>:component-test`.
|
|
70
|
+
2. Fix failures caused by this migration and re-run until green.
|
|
71
|
+
|
|
72
|
+
## Nx-Specific Notes
|
|
73
|
+
|
|
74
|
+
The support files live under each Cypress project's `src/support/` (or `cypress/support/` for component testing), or in a shared library the support file imports. Do not change the Cypress config or Nx target configuration for this migration.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Cypress 15 -> 16 Migration Instructions for LLM
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Migrate the workspace's Cypress projects from Cypress 15 to 16. Cypress 16 removed `Cypress.env()`, `cy.exec()` and `.end()`, stopped accepting `env` overrides in test configuration, turned the cookie and storage getters into queries, renamed or removed several config options, dropped CoffeeScript support, and requires Node 22, Vite 8 (Vite component testing), Angular 21 (Angular component testing) and Next.js 15.0.4 (component testing on Cypress's `next` dev server framework, which the Nx preset does not use). Chrome, Chromium and Edge now use the native browser network, which changes a few `cy.intercept()` details. Deterministic migrations already handled the config options in the Cypress config files, the `cypress/angular-zoneless` import and the `Cypress.Commands.overwrite()` renames; verify those, do not redo them. Config options held in another file reach the config through a spread and are handled in Step 9. This runbook covers the remaining source changes. Do not change Nx target configuration, the `nxE2EPreset` / `nxComponentTestingPreset` calls, or `webServerCommands`; those keep working on Cypress 16.
|
|
6
|
+
|
|
7
|
+
## Pre-Migration Checklist
|
|
8
|
+
|
|
9
|
+
1. Confirm `cypress` in `package.json` resolves to 16 or later. If it does not, make no changes and stop.
|
|
10
|
+
2. Confirm Node is 22, 24, or 26+ (`node -v`). Cypress 16 declares `engines` `^22.0.0 || ^24.0.0 || >=26.0.0` and refuses to install on Node 20 or 25.
|
|
11
|
+
3. List the Cypress projects: every project with a `cypress.config.{ts,js,mjs,cjs}` file. Search their spec and support files for the strings in the steps below.
|
|
12
|
+
4. Review `<advisory_context>` from the deterministic migrations: each entry (a non-literal `experimentalFastVisibility`, a removed `execTimeout`, a removed `experimentalSourceRewriting: true`, an unresolved spread, a renamed `overwriteQuery` callback) is pending work for the matching step.
|
|
13
|
+
|
|
14
|
+
## Step 1: Replace `Cypress.env()`
|
|
15
|
+
|
|
16
|
+
`Cypress.env()` throws in Cypress 16 and its typings are gone. Values now come from two sources:
|
|
17
|
+
|
|
18
|
+
- `cy.env([...keys])` reads `env` values (from `cypress.env.json`, `CYPRESS_*` variables, `--env`, the `env` config key and the `env` executor option). It is asynchronous, read-only, and yields an object with only the requested keys.
|
|
19
|
+
- `Cypress.expose(key)` reads `expose` values synchronously. `expose` is for non-sensitive values set through the `expose` config key, the `--expose` CLI flag, `Cypress.expose(key, value)` at runtime, or the `expose` test configuration.
|
|
20
|
+
|
|
21
|
+
Classify each value: sensitive values (keys, tokens, passwords, credentials) stay in `env` and are read with `cy.env()`; public values (feature flags, API versions, public URLs) can move to `expose` and be read with `Cypress.expose()`. Reading a value inside a `cy.origin()` callback follows the same rule.
|
|
22
|
+
|
|
23
|
+
**Before:**
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const apiUrl = Cypress.env('API_URL');
|
|
27
|
+
cy.request(`${apiUrl}/health`);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**After:**
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
cy.env(['API_URL']).then(({ API_URL }) => {
|
|
34
|
+
cy.request(`${API_URL}/health`);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Do not assert on the object `cy.env()` yields with `.should()`; the assertion prints the value in the command log. Derive a boolean and assert on that instead.
|
|
39
|
+
|
|
40
|
+
A `Cypress.env()` call outside a test body (a top-level constant in a support file, a `Cypress.env('x', value)` write) needs restructuring: read it inside `before()`/`beforeEach()` with `cy.env()` and store the result on an alias, move the value to `expose` and read it with `Cypress.expose()`, or for runtime writes store the value through a `cy.task()` registered in `setupNodeEvents`.
|
|
41
|
+
|
|
42
|
+
Plugins that call `Cypress.env()` throw on Cypress 16. Update them to the versions that use the new API: `@cypress/code-coverage` 4, `@cypress/grep` 6, `@badeball/cypress-cucumber-preprocessor` 27 (options move from `--env` to `--expose`), and the community plugins listed in the Cypress guide (https://docs.cypress.io/app/references/migration-guide#Migrate-plugins-that-use-Cypressenv). Move each plugin's options from `env` to `expose` as its changelog says.
|
|
43
|
+
|
|
44
|
+
## Step 2: Remove `env` overrides in test configuration
|
|
45
|
+
|
|
46
|
+
`env` can no longer be set in the `it`/`describe` configuration object; Cypress 16 fails the test. Move the values to `expose` and read them with `Cypress.expose()`. Apply the Step 1 classification first: a sensitive value (a key, token, password or credential) must not move to `expose`, which is for non-sensitive values; keep it in `env` (`cypress.env.json`, `CYPRESS_*` or `--env`) and read it with `cy.env()` inside the test.
|
|
47
|
+
|
|
48
|
+
**Before:**
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
it('uses the sandbox account', { env: { ACCOUNT: 'sandbox' } }, () => {
|
|
52
|
+
cy.visit(`/accounts/${Cypress.env('ACCOUNT')}`);
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**After:**
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
it('uses the sandbox account', { expose: { ACCOUNT: 'sandbox' } }, () => {
|
|
60
|
+
cy.visit(`/accounts/${Cypress.expose('ACCOUNT')}`);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Step 3: Replace `cy.exec()` with `cy.task()`
|
|
65
|
+
|
|
66
|
+
`cy.exec()` throws in Cypress 16. Run the command from a `task` registered in `setupNodeEvents` and call it with `cy.task()`. A task returns a value or `null` and times out after 60 seconds; set `taskTimeout` when a former `execTimeout` was higher. Spawn external programs with `execFileSync` and an argument array so no shell is involved. Register one task per command; never a generic task that runs a command string the spec passes in. Values the spec used to template into the command (an id, a file name) travel as task arguments and become entries of the `execFileSync` argument array. A Cypress config that uses `nxE2EPreset` must keep calling the preset's `setupNodeEvents` (see the Nx notes below).
|
|
67
|
+
|
|
68
|
+
**Before:**
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
cy.exec('npm run db:seed');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**After:**
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// cypress.config.ts
|
|
78
|
+
import { execFileSync } from 'node:child_process';
|
|
79
|
+
|
|
80
|
+
const preset = nxE2EPreset(import.meta.url, { cypressDir: 'src' });
|
|
81
|
+
|
|
82
|
+
export default defineConfig({
|
|
83
|
+
e2e: {
|
|
84
|
+
...preset,
|
|
85
|
+
async setupNodeEvents(on, config) {
|
|
86
|
+
on('task', {
|
|
87
|
+
seedDb() {
|
|
88
|
+
execFileSync('npm', ['run', 'db:seed'], { stdio: 'inherit' });
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return preset.setupNodeEvents(on, config);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// spec
|
|
98
|
+
cy.task('seedDb');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Step 4: Remove `.end()`
|
|
102
|
+
|
|
103
|
+
`.end()` throws in Cypress 16. Delete the call; the next `cy.*` command already starts a new chain.
|
|
104
|
+
|
|
105
|
+
**Before:**
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
cy.get('[data-test=list]').find('li').should('have.length', 3).end();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**After:**
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
cy.get('[data-test=list]').find('li').should('have.length', 3);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Step 5: Move runtime `Cypress.config()` writes to test configuration
|
|
118
|
+
|
|
119
|
+
`blockHosts`, `viewportWidth` and `viewportHeight` can no longer be set with `Cypress.config()` while a test runs; Cypress 16 throws. Set them in the `describe`/`it` configuration object, or use `cy.viewport()` for the viewport. Calls at the top level of a spec or support file are unchanged.
|
|
120
|
+
|
|
121
|
+
**Before:**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
it('renders on mobile', () => {
|
|
125
|
+
Cypress.config('viewportWidth', 375);
|
|
126
|
+
cy.visit('/');
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**After:**
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
it('renders on mobile', { viewportWidth: 375 }, () => {
|
|
134
|
+
cy.visit('/');
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Step 6: Adapt cookie and storage reads to queries
|
|
139
|
+
|
|
140
|
+
`cy.getCookie()`, `cy.getCookies()`, `cy.getAllCookies()`, `cy.getAllLocalStorage()` and `cy.getAllSessionStorage()` are queries: they retry assertions chained with `.should()` and follow `defaultCommandTimeout` (4000ms) instead of `responseTimeout` (30000ms). A `.then()` callback still runs once and does not retry. Change nothing unless a test fails; then move the assertion from `.then()` onto `.should()` so it retries, or pass `{ timeout }` when a cookie read needs longer than 4 seconds.
|
|
141
|
+
|
|
142
|
+
The deterministic migration renamed `Cypress.Commands.overwrite()` to `overwriteQuery()` for those five names; `<advisory_context>` lists each renamed call. Do not re-apply the rename. Adapt each renamed callback to the query contract: `overwriteQuery` calls the callback with the command as `this`, the original query function as the first argument, and then the arguments the test passed, and the callback must return a function that takes the subject. Cypress calls that returned function, possibly many times while retrying, to compute the result. It must not return a chainable, call `cy.*` commands or use `.then()`. The original query reads `this` as well, so every callback must be a `function` expression that calls `originalFn.call(this, ...)`. An arrow callback, or a plain `originalFn(...)` call, fails to typecheck (TS2684, `QueryFnWithOriginalFn` declares `this: Command`) and throws at runtime. Convert arrow callbacks even when they only forward their arguments:
|
|
143
|
+
|
|
144
|
+
**Before:**
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
Cypress.Commands.overwriteQuery('getCookie', (originalFn, name, options) => {
|
|
148
|
+
return originalFn(name, { ...options, log: false });
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**After:**
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
Cypress.Commands.overwriteQuery(
|
|
156
|
+
'getCookie',
|
|
157
|
+
function (originalFn, name, options) {
|
|
158
|
+
return originalFn.call(this, name, { ...options, log: false });
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Rewrite callbacks that post-process the result. Get the inner function from `originalFn`, then return a function that calls it with the subject and transforms the value:
|
|
164
|
+
|
|
165
|
+
**Before:**
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
Cypress.Commands.overwriteQuery('getCookies', (originalFn, options) => {
|
|
169
|
+
return originalFn(options).then((cookies) =>
|
|
170
|
+
cookies.filter((cookie) => cookie.name.startsWith('app.'))
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**After:**
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
Cypress.Commands.overwriteQuery('getCookies', function (originalFn, options) {
|
|
179
|
+
const innerFn = originalFn.call(this, options);
|
|
180
|
+
return (subject) =>
|
|
181
|
+
innerFn(subject).filter((cookie) => cookie.name.startsWith('app.'));
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Callbacks that call other `cy.*` commands, use `cy.wrap()`, or run asynchronous work cannot become queries. Move that logic into a separate custom command added with `Cypress.Commands.add()` and update the specs that relied on the overwritten behavior.
|
|
186
|
+
|
|
187
|
+
## Step 7: Review behavior changes that need no code by default
|
|
188
|
+
|
|
189
|
+
Flag these, and change code only when a test fails:
|
|
190
|
+
|
|
191
|
+
- Native browser network in Chrome, Chromium and Edge: `cy.intercept()` no longer reports `req.httpVersion`, request `content-length` or response `content-encoding`; revalidated responses report `200` instead of `304`; `responseTimeout` does not bound response handlers (use a `timeout` on `cy.wait()`); the browser validates the application's own TLS certificate. Rewrite affected assertions to describe the application rather than the transport. Do not set `forceHttp1: true` to make a suite pass; it is deprecated at introduction.
|
|
192
|
+
- `visibilityStrategy` defaults to `'modern'`: assertions that relied on legacy-only visibility semantics (ancestor `overflow` clipping, transform-based hiding, coverage of fixed elements) may change. Rewrite them; keep `visibilityStrategy: 'legacy'` only as a temporary aid.
|
|
193
|
+
- `keystrokeDelay` defaults to `0` instead of `10`. A test that depended on the implicit delay can restore it with `keystrokeDelay: 10` in the Cypress config, `Cypress.Keyboard.defaults({ keystrokeDelay: 10 })`, or `{ delay: 10 }` on the `.type()` call.
|
|
194
|
+
- `manageBrowserMemory` defaults to `true`. The deterministic migration turned `experimentalMemoryManagement: false` into `manageBrowserMemory: false` in the config file, so an opt-out there survives; one held in another file is handled in Step 9.
|
|
195
|
+
- `experimentalSourceRewriting` is gone. Only an application that pins resources with Subresource Integrity needs `removeSRIAttributes: true` in its place.
|
|
196
|
+
- CoffeeScript is no longer compiled. Convert `.coffee` specs, support files and fixtures to JavaScript or TypeScript.
|
|
197
|
+
|
|
198
|
+
## Step 8: Component testing
|
|
199
|
+
|
|
200
|
+
- Angular: `cypress/angular` mounts with zoneless change detection on Cypress 16, for zone-based apps too. A template no longer re-renders after a plain property mutation on the component instance, so state that tests assert on in the DOM must be signal-based. The `autoSpyOutputs` and `autoDetectChanges` mount options no longer exist: remove `autoDetectChanges`, and replace `autoSpyOutputs: true` with explicit spies passed through `componentProperties` (for example `{ saved: createOutputSpy('savedSpy') }`). Cypress officially supports Angular 21 and later; Angular 20 runs with a warning. The harness bootstraps through `@angular/platform-browser/testing`, so `@angular/platform-browser-dynamic` can be uninstalled when nothing else imports it. The deprecated `@cypress/angular-zoneless` npm package was removed and its imports rewritten to `cypress/angular` by the deterministic migration; remove any leftover reference to it.
|
|
201
|
+
- Vite: Cypress 16 component testing runs on Vite 8 only, and `@cypress/vite-dev-server` 8 declares it as a peer. The deterministic migration kept the workspace on Cypress 15 only when a Vite component testing config, or a config whose bundler could not be determined statically, resolved an installed Vite below 8. It cannot see a `vite` that is declared but not installed. It also does not check a Cypress config with a custom name that no `@nx/cypress:cypress` target references, such as one run through `nx:run-commands`. Bump any `vite` below 8 in the root or a project `package.json`, and any `@vitejs/*` plugin that still pins an older major. A Vite component testing config the migration did not check must resolve Vite 8 from its directory.
|
|
202
|
+
- Next.js: Cypress's `next` dev server framework requires Next.js 15.0.4 or later. The `@nx/next/plugins/component-testing` preset runs the `react` framework over a webpack config Nx builds, so a Cypress config on that preset does not use the dropped adapter. Only a hand-written config with `devServer: { framework: 'next' }` needs the Next.js bump.
|
|
203
|
+
- Electron: the bundled Electron browser is deprecated. Cypress prints a warning; set `defaultBrowser: 'chrome'` (or another installed browser) in the Cypress config, or pass `--browser`, when the warning matters in CI.
|
|
204
|
+
|
|
205
|
+
## Step 9: Migrate config options held outside the config file
|
|
206
|
+
|
|
207
|
+
The deterministic migration rewrote `experimentalMemoryManagement`, `experimentalFastVisibility`, `experimentalSourceRewriting`, `allowCypressEnv` and `execTimeout` in each `cypress.config.{ts,js,mjs,cjs}` file, including objects of the same file spread into the config. It cannot follow a spread of an import or a function call. For each spread of that kind in a Cypress config, other than `nxE2EPreset(...)` and `nxComponentTestingPreset(...)`, open the source of the spread object and apply the same changes there: rename `experimentalMemoryManagement` to `manageBrowserMemory` with the same value, replace `experimentalFastVisibility: true` with `visibilityStrategy: 'modern'` and `false` with `visibilityStrategy: 'legacy'`, and delete the three removed options. Search the workspace for the five option names to catch objects the config reaches through more than one file.
|
|
208
|
+
|
|
209
|
+
## Post-Migration Validation
|
|
210
|
+
|
|
211
|
+
1. Run typecheck on the Cypress projects (`npx nx run-many -t typecheck -p <projects>` or `npx tsc -p <project>/tsconfig.json --noEmit`). Removed APIs surface as type errors first.
|
|
212
|
+
2. Run `npx cypress verify` to confirm the Cypress 16 binary is installed.
|
|
213
|
+
3. Run the Cypress projects: `npx nx run-many -t e2e,component-test -p <projects>`. The run must start without removed-option warnings.
|
|
214
|
+
4. Fix failures caused by this migration and re-run until green.
|
|
215
|
+
|
|
216
|
+
## Nx-Specific Notes
|
|
217
|
+
|
|
218
|
+
- Keep `nxE2EPreset` and `nxComponentTestingPreset` in place. The preset reads `webServerCommands` through Cypress `env` in `setupNodeEvents`, which Nx still passes with `--env`; do not move those values to `expose`.
|
|
219
|
+
- When adding `setupNodeEvents` to a config that spreads `nxE2EPreset(...)`, call the preset's `setupNodeEvents` from yours and return its result (see https://nx.dev/docs/kb/cypress-setup-node-events).
|
|
220
|
+
- The `env` option of the `@nx/cypress:cypress` executor and `--env` in inferred targets keep working; only how specs read the values changed.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = updateAngularZonelessMountImport;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const internal_1 = require("@nx/js/internal");
|
|
6
|
+
const tsquery_1 = require("@phenomnomnominal/tsquery");
|
|
7
|
+
const migrations_1 = require("../../utils/migrations");
|
|
8
|
+
// Cypress 16 dropped the `cypress/angular-zoneless` export and deprecated the
|
|
9
|
+
// standalone npm package of the same harness; `cypress/angular` is zoneless there.
|
|
10
|
+
const OLD_SPECIFIERS = new Set([
|
|
11
|
+
'cypress/angular-zoneless',
|
|
12
|
+
'@cypress/angular-zoneless',
|
|
13
|
+
]);
|
|
14
|
+
const DEPRECATED_PACKAGE = '@cypress/angular-zoneless';
|
|
15
|
+
const NEW_SPECIFIER = 'cypress/angular';
|
|
16
|
+
let ts;
|
|
17
|
+
async function updateAngularZonelessMountImport(tree) {
|
|
18
|
+
let wereFilesMigrated = false;
|
|
19
|
+
const shadowedRequires = [];
|
|
20
|
+
// Shared support libraries can hold the import, so the whole workspace is
|
|
21
|
+
// scanned rather than the Cypress project roots.
|
|
22
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
|
|
23
|
+
if (!isJsTsFile(filePath)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const originalContent = tree.read(filePath, 'utf-8');
|
|
27
|
+
if (!originalContent.includes('angular-zoneless')) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const sourceFile = (0, migrations_1.parseSourceFile)(filePath, originalContent);
|
|
31
|
+
const specifiers = findModuleSpecifiers(sourceFile);
|
|
32
|
+
// A file with its own `require` value is not calling the CommonJS loader.
|
|
33
|
+
const shadowsRequire = specifiers.some(isRequireArgument) &&
|
|
34
|
+
(0, migrations_1.hasLocalValueBinding)(sourceFile, 'require');
|
|
35
|
+
if (shadowsRequire) {
|
|
36
|
+
shadowedRequires.push(filePath);
|
|
37
|
+
}
|
|
38
|
+
const changes = specifiers
|
|
39
|
+
.filter((specifier) => !shadowsRequire || !isRequireArgument(specifier))
|
|
40
|
+
.map((specifier) => {
|
|
41
|
+
const quote = specifier.getText()[0];
|
|
42
|
+
const start = specifier.getStart();
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
type: devkit_1.ChangeType.Delete,
|
|
46
|
+
start,
|
|
47
|
+
length: specifier.getEnd() - start,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: devkit_1.ChangeType.Insert,
|
|
51
|
+
index: start,
|
|
52
|
+
text: `${quote}${NEW_SPECIFIER}${quote}`,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
if (changes.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
tree.write(filePath, (0, devkit_1.applyChangesToString)(originalContent, changes.flat()));
|
|
60
|
+
wereFilesMigrated = true;
|
|
61
|
+
});
|
|
62
|
+
removeDeprecatedPackage(tree);
|
|
63
|
+
if (wereFilesMigrated) {
|
|
64
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
65
|
+
}
|
|
66
|
+
if (shadowedRequires.length > 0) {
|
|
67
|
+
const notes = shadowedRequires.map((filePath) => `Left the \`require()\` calls in ${filePath} untouched because it declares its own \`require\`; point them at \`${NEW_SPECIFIER}\` by hand if they load the Cypress harness`);
|
|
68
|
+
return { nextSteps: notes, agentContext: notes };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function removeDeprecatedPackage(tree) {
|
|
72
|
+
const { dependencies = {}, devDependencies = {} } = (0, devkit_1.readJson)(tree, 'package.json');
|
|
73
|
+
if (!dependencies[DEPRECATED_PACKAGE] &&
|
|
74
|
+
!devDependencies[DEPRECATED_PACKAGE]) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
(0, devkit_1.removeDependenciesFromPackageJson)(tree, [DEPRECATED_PACKAGE], [DEPRECATED_PACKAGE]);
|
|
78
|
+
}
|
|
79
|
+
// `import`/`export ... from`, `import()`, `typeof import()` and `require()` forms.
|
|
80
|
+
function findModuleSpecifiers(sourceFile) {
|
|
81
|
+
ts ??= (0, internal_1.ensureTypescript)();
|
|
82
|
+
return (0, tsquery_1.query)(sourceFile, `:matches(StringLiteral, NoSubstitutionTemplateLiteral)`).filter((literal) => {
|
|
83
|
+
if (!OLD_SPECIFIERS.has(literal.text)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const parent = literal.parent;
|
|
87
|
+
if (ts.isImportDeclaration(parent) ||
|
|
88
|
+
ts.isExportDeclaration(parent) ||
|
|
89
|
+
ts.isExternalModuleReference(parent) ||
|
|
90
|
+
(ts.isLiteralTypeNode(parent) && ts.isImportTypeNode(parent.parent))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return ((ts.isCallExpression(parent) &&
|
|
94
|
+
parent.arguments[0] === literal &&
|
|
95
|
+
parent.expression.kind === ts.SyntaxKind.ImportKeyword) ||
|
|
96
|
+
isRequireArgument(literal));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function isRequireArgument(literal) {
|
|
100
|
+
const parent = literal.parent;
|
|
101
|
+
return (ts.isCallExpression(parent) &&
|
|
102
|
+
parent.arguments[0] === literal &&
|
|
103
|
+
ts.isIdentifier(parent.expression) &&
|
|
104
|
+
parent.expression.text === 'require');
|
|
105
|
+
}
|
|
106
|
+
function isJsTsFile(filePath) {
|
|
107
|
+
return /\.[cm]?[jt]sx?$/.test(filePath);
|
|
108
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#### Use `cypress/angular` for zoneless Angular component tests in Cypress 16
|
|
2
|
+
|
|
3
|
+
Cypress 16 removed the `cypress/angular-zoneless` entry point and deprecated the `@cypress/angular-zoneless` npm package. `cypress/angular` now mounts components with zoneless change detection and supports Angular 21 and later. Imports of `cypress/angular-zoneless` fail to resolve on Cypress 16.
|
|
4
|
+
|
|
5
|
+
This migration rewrites `cypress/angular-zoneless` and `@cypress/angular-zoneless` module references to `cypress/angular` in every JavaScript and TypeScript file of the workspace, shared support libraries included: `import` and `export ... from` declarations, `import type`, dynamic `import()`, `require()` and `typeof import()` types. A `require()` call in a file that declares its own `require` value is left alone and listed as a next step. It also removes the `@cypress/angular-zoneless` package from `package.json` when it is installed.
|
|
6
|
+
|
|
7
|
+
#### Sample code changes
|
|
8
|
+
|
|
9
|
+
##### Before
|
|
10
|
+
|
|
11
|
+
```ts title="apps/myapp/cypress/support/component.ts"
|
|
12
|
+
import { mount } from 'cypress/angular-zoneless';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
##### After
|
|
16
|
+
|
|
17
|
+
```ts title="apps/myapp/cypress/support/component.ts"
|
|
18
|
+
import { mount } from 'cypress/angular';
|
|
19
|
+
```
|