nx/packages/cypress/src/generators/configuration/configuration.spec.ts
Leosvel Pérez Espinosa 5feafd64d4
feat(testing): add support for cypress v14 (#30618)
## Current Behavior

Cypress v14 is not supported.

## Expected Behavior

Cypress v14 is supported.

## Related Issue(s)

Fixes #30097
2025-04-09 17:12:39 -04:00

708 lines
20 KiB
TypeScript

import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
ProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import cypressE2EConfigurationGenerator from './configuration';
import { cypressInitGenerator } from '../init/init';
describe('Cypress e2e configuration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write('.eslintrc.json', '{}'); // we are explicitly checking for existance of config type
});
it('should add web server commands to the cypress config when the @nx/cypress/plugin is present', async () => {
await cypressInitGenerator(tree, {
addPlugin: true,
});
addProject(tree, { name: 'my-app', type: 'apps' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
baseUrl: 'http://localhost:4200',
webServerCommands: {
default: 'nx run my-app:serve',
production: 'nx run my-app:serve:production',
},
ciWebServerCommand: 'nx run my-app:serve-static',
addPlugin: true,
});
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
webServerCommands: {
default: 'nx run my-app:serve',
production: 'nx run my-app:serve:production',
},
ciWebServerCommand: 'nx run my-app:serve-static',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
expect(
readProjectConfiguration(tree, 'my-app').targets.e2e
).toBeUndefined();
expect(readJson(tree, 'apps/my-app/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": [
"cypress",
"node",
],
},
"extends": "../../tsconfig.base.json",
"include": [
"**/*.ts",
"**/*.js",
"cypress.config.ts",
"**/*.cy.ts",
"**/*.cy.js",
"**/*.d.ts",
],
}
`);
assertCypressFiles(tree, 'apps/my-app/src');
});
it('should add e2e target to existing app when not using plugin', async () => {
addProject(tree, { name: 'my-app', type: 'apps' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
addPlugin: false,
});
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
}),
},
});
"
`);
expect(readProjectConfiguration(tree, 'my-app').targets.e2e)
.toMatchInlineSnapshot(`
{
"configurations": {
"ci": {
"devServerTarget": "my-app:serve-static",
},
"production": {
"devServerTarget": "my-app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/my-app/cypress.config.ts",
"devServerTarget": "my-app:serve",
"testingType": "e2e",
},
}
`);
expect(readJson(tree, 'apps/my-app/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": [
"cypress",
"node",
],
},
"extends": "../../tsconfig.base.json",
"include": [
"**/*.ts",
"**/*.js",
"cypress.config.ts",
"**/*.cy.ts",
"**/*.cy.js",
"**/*.d.ts",
],
}
`);
assertCypressFiles(tree, 'apps/my-app/src');
});
it('should add e2e target to existing lib', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
addProject(tree, { name: 'my-app', type: 'apps' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
directory: 'cypress',
devServerTarget: 'my-app:serve',
addPlugin: true,
});
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'cypress',
webServerCommands: {
default: 'nx run my-app:serve',
production: 'nx run my-app:serve:production',
},
ciWebServerCommand: 'nx run my-app:serve-static',
}),
},
});
"
`);
assertCypressFiles(tree, 'libs/my-lib/cypress');
});
it('should use --baseUrl', async () => {
addProject(tree, { name: 'my-app', type: 'apps' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
baseUrl: 'http://localhost:4200',
addPlugin: true,
});
assertCypressFiles(tree, 'apps/my-app/src');
expect(tree.read('apps/my-app/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
webServerCommands: {
default: 'nx run my-app:serve',
production: 'nx run my-app:serve:production',
},
ciWebServerCommand: 'nx run my-app:serve-static',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should not overwrite existing e2e target', async () => {
addProject(tree, { name: 'my-app', type: 'apps' });
const pc = readProjectConfiguration(tree, 'my-app');
pc.targets.e2e = {};
updateProjectConfiguration(tree, 'my-app', pc);
await expect(async () => {
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
addPlugin: true,
});
}).rejects.toThrowErrorMatchingInlineSnapshot(`
"Project my-app already has an e2e target.
Rename or remove the existing e2e target."
`);
});
it('should customize directory name', async () => {
addProject(tree, { name: 'my-app', type: 'apps' });
tree.write(
'apps/my-app/tsconfig.json',
JSON.stringify(
{
compilerOptions: {
target: 'es2022',
useDefineForClassFields: false,
forceConsistentCasingInFileNames: true,
strict: true,
noImplicitOverride: true,
noPropertyAccessFromIndexSignature: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
},
files: [],
include: [],
references: [
{
path: './tsconfig.app.json',
},
{
path: './tsconfig.spec.json',
},
{
path: './tsconfig.editor.json',
},
],
extends: '../../tsconfig.base.json',
angularCompilerOptions: {
enableI18nLegacyMessageIdFormat: false,
strictInjectionParameters: true,
strictInputAccessModifiers: true,
strictTemplates: true,
},
},
null,
2
)
);
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
directory: 'e2e/something',
addPlugin: true,
});
assertCypressFiles(tree, 'apps/my-app/e2e/something');
expect(readJson(tree, 'apps/my-app/e2e/something/tsconfig.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": [
"cypress",
"node",
],
},
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.js",
"../../cypress.config.ts",
"../../**/*.cy.ts",
"../../**/*.cy.js",
"../../**/*.d.ts",
],
}
`);
expect(readJson(tree, 'apps/my-app/tsconfig.json').references).toEqual(
expect.arrayContaining([{ path: './e2e/something/tsconfig.json' }])
);
});
it('should use js instead of ts files with --js', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
directory: 'src/e2e',
js: true,
baseUrl: 'http://localhost:4200',
addPlugin: true,
});
assertCypressFiles(tree, 'libs/my-lib/src/e2e', 'js');
});
it('should not override eslint settings if preset', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
const ngEsLintContents = {
extends: ['../../.eslintrc.json'],
ignorePatterns: ['!**/*'],
overrides: [
{
files: ['*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'cyPortTest',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'cy-port-test',
style: 'kebab-case',
},
],
},
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
},
{
files: ['*.html'],
extends: ['plugin:@nx/angular-template'],
rules: {},
},
],
};
tree.write(
'libs/my-lib/.eslintrc.json',
JSON.stringify(ngEsLintContents, null, 2)
);
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
directory: 'cypress',
baseUrl: 'http://localhost:4200',
addPlugin: true,
});
expect(readJson(tree, 'libs/my-lib/.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"plugin:cypress/recommended",
"../../.eslintrc.json",
],
"ignorePatterns": [
"!**/*",
],
"overrides": [
{
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": [
"*.ts",
],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"prefix": "cy-port-test",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": [
"error",
{
"prefix": "cyPortTest",
"style": "camelCase",
"type": "attribute",
},
],
},
},
{
"extends": [
"plugin:@nx/angular-template",
],
"files": [
"*.html",
],
"rules": {},
},
{
"files": [
"*.cy.{ts,js,tsx,jsx}",
"cypress/**/*.{ts,js,tsx,jsx}",
],
"rules": {},
},
],
}
`);
});
it('should add serve-static target to CI configuration', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
addProject(tree, { name: 'my-app', type: 'apps' });
const pc = readProjectConfiguration(tree, 'my-app');
pc.targets['serve-static'] = {
executor: 'some-file-server',
};
updateProjectConfiguration(tree, 'my-lib', pc);
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
devServerTarget: 'my-app:serve',
directory: 'cypress',
addPlugin: false,
});
assertCypressFiles(tree, 'libs/my-lib/cypress');
expect(
readProjectConfiguration(tree, 'my-lib').targets['e2e'].configurations.ci
).toMatchInlineSnapshot(`
{
"devServerTarget": "my-app:serve-static",
}
`);
});
it('should set --port', async () => {
addProject(tree, { name: 'my-app', type: 'apps' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-app',
port: 0,
addPlugin: false,
});
expect(readProjectConfiguration(tree, 'my-app').targets['e2e'].options)
.toMatchInlineSnapshot(`
{
"cypressConfig": "apps/my-app/cypress.config.ts",
"devServerTarget": "my-app:serve",
"port": 0,
"testingType": "e2e",
}
`);
});
it('should add e2e to an existing config', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
tree.write(
'libs/my-lib/cypress.config.ts',
`import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
});
`
);
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
baseUrl: 'http://localhost:4200',
addPlugin: true,
});
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
// these files are only added when there isn't already a cypress config
expect(
tree.exists('libs/my-lib/cypress/fixtures/example.json')
).toBeFalsy();
expect(tree.exists('libs/my-lib/cypress/support/commands.ts')).toBeFalsy();
});
it('should not throw if e2e is already defined', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
tree.write(
'libs/my-lib/cypress.config.ts',
`import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {exists: true},
});
`
);
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
baseUrl: 'http://localhost:4200',
addPlugin: true,
});
expect(tree.read('libs/my-lib/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig } from 'cypress';
export default defineConfig({
e2e: { exists: true },
});
"
`);
});
it('should support --js option with CommonJS format', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
baseUrl: 'http://localhost:4200',
js: true,
});
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { nxE2EPreset } = require('@nx/cypress/plugins/cypress-preset');
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should support --js option with ESM format', async () => {
// When type is "module", Node will treat .js files as ESM format.
updateJson(tree, 'package.json', (json) => {
json.type = 'module';
return json;
});
addProject(tree, { name: 'my-lib', type: 'libs' });
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
baseUrl: 'http://localhost:4200',
js: true,
});
expect(tree.read('libs/my-lib/cypress.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
describe('TS Solution Setup', () => {
beforeEach(() => {
updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(tree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(tree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
});
it('should handle existing tsconfig.json files', async () => {
addProject(tree, { name: 'my-lib', type: 'libs' });
writeJson(tree, 'libs/my-lib/tsconfig.json', {
include: [],
files: [],
references: [],
});
await cypressE2EConfigurationGenerator(tree, {
project: 'my-lib',
baseUrl: 'http://localhost:4200',
js: true,
});
expect(tree.read('libs/my-lib/tsconfig.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"include": [],
"files": [],
"references": [
{
"path": "./src/tsconfig.json"
}
]
}
"
`);
expect(tree.read('libs/my-lib/src/tsconfig.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "out-tsc/cypress",
"allowJs": true,
"types": ["cypress", "node"],
"sourceMap": false
},
"include": [
"**/*.ts",
"**/*.js",
"../cypress.config.ts",
"../**/*.cy.ts",
"../**/*.cy.js",
"../**/*.d.ts"
],
"exclude": ["out-tsc", "test-output"]
}
"
`);
});
});
});
function addProject(
tree: Tree,
opts: { name: string; standalone?: boolean; type: 'apps' | 'libs' }
) {
const config: ProjectConfiguration = {
name: opts.name,
root: `${opts.type}/${opts.name}`,
sourceRoot: `${opts.type}/${opts.name}`,
targets: {
serve:
opts.type === 'apps'
? {
configurations: {
production: {},
},
}
: undefined,
'serve-static': opts.type === 'apps' ? {} : undefined,
},
};
addProjectConfiguration(tree, opts.name, config);
}
function assertCypressFiles(tree: Tree, basePath: string, ext = 'ts') {
expect(tree.exists(`${basePath}/fixtures/example.json`)).toBeTruthy();
expect(tree.exists(`${basePath}/support/e2e.${ext}`)).toBeTruthy();
expect(tree.exists(`${basePath}/support/commands.${ext}`)).toBeTruthy();
expect(tree.exists(`${basePath}/support/app.po.${ext}`)).toBeTruthy();
expect(tree.exists(`${basePath}/e2e/app.cy.${ext}`)).toBeTruthy();
}