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(); }