feat(testing): Cypress 12 Support (#14058)

This commit is contained in:
Caleb Ukle 2023-01-10 18:48:29 -06:00 committed by GitHub
parent 5970246b51
commit 0bc93ee83d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1096 additions and 3 deletions

View File

@ -35,6 +35,12 @@
"version": "15.1.0-beta.0",
"description": "Update to Cypress v11. This migration will only update if the workspace is already on v10. https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/",
"factory": "./src/migrations/update-15-1-0/cypress-11"
},
"update-to-cypress-12": {
"cli": "nx",
"version": "15.5.0-beta.0",
"description": "Update to Cypress v12. Cypress 12 contains a handful of breaking changes that might causes tests to start failing that nx cannot directly fix. Read more Cypress 12 changes: https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-12-0.This migration will only run if you are already using Cypress v11.",
"factory": "./src/migrations/update-15-5-0/update-to-cypress-12"
}
},
"packageJsonUpdates": {}

View File

@ -43,7 +43,7 @@
"semver": "7.3.4"
},
"peerDependencies": {
"cypress": ">= 3 < 12"
"cypress": ">= 3 < 13"
},
"peerDependenciesMeta": {
"cypress": {

View File

@ -120,7 +120,7 @@ https://nx.dev/cypress/v10-migration-guide
);
updateJson(tree, 'package.json', (json) => {
json.devDependencies['cypress'] = cypressVersion;
json.devDependencies['cypress'] = '^11.2.0';
return json;
});

View File

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cypress 12 Migration should migrate to cy 12 1`] = `
"describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})"
`;
exports[`Cypress 12 Migration should migrate to cy 12 2`] = `
"describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})"
`;

View File

@ -0,0 +1,38 @@
import type { Node } from 'typescript';
export function isAlreadyCommented(node: Node) {
return node.getFullText().includes('TODO(@nrwl/cypress)');
}
export const BANNED_COMMANDS = [
'as',
'children',
'closest',
'contains',
'debug',
'document',
'eq',
'filter',
'find',
'first',
'focused',
'get',
'hash',
'its',
'last',
'location',
'next',
'nextAll',
'not',
'parent',
'parents',
'parentsUntil',
'prev',
'prevUntil',
'root',
'shadow',
'siblings',
'title',
'url',
'window',
];

View File

@ -0,0 +1,717 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
addProjectConfiguration,
stripIndents,
Tree,
readJson,
} from '@nrwl/devkit';
import {
shouldNotOverrideCommands,
shouldNotUseCyInShouldCB,
shouldUseCyIntercept,
shouldUseCySession,
turnOffTestIsolation,
updateToCypress12,
} from './update-to-cypress-12';
import { installedCypressVersion } from '../../utils/cypress-version';
jest.mock('../../utils/cypress-version');
describe('Cypress 12 Migration', () => {
let tree: Tree;
let mockInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
jest.resetAllMocks();
});
it('should migrate to cy 12', () => {
mockInstalledCypressVersion.mockReturnValue(11);
addCypressProject(tree, 'my-app-e2e');
addCypressProject(tree, 'my-other-app-e2e');
updateToCypress12(tree);
assertMigration(tree, 'my-app-e2e');
assertMigration(tree, 'my-other-app-e2e');
const pkgJson = readJson(tree, 'package.json');
expect(pkgJson.devDependencies['cypress']).toEqual('^12.2.0');
});
it('should not migrate if cypress version is < 11', () => {
mockInstalledCypressVersion.mockReturnValue(10);
addCypressProject(tree, 'my-app-e2e');
updateToCypress12(tree);
expect(tree.read('apps/my-app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename)
})"
`);
});
describe('nest cypress commands in should callback', () => {
beforeEach(() => {
tree.write(
'should-callback.ts',
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
cy.should(($s) => {
cy.task("");
})
cy.should(function($el) {
cy.task("");
})
})
})
`
);
});
it('should comment', () => {
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toMatchInlineSnapshot(`
"describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.task(\\"\\");
})
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(function($el) {
cy.task(\\"\\");
})
})
})
"
`);
});
it('should be idempotent', () => {
const expected = `describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.task("");
})
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(function($el) {
cy.task("");
})
})
})
`;
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toEqual(expected);
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toEqual(expected);
});
});
describe('banned Cypres.Commands.overwrite', () => {
beforeEach(() => {
tree.write(
'commands.ts',
`declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
Cypress.Commands.overwrite('find', () => {});
`
);
});
it('should comment', () => {
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toMatchInlineSnapshot(`
"declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_find'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
Cypress.Commands.overwrite('find', () => {});
"
`);
});
it('should be idempotent', () => {
const expected = `declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_find'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
Cypress.Commands.overwrite('find', () => {});
`;
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toEqual(expected);
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toEqual(expected);
});
});
describe('api removal', () => {
it('should be idempotent', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
const expected = stripIndents`describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})`;
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(stripIndents`${tree.read('my-cool-test.cy.ts', 'utf-8')}`).toEqual(
expected
);
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(stripIndents`${tree.read('my-cool-test.cy.ts', 'utf-8')}`).toEqual(
expected
);
});
it('comment on cy.route,cy.server, & Cypress.Server.defaults usage', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
expect(tree.read('my-cool-test.cy.ts', 'utf-8')).toMatchInlineSnapshot(`
"
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
"
`);
});
it('comment on Cypress.Cookies.defaults & Cypress.Cookies.preserveOnce', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(tree.read('my-cool-test.cy.ts', 'utf-8')).toMatchInlineSnapshot(`
"
describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
"
`);
});
});
describe('testIsolation', () => {
it('should be idempotent', () => {
const content = `
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
testIsolation: true,
})
`;
tree.write('my-cypress.config.ts', content);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toEqual(content);
});
it('should add testIsolation: false to the default e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename),
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
},
})
"
`);
});
it('should add testIsolation: false to inline object e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
video: false
}
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
video: false,
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
}
})
"
`);
});
it('should add testIsolation: false for a variable e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const myConfig = {
...nxE2EPreset(__filename),
video: false
}
export default defineConfig({
e2e: myConfig,
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const myConfig = {
...nxE2EPreset(__filename),
video: false
}
export default defineConfig({
e2e: {
...myConfig,
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
},
})
"
`);
});
});
});
function addCypressProject(tree: Tree, name: string) {
const targets = {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {
tsConfig: `apps/${name}/tsconfig.e2e.json`,
testingType: 'e2e',
browser: 'chrome',
},
configurations: {
dev: {
cypressConfig: `apps/${name}/cypress.config.ts`,
devServerTarget: 'client:serve:dev',
baseUrl: 'http://localhost:4206',
},
watch: {
cypressConfig: 'apps/client-e2e/cypress-custom.config.ts',
devServerTarget: 'client:serve:watch',
baseUrl: 'http://localhost:4204',
},
},
defaultConfiguration: 'dev',
},
};
addProjectConfiguration(tree, name, {
root: `apps/${name}`,
sourceRoot: `apps/${name}/src`,
projectType: 'application',
targets,
});
// testIsolation
tree.write(
`apps/${name}/cypress.config.ts`,
`import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename)
})`
);
// test Cypress.Commands.Override
tree.write(
`apps/${name}/src/support/commands.ts`,
`declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
Cypress.Commands.overwrite('find', () => {});
`
);
// test .should(() => cy.<cmd>)
tree.write(
`apps/${name}/src/e2e/callback.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
cy.should(($s) => {
cy.task("");
})
cy.should(function($el) {
cy.task("");
})
})
})`
);
tree.write(
`apps/${name}/src/e2e/intercept-session.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})`
);
tree.write(
`apps/${name}/src/e2e/combo.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})`
);
}
function assertMigration(tree: Tree, name: string) {
expect(tree.read(`apps/${name}/cypress.config.ts`, 'utf-8')).toContain(
'testIsolation: false'
);
// command overrides
expect(tree.read(`apps/${name}/src/support/commands.ts`, 'utf-8')).toContain(
'TODO(@nrwl/cypress): This command can no longer be overridden'
);
// test .should(() => cy.<cmd>)
expect(tree.read(`apps/${name}/src/e2e/callback.spec.ts`, 'utf-8')).toContain(
'TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.'
);
// use cy.intercept, cy.session
const interceptSessionSpec = tree.read(
`apps/${name}/src/e2e/intercept-session.spec.ts`,
'utf-8'
);
expect(interceptSessionSpec).toContain(
'// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes'
);
expect(interceptSessionSpec).toContain(
'// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults'
);
// intercept,session & callback
expect(
tree.read(`apps/${name}/src/e2e/combo.spec.ts`, 'utf-8')
).toMatchSnapshot();
}

