@nx/cypress 23.3.0-beta.0 → 23.3.0-beta.2
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/dist/src/generators/component-configuration/component-configuration.js +17 -5
- 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 +177 -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 +241 -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.md +27 -0
- package/dist/src/utils/config.d.ts +8 -1
- package/dist/src/utils/config.js +33 -21
- package/dist/src/utils/migrations.d.ts +13 -1
- package/dist/src/utils/migrations.js +110 -9
- package/dist/src/utils/versions.d.ts +7 -5
- package/dist/src/utils/versions.js +58 -5
- package/migrations.json +60 -0
- package/package.json +6 -6
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = updateCypress16ConfigOptions;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const internal_1 = require("@nx/js/internal");
|
|
6
|
+
const config_1 = require("../../utils/config");
|
|
7
|
+
const migrations_1 = require("../../utils/migrations");
|
|
8
|
+
// Cypress 16 breakingOptions (packages/config/src/options.ts).
|
|
9
|
+
const REMOVED_OPTIONS = [
|
|
10
|
+
'experimentalSourceRewriting',
|
|
11
|
+
'allowCypressEnv',
|
|
12
|
+
'execTimeout',
|
|
13
|
+
];
|
|
14
|
+
const RENAMED_OPTIONS = {
|
|
15
|
+
experimentalMemoryManagement: 'manageBrowserMemory',
|
|
16
|
+
};
|
|
17
|
+
const FAST_VISIBILITY_OPTION = 'experimentalFastVisibility';
|
|
18
|
+
const VISIBILITY_STRATEGY_OPTION = 'visibilityStrategy';
|
|
19
|
+
const MIGRATE_BY_HAND = `migrate ${[
|
|
20
|
+
...REMOVED_OPTIONS,
|
|
21
|
+
...Object.keys(RENAMED_OPTIONS),
|
|
22
|
+
FAST_VISIBILITY_OPTION,
|
|
23
|
+
]
|
|
24
|
+
.map((option) => `\`${option}\``)
|
|
25
|
+
.join(', ')} in its source by hand if it sets them`;
|
|
26
|
+
// Cypress reads these options at the top level and inside `e2e`/`component`.
|
|
27
|
+
const TESTING_TYPE_BLOCKS = ['e2e', 'component'];
|
|
28
|
+
// The Nx presets never return one of the options above, so their spreads
|
|
29
|
+
// are not reported as unresolved.
|
|
30
|
+
const NX_PRESET_CALLS = ['nxE2EPreset', 'nxComponentTestingPreset'];
|
|
31
|
+
let ts;
|
|
32
|
+
async function updateCypress16ConfigOptions(tree) {
|
|
33
|
+
const unhandled = [];
|
|
34
|
+
let wereConfigsMigrated = false;
|
|
35
|
+
for await (const { cypressConfigPath } of (0, migrations_1.cypressProjectConfigs)(tree)) {
|
|
36
|
+
if (!tree.exists(cypressConfigPath)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const contents = tree.read(cypressConfigPath, 'utf-8');
|
|
40
|
+
const config = (0, config_1.resolveCypressConfigObject)(contents);
|
|
41
|
+
if (!config) {
|
|
42
|
+
unhandled.push(`${cypressConfigPath}: the config object could not be resolved statically; ${MIGRATE_BY_HAND}`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
ts ??= (0, internal_1.ensureTypescript)();
|
|
46
|
+
const { blocks, unresolvedSpreads } = getOptionBlocks(config);
|
|
47
|
+
for (const spread of unresolvedSpreads) {
|
|
48
|
+
unhandled.push(`${cypressConfigPath}: the \`${spread.getText()}\` spread could not be resolved statically; ${MIGRATE_BY_HAND}`);
|
|
49
|
+
}
|
|
50
|
+
const changes = [];
|
|
51
|
+
for (const block of blocks) {
|
|
52
|
+
for (const property of block.properties) {
|
|
53
|
+
if (!isConfigProperty(property)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const name = getPropertyName(property.name);
|
|
57
|
+
if (name === null) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (REMOVED_OPTIONS.includes(name)) {
|
|
61
|
+
changes.push(...removeProperty(contents, property));
|
|
62
|
+
const followUp = getRemovalFollowUp(property);
|
|
63
|
+
if (followUp) {
|
|
64
|
+
unhandled.push(`${cypressConfigPath}: ${followUp}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (name in RENAMED_OPTIONS) {
|
|
68
|
+
const newName = RENAMED_OPTIONS[name];
|
|
69
|
+
if (hasProperty(block, newName)) {
|
|
70
|
+
changes.push(...removeProperty(contents, property));
|
|
71
|
+
}
|
|
72
|
+
else if (ts.isPropertyAssignment(property)) {
|
|
73
|
+
changes.push(...replaceNode(property.name, newName));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
changes.push(...replaceNode(property, `${newName}: ${name}`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (name === FAST_VISIBILITY_OPTION) {
|
|
80
|
+
const strategy = getVisibilityStrategy(property);
|
|
81
|
+
if (hasProperty(block, VISIBILITY_STRATEGY_OPTION)) {
|
|
82
|
+
changes.push(...removeProperty(contents, property));
|
|
83
|
+
}
|
|
84
|
+
else if (strategy) {
|
|
85
|
+
changes.push(...replaceNode(property, `${VISIBILITY_STRATEGY_OPTION}: '${strategy}'`));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
unhandled.push(`${cypressConfigPath}: \`${FAST_VISIBILITY_OPTION}\` is set to a non-literal value (${getValueText(property)}); replace it with \`${VISIBILITY_STRATEGY_OPTION}: 'modern'\` (was true) or \`${VISIBILITY_STRATEGY_OPTION}: 'legacy'\` (was false)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (changes.length > 0) {
|
|
94
|
+
tree.write(cypressConfigPath, (0, devkit_1.applyChangesToString)(contents, changes));
|
|
95
|
+
wereConfigsMigrated = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (wereConfigsMigrated) {
|
|
99
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
100
|
+
}
|
|
101
|
+
if (unhandled.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
nextSteps: unhandled.map((item) => `Review the Cypress config option change: ${item}`),
|
|
104
|
+
agentContext: unhandled,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Removed options whose value carried intent the replacement needs. A
|
|
109
|
+
// shorthand value is unknown, so it gets the follow-up too.
|
|
110
|
+
function getRemovalFollowUp(property) {
|
|
111
|
+
const name = getPropertyName(property.name);
|
|
112
|
+
if (name === 'execTimeout') {
|
|
113
|
+
return `removed \`${property.getText()}\`; \`cy.exec()\` is gone, set \`taskTimeout\` if the replacement \`cy.task()\` needs more than the 60000ms default`;
|
|
114
|
+
}
|
|
115
|
+
if (name === 'experimentalSourceRewriting' &&
|
|
116
|
+
(ts.isShorthandPropertyAssignment(property) ||
|
|
117
|
+
property.initializer.kind === ts.SyntaxKind.TrueKeyword)) {
|
|
118
|
+
return `removed \`${property.getText()}\`; set \`removeSRIAttributes: true\` if it worked around Subresource Integrity errors`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
// The option blocks the config is made of: the config object, its `e2e` and
|
|
123
|
+
// `component` blocks, and the same-file objects spread into any of them,
|
|
124
|
+
// each edited in place. `e2e`/`component` are only read at the top level,
|
|
125
|
+
// which the objects spread into the config are part of, so that level is
|
|
126
|
+
// collected first. A spread that resolves to nothing is returned so it can
|
|
127
|
+
// be reported, the Nx preset calls aside.
|
|
128
|
+
function getOptionBlocks(config) {
|
|
129
|
+
const sourceFile = config.getSourceFile();
|
|
130
|
+
const unresolvedSpreads = [];
|
|
131
|
+
// Collects `root` and the objects reachable from it through spreads.
|
|
132
|
+
// A collected object is not scanned again, so an object shared by
|
|
133
|
+
// several blocks is edited and reported once.
|
|
134
|
+
const collect = (root, into) => {
|
|
135
|
+
if (into.has(root)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
into.add(root);
|
|
139
|
+
for (const property of root.properties) {
|
|
140
|
+
if (!ts.isSpreadAssignment(property) ||
|
|
141
|
+
isNxPresetCall(property.expression)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const block = (0, config_1.resolveObjectLiteral)(property.expression, sourceFile);
|
|
145
|
+
if (block) {
|
|
146
|
+
collect(block, into);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
unresolvedSpreads.push(property);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const topLevel = new Set();
|
|
154
|
+
collect(config, topLevel);
|
|
155
|
+
const blocks = new Set(topLevel);
|
|
156
|
+
for (const block of topLevel) {
|
|
157
|
+
for (const property of block.properties) {
|
|
158
|
+
if (!isConfigProperty(property) ||
|
|
159
|
+
!TESTING_TYPE_BLOCKS.includes(getPropertyName(property.name))) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const nested = (0, config_1.resolveObjectLiteral)(ts.isPropertyAssignment(property)
|
|
163
|
+
? property.initializer
|
|
164
|
+
: property.name, sourceFile);
|
|
165
|
+
if (nested) {
|
|
166
|
+
collect(nested, blocks);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { blocks, unresolvedSpreads };
|
|
171
|
+
}
|
|
172
|
+
function isNxPresetCall(expression) {
|
|
173
|
+
return (ts.isCallExpression(expression) &&
|
|
174
|
+
ts.isIdentifier(expression.expression) &&
|
|
175
|
+
NX_PRESET_CALLS.includes(expression.expression.text));
|
|
176
|
+
}
|
|
177
|
+
function hasProperty(block, name) {
|
|
178
|
+
return block.properties.some((property) => isConfigProperty(property) && getPropertyName(property.name) === name);
|
|
179
|
+
}
|
|
180
|
+
function isConfigProperty(node) {
|
|
181
|
+
return (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node));
|
|
182
|
+
}
|
|
183
|
+
// A computed identifier (`[key]: value`) resolves at runtime, so it is skipped.
|
|
184
|
+
function getPropertyName(name) {
|
|
185
|
+
if (ts.isIdentifier(name) || isStringLiteralName(name)) {
|
|
186
|
+
return name.text;
|
|
187
|
+
}
|
|
188
|
+
if (ts.isComputedPropertyName(name) && isStringLiteralName(name.expression)) {
|
|
189
|
+
return name.expression.text;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
function isStringLiteralName(node) {
|
|
194
|
+
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
|
|
195
|
+
}
|
|
196
|
+
function getValueText(property) {
|
|
197
|
+
return ts.isPropertyAssignment(property)
|
|
198
|
+
? property.initializer.getText()
|
|
199
|
+
: property.name.text;
|
|
200
|
+
}
|
|
201
|
+
function getVisibilityStrategy(property) {
|
|
202
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
switch (property.initializer.kind) {
|
|
206
|
+
case ts.SyntaxKind.TrueKeyword:
|
|
207
|
+
return 'modern';
|
|
208
|
+
case ts.SyntaxKind.FalseKeyword:
|
|
209
|
+
return 'legacy';
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Removes the property with its trailing comma, and the whole line when the
|
|
215
|
+
// property is alone on it.
|
|
216
|
+
function removeProperty(contents, property) {
|
|
217
|
+
let start = property.getStart();
|
|
218
|
+
let end = property.getEnd();
|
|
219
|
+
// The node ends before its trailing trivia, so the comma can sit behind a
|
|
220
|
+
// comment or a line break.
|
|
221
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, contents, undefined, end);
|
|
222
|
+
if (scanner.scan() === ts.SyntaxKind.CommaToken) {
|
|
223
|
+
end = scanner.getTokenEnd();
|
|
224
|
+
}
|
|
225
|
+
const lineStart = contents.lastIndexOf('\n', start - 1) + 1;
|
|
226
|
+
const lineEnd = contents.indexOf('\n', end);
|
|
227
|
+
const restOfLine = lineEnd === -1 ? contents.slice(end) : contents.slice(end, lineEnd);
|
|
228
|
+
if (contents.slice(lineStart, start).trim() === '' &&
|
|
229
|
+
restOfLine.trim() === '') {
|
|
230
|
+
start = lineStart;
|
|
231
|
+
end = lineEnd === -1 ? contents.length : lineEnd + 1;
|
|
232
|
+
}
|
|
233
|
+
return [{ type: devkit_1.ChangeType.Delete, start, length: end - start }];
|
|
234
|
+
}
|
|
235
|
+
function replaceNode(node, text) {
|
|
236
|
+
const start = node.getStart();
|
|
237
|
+
return [
|
|
238
|
+
{ type: devkit_1.ChangeType.Delete, start, length: node.getEnd() - start },
|
|
239
|
+
{ type: devkit_1.ChangeType.Insert, index: start, text },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#### Update Cypress config options renamed or removed in Cypress 16
|
|
2
|
+
|
|
3
|
+
Cypress 16 renamed `experimentalMemoryManagement` to `manageBrowserMemory` (now on by default), replaced `experimentalFastVisibility` with `visibilityStrategy` (`'modern'` by default, `'legacy'` deprecated), and removed `experimentalSourceRewriting`, `allowCypressEnv` and `execTimeout` (`cy.exec()` no longer exists). Cypress warns on startup while the old keys stay in the config.
|
|
4
|
+
|
|
5
|
+
This migration rewrites `cypress.config.{ts,js,mjs,cjs}` files of Cypress projects: it renames `experimentalMemoryManagement`, turns `experimentalFastVisibility: true` into `visibilityStrategy: 'modern'` and `experimentalFastVisibility: false` into `visibilityStrategy: 'legacy'`, and deletes the removed options. The config object is found behind `export default` or `module.exports`, through a `defineConfig()` call and through variables of the same file holding either. It edits the top level of the config object and the `e2e` and `component` blocks, inline or held in a variable of the same file, and the objects of the same file spread into any of them, and handles plain, quoted, shorthand and statically computed keys. A config object the migration cannot resolve statically is reported as a next step, and so is a spread of an import or a call, the `nxE2EPreset()` and `nxComponentTestingPreset()` spreads aside. A renamed shorthand key becomes `manageBrowserMemory: experimentalMemoryManagement`. A shorthand `experimentalFastVisibility` or `experimentalSourceRewriting` has a value the migration cannot read, so it is reported as a next step. When the new key is already set next to the old one, only the old key is removed. An `experimentalFastVisibility` value that is not a boolean literal is left in place and reported as a next step. Removing `execTimeout`, or `experimentalSourceRewriting: true`, is also reported as a next step: the `cy.task()` calls that replace `cy.exec()` use `taskTimeout`, and an application that pins resources with Subresource Integrity needs `removeSRIAttributes: true` instead of the source rewriter.
|
|
6
|
+
|
|
7
|
+
#### Sample code changes
|
|
8
|
+
|
|
9
|
+
##### Before
|
|
10
|
+
|
|
11
|
+
```ts title="apps/myapp-e2e/cypress.config.ts"
|
|
12
|
+
import { defineConfig } from 'cypress';
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
experimentalMemoryManagement: true,
|
|
16
|
+
e2e: {
|
|
17
|
+
baseUrl: 'http://localhost:4200',
|
|
18
|
+
experimentalFastVisibility: true,
|
|
19
|
+
execTimeout: 60000,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
##### After
|
|
25
|
+
|
|
26
|
+
```ts title="apps/myapp-e2e/cypress.config.ts"
|
|
27
|
+
import { defineConfig } from 'cypress';
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
manageBrowserMemory: true,
|
|
31
|
+
e2e: {
|
|
32
|
+
baseUrl: 'http://localhost:4200',
|
|
33
|
+
visibilityStrategy: 'modern',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
export default function updateCypress16QueryCommandOverwrites(tree: Tree): Promise<{
|
|
3
|
+
skipAgentic: boolean;
|
|
4
|
+
nextSteps?: undefined;
|
|
5
|
+
agentContext?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
skipAgentic?: undefined;
|
|
8
|
+
nextSteps: string[];
|
|
9
|
+
agentContext: string[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = updateCypress16QueryCommandOverwrites;
|
|
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
|
+
// Commands that became queries in Cypress 16
|
|
9
|
+
// (packages/driver/src/cy/commands/{cookies,storage}.ts).
|
|
10
|
+
const QUERY_COMMANDS = [
|
|
11
|
+
'getCookie',
|
|
12
|
+
'getCookies',
|
|
13
|
+
'getAllCookies',
|
|
14
|
+
'getAllLocalStorage',
|
|
15
|
+
'getAllSessionStorage',
|
|
16
|
+
];
|
|
17
|
+
let ts;
|
|
18
|
+
async function updateCypress16QueryCommandOverwrites(tree) {
|
|
19
|
+
const migrated = [];
|
|
20
|
+
const shadowed = [];
|
|
21
|
+
// Shared support libraries can hold the overwrites, so the whole workspace
|
|
22
|
+
// is scanned rather than the Cypress project roots.
|
|
23
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
|
|
24
|
+
if (!isJsTsFile(filePath)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const originalContent = tree.read(filePath, 'utf-8');
|
|
28
|
+
if (!originalContent.includes('overwrite')) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const sourceFile = (0, migrations_1.parseSourceFile)(filePath, originalContent);
|
|
32
|
+
const overwrites = findQueryOverwrites(sourceFile);
|
|
33
|
+
if (overwrites.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// A file with its own `Cypress` value is not calling the global.
|
|
37
|
+
if ((0, migrations_1.hasLocalValueBinding)(sourceFile, 'Cypress')) {
|
|
38
|
+
shadowed.push(filePath);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
tree.write(filePath, (0, devkit_1.applyChangesToString)(originalContent, overwrites.flatMap((call) => renameToOverwriteQuery(call))));
|
|
42
|
+
migrated.push(`${filePath}: ${overwrites
|
|
43
|
+
.map((call) => `\`${call.arguments[0].getText()}\``)
|
|
44
|
+
.join(', ')}`);
|
|
45
|
+
});
|
|
46
|
+
if (migrated.length === 0 && shadowed.length === 0) {
|
|
47
|
+
return { skipAgentic: true };
|
|
48
|
+
}
|
|
49
|
+
if (migrated.length > 0) {
|
|
50
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
51
|
+
}
|
|
52
|
+
const shadowedNotes = shadowed.map((filePath) => `Left ${filePath} untouched because it declares its own \`Cypress\`; rename its \`Cypress.Commands.overwrite()\` calls for the cookie and storage queries to \`overwriteQuery()\` by hand if they target the Cypress global`);
|
|
53
|
+
return {
|
|
54
|
+
nextSteps: [
|
|
55
|
+
...migrated.map((item) => `Adapt the \`Cypress.Commands.overwriteQuery()\` callback in ${item}: make it a \`function\` that calls \`originalFn.call(this, ...)\` and returns a function computing the query result, not a chainable`),
|
|
56
|
+
...shadowedNotes,
|
|
57
|
+
],
|
|
58
|
+
agentContext: [
|
|
59
|
+
...migrated.map((item) => `Renamed \`Cypress.Commands.overwrite()\` to \`overwriteQuery()\` in ${item}`),
|
|
60
|
+
...shadowedNotes,
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// `Cypress.Commands.overwrite(...)` for a command that became a query, with
|
|
65
|
+
// each member in dot or static bracket form.
|
|
66
|
+
function findQueryOverwrites(sourceFile) {
|
|
67
|
+
ts ??= (0, internal_1.ensureTypescript)();
|
|
68
|
+
return (0, tsquery_1.query)(sourceFile, 'CallExpression').filter((call) => {
|
|
69
|
+
const [commandName] = call.arguments;
|
|
70
|
+
return (isCypressCommandsMember(call.expression, 'overwrite') &&
|
|
71
|
+
commandName !== undefined &&
|
|
72
|
+
isStringLiteral(commandName) &&
|
|
73
|
+
QUERY_COMMANDS.includes(commandName.text));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function isCypressCommandsMember(node, member) {
|
|
77
|
+
if (!isMemberAccess(node) || getStaticMemberName(node) !== member) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const commands = node.expression;
|
|
81
|
+
return (isMemberAccess(commands) &&
|
|
82
|
+
getStaticMemberName(commands) === 'Commands' &&
|
|
83
|
+
ts.isIdentifier(commands.expression) &&
|
|
84
|
+
commands.expression.text === 'Cypress');
|
|
85
|
+
}
|
|
86
|
+
function isMemberAccess(node) {
|
|
87
|
+
return (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node));
|
|
88
|
+
}
|
|
89
|
+
function getStaticMemberName(node) {
|
|
90
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
91
|
+
return node.name.text;
|
|
92
|
+
}
|
|
93
|
+
return isStringLiteral(node.argumentExpression)
|
|
94
|
+
? node.argumentExpression.text
|
|
95
|
+
: null;
|
|
96
|
+
}
|
|
97
|
+
function isStringLiteral(node) {
|
|
98
|
+
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
|
|
99
|
+
}
|
|
100
|
+
function renameToOverwriteQuery(call) {
|
|
101
|
+
const callee = call.expression;
|
|
102
|
+
const member = ts.isPropertyAccessExpression(callee)
|
|
103
|
+
? callee.name
|
|
104
|
+
: callee.argumentExpression;
|
|
105
|
+
const quote = ts.isPropertyAccessExpression(callee)
|
|
106
|
+
? ''
|
|
107
|
+
: member.getText()[0];
|
|
108
|
+
const start = member.getStart();
|
|
109
|
+
return [
|
|
110
|
+
{ type: devkit_1.ChangeType.Delete, start, length: member.getEnd() - start },
|
|
111
|
+
{
|
|
112
|
+
type: devkit_1.ChangeType.Insert,
|
|
113
|
+
index: start,
|
|
114
|
+
text: `${quote}overwriteQuery${quote}`,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
function isJsTsFile(filePath) {
|
|
119
|
+
return /\.[cm]?[jt]sx?$/.test(filePath);
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#### Overwrite the cookie and storage queries with `overwriteQuery` in Cypress 16
|
|
2
|
+
|
|
3
|
+
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()`, so an existing `Cypress.Commands.overwrite()` for any of them fails at runtime with "Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`".
|
|
4
|
+
|
|
5
|
+
This migration renames those `Cypress.Commands.overwrite()` calls to `Cypress.Commands.overwriteQuery()` in every JavaScript and TypeScript file of the workspace, shared support libraries included; the `Cypress.Commands['overwrite']()` spelling is renamed too. A file that declares its own `Cypress` value (a variable, an import, a function, a class, an enum or a namespace holding a value; a type-only namespace and the usual `declare global { namespace Cypress { ... } }` augmentation are not ones) is left alone and listed as a next step. The callback contract differs: `overwriteQuery` calls the callback with the command as `this`, hands it the original query function, and expects it to return a function that computes the result from the subject, instead of a chainable. The original query reads `this` too, so the callback must be a `function` that forwards with `originalFn.call(this, ...)`; an arrow callback fails to typecheck (TS2684) and throws at runtime. Every renamed call is listed as a next step so the callback can be adapted by hand, and the paired AI prompt does that in agentic runs.
|
|
6
|
+
|
|
7
|
+
#### Sample code changes
|
|
8
|
+
|
|
9
|
+
##### Before
|
|
10
|
+
|
|
11
|
+
```ts title="apps/myapp-e2e/src/support/commands.ts"
|
|
12
|
+
Cypress.Commands.overwrite('getCookie', (originalFn, name, options) => {
|
|
13
|
+
return originalFn(name, { ...options, log: false });
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
##### After the automatic rename (the callback still needs adapting)
|
|
18
|
+
|
|
19
|
+
```ts title="apps/myapp-e2e/src/support/commands.ts"
|
|
20
|
+
Cypress.Commands.overwriteQuery('getCookie', (originalFn, name, options) => {
|
|
21
|
+
return originalFn(name, { ...options, log: false });
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
##### After adapting the callback by hand or through the AI prompt
|
|
26
|
+
|
|
27
|
+
```ts title="apps/myapp-e2e/src/support/commands.ts"
|
|
28
|
+
Cypress.Commands.overwriteQuery(
|
|
29
|
+
'getCookie',
|
|
30
|
+
function (originalFn, name, options) {
|
|
31
|
+
return originalFn.call(this, name, { ...options, log: false });
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#### Upgrade Cypress 15 to 16
|
|
2
|
+
|
|
3
|
+
Bumps `cypress` to 16 together with `@cypress/vite-dev-server` 8 and `@cypress/webpack-dev-server` 6. A workspace with a Vite below 8 installed at the workspace root that does not land on Vite 8 in the same run stays on Cypress 15, since Cypress 16 component testing rejects an older Vite. A Vite declared only in a project's `package.json` is not detected, so check those projects before running their component tests. Cypress 16 removes `Cypress.env()` (replaced by the asynchronous `cy.env()` for `env` values and `Cypress.expose()` for the new `expose` values), `cy.exec()` (use `cy.task()`) and `.end()`, drops `env` from test configuration overrides, turns the cookie and storage getters into queries, drops CoffeeScript support, and routes Chrome, Chromium and Edge through the native browser network. It requires Node 22, 24 or 26+, Vite 8 for Vite component testing, Angular 21 for Angular component testing and Next.js 15.0.4 for component testing on Cypress's `next` dev server framework, which the Nx preset does not use. Read more in the [Cypress 16 migration guide](https://docs.cypress.io/app/references/migration-guide#Migrating-to-Cypress-160).
|
|
4
|
+
|
|
5
|
+
The generator-based migrations in this release update the renamed and removed config options, the `cypress/angular-zoneless` import and the `Cypress.Commands.overwrite()` calls for the new queries. The paired AI instructions migration walks an agent through the source changes that need judgment. The most common one is shown below.
|
|
6
|
+
|
|
7
|
+
#### Examples
|
|
8
|
+
|
|
9
|
+
##### Before
|
|
10
|
+
|
|
11
|
+
```ts title="apps/myapp-e2e/src/e2e/app.cy.ts"
|
|
12
|
+
const apiUrl = Cypress.env('API_URL');
|
|
13
|
+
|
|
14
|
+
it('is healthy', () => {
|
|
15
|
+
cy.request(`${apiUrl}/health`).its('status').should('eq', 200);
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
##### After
|
|
20
|
+
|
|
21
|
+
```ts title="apps/myapp-e2e/src/e2e/app.cy.ts"
|
|
22
|
+
it('is healthy', () => {
|
|
23
|
+
cy.env(['API_URL']).then(({ API_URL }) => {
|
|
24
|
+
cy.request(`${API_URL}/health`).its('status').should('eq', 200);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Tree } from '@nx/devkit';
|
|
2
|
-
import type { ObjectLiteralExpression } from 'typescript';
|
|
2
|
+
import type { Expression, ObjectLiteralExpression, SourceFile } from 'typescript';
|
|
3
3
|
import type { NxComponentTestingOptions, NxCypressE2EPresetOptions } from '../../plugins/cypress-preset';
|
|
4
4
|
export declare const CYPRESS_CONFIG_FILE_NAME_PATTERN = "cypress.config.{js,ts,mjs,cjs}";
|
|
5
5
|
export declare const JIT_COMPILE_DISABLE_COMMENT: string[];
|
|
@@ -29,3 +29,10 @@ export declare function addDefaultCTConfig(cyConfigContents: string, options?: N
|
|
|
29
29
|
export declare function addMountDefinition(cmpCommandFileContents: string): Promise<string>;
|
|
30
30
|
export declare function getProjectCypressConfigPath(tree: Tree, projectRoot: string): string;
|
|
31
31
|
export declare function resolveCypressConfigObject(cypressConfigContents: string): ObjectLiteralExpression | null;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the object literal an expression stands for in a Cypress config
|
|
34
|
+
* file: the literal itself, the argument of a `defineConfig()` call, or a
|
|
35
|
+
* variable declared at the top level of the file that holds either. Anything
|
|
36
|
+
* else (a spread, a function call, an import) resolves to `null`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveObjectLiteral(expression: Expression, sourceFile: SourceFile, visitedIdentifiers?: Set<string>): ObjectLiteralExpression | null;
|
package/dist/src/utils/config.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.addDefaultCTConfig = addDefaultCTConfig;
|
|
|
6
6
|
exports.addMountDefinition = addMountDefinition;
|
|
7
7
|
exports.getProjectCypressConfigPath = getProjectCypressConfigPath;
|
|
8
8
|
exports.resolveCypressConfigObject = resolveCypressConfigObject;
|
|
9
|
+
exports.resolveObjectLiteral = resolveObjectLiteral;
|
|
9
10
|
const devkit_1 = require("@nx/devkit");
|
|
10
11
|
const internal_1 = require("@nx/js/internal");
|
|
11
12
|
exports.CYPRESS_CONFIG_FILE_NAME_PATTERN = 'cypress.config.{js,ts,mjs,cjs}';
|
|
@@ -189,41 +190,52 @@ function resolveCypressConfigObject(cypressConfigContents) {
|
|
|
189
190
|
const sourceFile = tsquery.ast(cypressConfigContents);
|
|
190
191
|
const exportDefaultStatement = sourceFile.statements.find((statement) => ts.isExportAssignment(statement));
|
|
191
192
|
if (exportDefaultStatement) {
|
|
192
|
-
return
|
|
193
|
+
return resolveObjectLiteral(exportDefaultStatement.expression, sourceFile);
|
|
193
194
|
}
|
|
194
195
|
const moduleExportsStatement = sourceFile.statements.find((statement) => ts.isExpressionStatement(statement) &&
|
|
195
196
|
ts.isBinaryExpression(statement.expression) &&
|
|
196
197
|
statement.expression.left.getText() === 'module.exports');
|
|
197
198
|
if (moduleExportsStatement) {
|
|
198
|
-
return
|
|
199
|
+
return resolveObjectLiteral(moduleExportsStatement.expression.right, sourceFile);
|
|
199
200
|
}
|
|
200
201
|
return null;
|
|
201
202
|
}
|
|
202
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Resolves the object literal an expression stands for in a Cypress config
|
|
205
|
+
* file: the literal itself, the argument of a `defineConfig()` call, or a
|
|
206
|
+
* variable declared at the top level of the file that holds either. Anything
|
|
207
|
+
* else (a spread, a function call, an import) resolves to `null`.
|
|
208
|
+
*/
|
|
209
|
+
function resolveObjectLiteral(expression, sourceFile, visitedIdentifiers = new Set()) {
|
|
203
210
|
const ts = (0, internal_1.ensureTypescript)();
|
|
204
|
-
if (ts.isObjectLiteralExpression(
|
|
205
|
-
return
|
|
211
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
212
|
+
return expression;
|
|
213
|
+
}
|
|
214
|
+
if (ts.isCallExpression(expression) &&
|
|
215
|
+
ts.isIdentifier(expression.expression) &&
|
|
216
|
+
expression.expression.text === 'defineConfig' &&
|
|
217
|
+
expression.arguments[0]) {
|
|
218
|
+
return resolveObjectLiteral(expression.arguments[0], sourceFile, visitedIdentifiers);
|
|
206
219
|
}
|
|
207
|
-
if (ts.isIdentifier(
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
if (ts.isIdentifier(expression)) {
|
|
221
|
+
// `const a = b; const b = a;` would otherwise loop forever.
|
|
222
|
+
if (visitedIdentifiers.has(expression.text)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
visitedIdentifiers.add(expression.text);
|
|
226
|
+
for (const statement of sourceFile.statements) {
|
|
227
|
+
if (!ts.isVariableStatement(statement)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
213
231
|
if (ts.isIdentifier(declaration.name) &&
|
|
214
|
-
declaration.name.
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
declaration.name.text === expression.text) {
|
|
233
|
+
return declaration.initializer
|
|
234
|
+
? resolveObjectLiteral(declaration.initializer, sourceFile, visitedIdentifiers)
|
|
235
|
+
: null;
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
238
|
}
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
if (ts.isCallExpression(exportExpression) &&
|
|
223
|
-
ts.isIdentifier(exportExpression.expression) &&
|
|
224
|
-
exportExpression.expression.getText() === 'defineConfig' &&
|
|
225
|
-
ts.isObjectLiteralExpression(exportExpression.arguments[0])) {
|
|
226
|
-
return exportExpression.arguments[0];
|
|
227
239
|
}
|
|
228
240
|
return null;
|
|
229
241
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProjectConfiguration, type Tree } from '@nx/devkit';
|
|
2
|
-
import type { Expression, ObjectLiteralExpression, PropertyAssignment } from 'typescript';
|
|
2
|
+
import type { Expression, ObjectLiteralExpression, PropertyAssignment, SourceFile } from 'typescript';
|
|
3
3
|
export declare function cypressProjectConfigs(tree: Tree): AsyncGenerator<{
|
|
4
4
|
projectName: string;
|
|
5
5
|
projectConfig: ProjectConfiguration;
|
|
@@ -11,3 +11,15 @@ export declare function updateObjectProperty(config: ObjectLiteralExpression, pr
|
|
|
11
11
|
newName?: string;
|
|
12
12
|
newValue?: Expression;
|
|
13
13
|
}): ObjectLiteralExpression;
|
|
14
|
+
export declare function parseSourceFile(filePath: string, content: string): SourceFile;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the file binds `name` as a runtime value anywhere: a variable,
|
|
17
|
+
* parameter, destructured element, function or class (declaration or named
|
|
18
|
+
* expression), enum, namespace holding a value, or a value import (`import x`,
|
|
19
|
+
* `import { x }`, `import * as x`, `import x =`).
|
|
20
|
+
* Type-only imports and ambient declarations (`declare ...`, including
|
|
21
|
+
* everything under `declare global`) do not count, so the usual
|
|
22
|
+
* `declare global { namespace Cypress { ... } }` augmentation never hides the
|
|
23
|
+
* `Cypress` global.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasLocalValueBinding(sourceFile: SourceFile, name: string): boolean;
|