View File

@ -0,0 +1,245 @@
import {
getProjects,
stripIndents,
Tree,
updateJson,
visitNotIgnoredFiles,
installPackagesTask,
GeneratorCallback,
} from '@nrwl/devkit';
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import {
CallExpression,
isArrowFunction,
isCallExpression,
isFunctionExpression,
isObjectLiteralExpression,
PropertyAccessExpression,
PropertyAssignment,
} from 'typescript';
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
import { installedCypressVersion } from '../../utils/cypress-version';
import { BANNED_COMMANDS, isAlreadyCommented } from './helpers';
const JS_TS_FILE_MATCHER = /\.[jt]sx?$/;
export function updateToCypress12(tree: Tree): GeneratorCallback {
if (installedCypressVersion() < 11) {
return;
}
const projects = getProjects(tree);
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(options, projectName, targetName, configName) => {
if (!(options.cypressConfig && tree.exists(options.cypressConfig))) {
return;
}
const projectConfig = projects.get(projectName);
turnOffTestIsolation(tree, options.cypressConfig);
visitNotIgnoredFiles(tree, projectConfig.root, (filePath) => {
if (!JS_TS_FILE_MATCHER.test(filePath)) {
return;
}
shouldUseCyIntercept(tree, filePath);
shouldUseCySession(tree, filePath);
shouldNotUseCyInShouldCB(tree, filePath);
shouldNotOverrideCommands(tree, filePath);
});
}
);
console.warn(stripIndents`Cypress 12 has lots of breaking changes that might subility break your tests.
This migration marked known issues that need to be manually migrated,
but there can still be runtime based errors that were not detected.
Please consult the offical Cypress v12 migration guide for more info on these changes and the next steps.
https://docs.cypress.io/guides/references/migration-guide
`);
updateJson(tree, 'package.json', (json) => {
json.devDependencies.cypress = '^12.2.0';
return json;
});
return () => {
installPackagesTask(tree);
};
}
export function turnOffTestIsolation(tree: Tree, configPath: string) {
const config = tree.read(configPath, 'utf-8');
const isTestIsolationSet = tsquery.query<PropertyAssignment>(
config,
'ExportAssignment ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="testIsolation"])'
);
if (isTestIsolationSet.length > 0) {
return;
}
const testIsolationProperty = `/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,`;
const updated = tsquery.replace(
config,
'ExportAssignment ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="e2e"])',
(node: PropertyAssignment) => {
if (isObjectLiteralExpression(node.initializer)) {
const listOfProperties = node.initializer.properties
.map((j) => j.getText())
.join(',\n ');
return `e2e: {
${listOfProperties},
${testIsolationProperty}
}`;
}
return `e2e: {
...${node.initializer.getText()},
${testIsolationProperty}
}`;
}
);
tree.write(configPath, updated);
}
/**
* Leave a comment on all apis that have been removed andsuperseded by cy.intercept
* stating they these API are now removed and need to update.
* cy.route, cy.server, Cypress.Server.defaults
**/
export function shouldUseCyIntercept(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedRemovedCommands = tsquery.replace(
content,
':matches(PropertyAccessExpression:has(Identifier[name="cy"]):has(Identifier[name="server"], Identifier[name="route"]), PropertyAccessExpression:has(Identifier[name="defaults"]):has(Identifier[name="Cypress"], Identifier[name="Server"]))',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent extra chaining i.e. cy.route().as() will return 2 results
// cy.route and cy.route().as
// only need the first 1 so skip any extra chaining
if (expression === 'cy' || expression === 'Cypress.Server') {
return `// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
${node.getText()}`;
}
}
);
tree.write(filePath, markedRemovedCommands);
}
/**
* Leave a comment on all apis that have been removed and superseded by cy.session
* stating they these API are now removed and need to update.
* Cypress.Cookies.defaults & Cypress.Cookies.preserveOnce
**/
export function shouldUseCySession(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedRemovedCommands = tsquery.replace(
content,
':matches(PropertyAccessExpression:has(Identifier[name="defaults"]):has(Identifier[name="Cypress"], Identifier[name="Cookies"]), PropertyAccessExpression:has(Identifier[name="preserveOnce"]):has(Identifier[name="Cypress"], Identifier[name="Cookies"]))',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent grabbing other Cypress.<something>.defaults
if (expression === 'Cypress.Cookies') {
return `// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
${node.getText()}`;
}
}
);
tree.write(filePath, markedRemovedCommands);
}
/**
* leave a comment about nested cy commands in a cy.should callback
* */
export function shouldNotUseCyInShouldCB(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedNestedCyCommands = tsquery.replace(
content,
'CallExpression > PropertyAccessExpression:has(Identifier[name="cy"]):has(Identifier[name="should"])',
(node: PropertyAccessExpression) => {
if (
isAlreadyCommented(node) ||
(node.parent && !isCallExpression(node.parent))
) {
return;
}
const parentExpression = node.parent as CallExpression;
if (
parentExpression?.arguments?.[0] &&
(isArrowFunction(parentExpression.arguments[0]) ||
isFunctionExpression(parentExpression.arguments[0]))
) {
const isUsingNestedCyCommand =
tsquery.query<PropertyAccessExpression>(
parentExpression.arguments[0],
'CallExpression > PropertyAccessExpression:has(Identifier[name="cy"])'
)?.length > 0;
if (isUsingNestedCyCommand) {
return `/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
${node.getText()}`;
}
return node.getText();
}
}
);
tree.write(filePath, markedNestedCyCommands);
}
/**
* leave a comment on all usages of overriding built-ins that are now banned
* */
export function shouldNotOverrideCommands(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedOverrideUsage = tsquery.replace(
content,
'PropertyAccessExpression:has(Identifier[name="overwrite"]):has(Identifier[name="Cypress"])',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent grabbing other Cypress.<something>.defaults
if (expression === 'Cypress.Commands') {
// get value.
const overwriteExpression = node.parent as CallExpression;
const command = (overwriteExpression.arguments?.[0] as any)?.text; // need string without quotes
if (BANNED_COMMANDS.includes(command)) {
// overwrite
return `/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_${command}'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
${node.getText()}`;
}
}
}
);
tree.write(filePath, markedOverrideUsage);
}
export default updateToCypress12;

View File

@ -1,8 +1,8 @@
export const nxVersion = require('../../package.json').version;
export const eslintPluginCypressVersion = '^2.10.3';
export const typesNodeVersion = '16.11.7';
export const cypressVersion = '^11.0.0';
export const cypressViteDevServerVersion = '^2.2.1';
export const cypressVersion = '^12.2.0';
export const cypressWebpackVersion = '^2.0.0';
export const webpackHttpPluginVersion = '^5.5.0';
export const viteVersion = '^4.0.1';