From ea952fb283e673884a5e28ea6273e6c4266836ab Mon Sep 17 00:00:00 2001 From: FrozenPandaz Date: Fri, 17 Nov 2023 10:59:17 -0500 Subject: [PATCH] fix(testing): update the cypress plugin implementation --- packages/cypress/migrations.json | 5 - packages/cypress/plugins/cypress-preset.ts | 95 ++++++---- .../add-nx-cypress-plugin.spec.ts | 170 ------------------ .../update-17-2-0/add-nx-cypress-plugin.ts | 24 --- packages/cypress/src/plugins/plugin.spec.ts | 103 ++++++++--- packages/cypress/src/plugins/plugin.ts | 96 +++++----- 6 files changed, 177 insertions(+), 316 deletions(-) delete mode 100644 packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.spec.ts delete mode 100644 packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.ts diff --git a/packages/cypress/migrations.json b/packages/cypress/migrations.json index e502df40c652b5..18cdc7dfe619aa 100644 --- a/packages/cypress/migrations.json +++ b/packages/cypress/migrations.json @@ -52,11 +52,6 @@ "version": "17.2.0-beta.0", "description": "Add devServerTargets into cypress.config.ts files for @nx/cypress/plugin", "implementation": "./src/migrations/update-17-2-0/add-dev-server-targets-to-cypress-configs" - }, - "add-nx-cypress-plugin": { - "version": "17.2.0-beta.0", - "description": "Add the @nx/cypress/plugin to nx.json plugins", - "implementation": "./src/migrations/update-17-2-0/add-nx-cypress-plugin" } }, "packageJsonUpdates": { diff --git a/packages/cypress/plugins/cypress-preset.ts b/packages/cypress/plugins/cypress-preset.ts index 95b7e1e461a142..ed62b6a8c3de66 100644 --- a/packages/cypress/plugins/cypress-preset.ts +++ b/packages/cypress/plugins/cypress-preset.ts @@ -1,6 +1,5 @@ import { createProjectGraphAsync, - logger, parseTargetString, workspaceRoot, } from '@nx/devkit'; @@ -8,7 +7,6 @@ import { dirname, join, relative } from 'path'; import { lstatSync } from 'fs'; import vitePreprocessor from '../src/plugins/preprocessor-vite'; -import { ChildProcess, fork } from 'node:child_process'; import { createExecutorContext } from '../src/utils/ct-helpers'; import { startDevServer } from '../src/utils/start-dev-server'; @@ -90,46 +88,56 @@ export function nxE2EPreset( devServerTargetOptions: {}, ciDevServerTarget: options?.ciDevServerTarget, }, - async setupNodeEvents(on, config) { - if (options?.bundler === 'vite') { - on('file:preprocessor', vitePreprocessor()); - } - if (!config.env.devServerTargets) { - return; - } - const devServerTarget = - config.env.devServerTarget ?? config.env.devServerTargets['default']; - - if (!devServerTarget) { - return; - } - if (!config.baseUrl && devServerTarget) { - const graph = await createProjectGraphAsync(); - const target = parseTargetString(devServerTarget, graph); - const context = createExecutorContext( - graph, - graph.nodes[target.project].data?.targets, - target.project, - target.target, - target.configuration - ); + setupNodeEvents(on, config) { + return new Promise(async (resolve, reject) => { + if (options?.bundler === 'vite') { + on('file:preprocessor', vitePreprocessor()); + } + if (!config.env.devServerTargets) { + resolve(undefined); + return; + } + const devServerTarget = + config.env.devServerTarget ?? config.env.devServerTargets['default']; - const devServer = startDevServer( - { - devServerTarget, - ...config.env.devServerTargetOptions, - }, - context - ); - on('after:run', () => { - devServer.return(); - }); - const devServerValue = (await devServer.next()).value; - if (!devServerValue) { + if (!devServerTarget) { + resolve(undefined); return; } - return { ...config, baseUrl: devServerValue.baseUrl }; - } + if (!config.baseUrl && devServerTarget) { + const graph = await createProjectGraphAsync(); + const target = parseTargetString(devServerTarget, graph); + const context = createExecutorContext( + graph, + graph.nodes[target.project].data?.targets, + target.project, + target.target, + target.configuration + ); + + const devServer = startDevServer( + { + devServerTarget, + ...config.env.devServerTargetOptions, + }, + context + ); + on('after:run', () => { + devServer.return(); + }); + process.on('message', (msg) => { + if (isServerReadyMessage(msg)) { + resolve({ ...config, baseUrl: msg.baseUrl }); + } + }); + const devServerValue = (await devServer.next()).value; + if (!devServerValue) { + resolve(undefined); + return; + } + resolve({ ...config, baseUrl: devServerValue.baseUrl }); + } + }); }, }; @@ -148,3 +156,12 @@ export type NxCypressE2EPresetOptions = { devServerTargets?: Record; ciDevServerTarget?: string; }; +function isServerReadyMessage( + msg: any +): msg is { type: string; baseUrl: string } { + return ( + typeof msg === 'object' && + msg.type === 'dev-server-ready' && + typeof msg.baseUrl === 'string' + ); +} diff --git a/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.spec.ts b/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.spec.ts deleted file mode 100644 index 26979c983990ec..00000000000000 --- a/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { - readProjectConfiguration, - Tree, - updateProjectConfiguration, -} from '@nx/devkit'; - -import update from './add-nx-cypress-plugin'; -import { defineConfig } from 'cypress'; -import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; -import { join } from 'path'; - -describe('add-nx-cypress-plugin migration', () => { - let tree: Tree; - let tempFs: TempFs; - - function mockCypressConfig(cypressConfig: Cypress.ConfigOptions) { - jest.mock( - join(tempFs.tempDir, 'e2e/cypress.config.ts'), - () => ({ - default: cypressConfig, - }), - { - virtual: true, - } - ); - } - - beforeEach(async () => { - tempFs = new TempFs('test'); - tree = createTreeWithEmptyWorkspace(); - tree.root = tempFs.tempDir; - await tempFs.createFiles({ - 'e2e/cypress.config.ts': '', - 'e2e/project.json': '{ "name": "e2e" }', - }); - tree.write('e2e/cypress.config.ts', `console.log('hi');`); - }); - - afterEach(() => { - jest.resetModules(); - tempFs.cleanup(); - }); - - it('should remove the e2e target when there are no other options', async () => { - mockCypressConfig( - defineConfig({ - env: { - devServerTargets: { - default: 'my-app:serve', - production: 'my-app:serve:production', - }, - ciDevServerTarget: 'my-app:serve-static', - }, - e2e: { - specPattern: '**/*.cy.ts', - }, - }) - ); - updateProjectConfiguration(tree, 'e2e', { - name: 'e2e', - root: 'e2e', - targets: { - e2e: { - executor: '@nx/cypress:cypress', - options: { - devServerTarget: 'my-app:serve', - }, - configurations: { - production: { - devServerTarget: 'my-app:serve:production', - }, - ci: { - devServerTarget: 'my-app:serve-static', - }, - }, - }, - }, - }); - - await update(tree); - - expect(readProjectConfiguration(tree, 'e2e').targets.e2e).toEqual({ - configurations: { - ci: { - devServerTarget: 'my-app:serve-static', - }, - }, - }); - }); - - it('should not the e2e target when it uses a different executor', async () => { - const e2eTarget = { - executor: '@nx/playwright:playwright', - options: { - devServerTarget: 'my-app:serve', - }, - configurations: { - production: { - devServerTarget: 'my-app:serve:production', - }, - ci: { - devServerTarget: 'my-app:serve-static', - }, - }, - }; - updateProjectConfiguration(tree, 'e2e', { - root: 'e2e', - targets: { - e2e: e2eTarget, - }, - }); - - await update(tree); - - expect(readProjectConfiguration(tree, 'e2e').targets.e2e).toEqual( - e2eTarget - ); - }); - - it('should leave the e2e target with other options', async () => { - mockCypressConfig( - defineConfig({ - env: { - devServerTargets: { - default: 'my-app:serve', - production: 'my-app:serve:production', - }, - ciDevServerTarget: 'my-app:serve-static', - }, - e2e: { - specPattern: '**/*.cy.ts', - }, - }) - ); - updateProjectConfiguration(tree, 'e2e', { - root: 'e2e', - targets: { - e2e: { - executor: '@nx/cypress:cypress', - options: { - devServerTarget: 'my-app:serve', - watch: false, - }, - configurations: { - production: { - devServerTarget: 'my-app:serve:production', - }, - ci: { - devServerTarget: 'my-app:serve-static', - }, - }, - }, - }, - }); - - await update(tree); - - expect(readProjectConfiguration(tree, 'e2e').targets.e2e).toEqual({ - options: { - watch: false, - }, - configurations: { - ci: { - devServerTarget: 'my-app:serve-static', - }, - }, - }); - }); -}); diff --git a/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.ts b/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.ts deleted file mode 100644 index 3f3a4a57b36a23..00000000000000 --- a/packages/cypress/src/migrations/update-17-2-0/add-nx-cypress-plugin.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { formatFiles, getProjects, Tree } from '@nx/devkit'; -import { createNodes } from '../../plugins/plugin'; - -import { createProjectRootMappingsFromProjectConfigurations } from 'nx/src/project-graph/utils/find-project-for-path'; -import { replaceProjectConfigurationsWithPlugin } from '@nx/devkit/src/utils/replace-project-configuration-with-plugin'; - -export default async function update(tree: Tree) { - const proj = Object.fromEntries(getProjects(tree).entries()); - - const rootMappings = createProjectRootMappingsFromProjectConfigurations(proj); - - await replaceProjectConfigurationsWithPlugin( - tree, - rootMappings, - '@nx/cypress/plugin', - createNodes, - { - targetName: 'e2e', - componentTestingTargetName: 'component-test', - } - ); - - await formatFiles(tree); -} diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index 9e161e17de8bf2..2224707c3b84c7 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -37,6 +37,12 @@ describe('@nx/cypress/plugin', () => { mockCypressConfig( defineConfig({ e2e: { + env: { + devServerTargets: { + default: 'my-app:serve', + production: 'my-app:serve:production', + }, + }, videosFolder: './dist/videos', screenshotsFolder: './dist/screenshots', }, @@ -57,19 +63,27 @@ describe('@nx/cypress/plugin', () => { "projectType": "application", "targets": { "e2e": { - "cache": true, - "executor": "@nx/cypress:cypress", + "cache": false, + "command": "cypress run --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve", + "configurations": { + "production": { + "command": "cypress run --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve:production", + }, + }, "inputs": [ "default", "^production", + { + "externalDependencies": [ + "cypress", + ], + }, ], "options": { - "cypressConfig": "cypress.config.js", - "testingType": "e2e", + "cwd": ".", + "devServerTarget": "my-app:serve", }, "outputs": [ - "{options.videosFolder}", - "{options.screenshotsFolder}", "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], @@ -110,18 +124,20 @@ describe('@nx/cypress/plugin', () => { "targets": { "component-test": { "cache": true, - "executor": "@nx/cypress:cypress", + "command": "cypress open --config-file cypress.config.js --component", "inputs": [ "default", "^production", + { + "externalDependencies": [ + "cypress", + ], + }, ], "options": { - "cypressConfig": "cypress.config.js", - "testingType": "component", + "cwd": ".", }, "outputs": [ - "{options.videosFolder}", - "{options.screenshotsFolder}", "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], @@ -138,6 +154,8 @@ describe('@nx/cypress/plugin', () => { defineConfig({ e2e: { specPattern: '**/*.cy.ts', + videosFolder: './dist/videos', + screenshotsFolder: './dist/screenshots', env: { devServerTargets: { default: 'my-app:serve', @@ -163,25 +181,29 @@ describe('@nx/cypress/plugin', () => { "projectType": "application", "targets": { "e2e": { - "cache": true, + "cache": false, + "command": "cypress open --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve", "configurations": { "production": { - "devServerTarget": "my-app:serve:production", + "command": "cypress open --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve:production", }, }, - "executor": "@nx/cypress:cypress", "inputs": [ "default", "^production", + { + "externalDependencies": [ + "cypress", + ], + }, ], "options": { - "cypressConfig": "cypress.config.js", + "cwd": ".", "devServerTarget": "my-app:serve", - "testingType": "e2e", }, "outputs": [ - "{options.videosFolder}", - "{options.screenshotsFolder}", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, "e2e-ci": { @@ -197,29 +219,56 @@ describe('@nx/cypress/plugin', () => { "inputs": [ "default", "^production", + { + "externalDependencies": [ + "cypress", + ], + }, ], "outputs": [ - "{options.videosFolder}", - "{options.screenshotsFolder}", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, "e2e-ci--test.cy.ts": { "cache": true, + "command": "cypress run --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve-static --spec test.cy.ts", "configurations": undefined, - "executor": "@nx/cypress:cypress", "inputs": [ "default", "^production", + { + "externalDependencies": [ + "cypress", + ], + }, + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", + ], + }, + "run-e2e": { + "cache": true, + "command": "cypress run --config-file cypress.config.js --e2e --env.devServerTarget my-app:serve", + "inputs": [ + "default", + "^production", + { + "externalDependencies": [ + "cypress", + ], + }, ], "options": { - "cypressConfig": "cypress.config.js", - "devServerTarget": "my-app:serve-static", - "spec": "test.cy.ts", - "testingType": "e2e", + "cwd": ".", }, "outputs": [ - "{options.videosFolder}", - "{options.screenshotsFolder}", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, }, diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index 97cf61d401c468..1fb994463407f3 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -2,11 +2,12 @@ import { CreateDependencies, CreateNodes, CreateNodesContext, + NxJsonConfiguration, readJsonFile, TargetConfiguration, writeJsonFile, } from '@nx/devkit'; -import { dirname, extname, join } from 'path'; +import { dirname, extname, join, relative } from 'path'; import { registerTsProject } from '@nx/js/src/internal'; import { getRootTsConfigPath } from '@nx/js'; @@ -102,7 +103,7 @@ function getOutputs( } const { screenshotsFolder, videosFolder, e2e, component } = cypressConfig; - const outputs = ['{options.videosFolder}', '{options.screenshotsFolder}']; + const outputs = []; if (videosFolder) { outputs.push(getOutput(videosFolder)); @@ -143,44 +144,39 @@ function buildCypressTargets( ) { const cypressConfig = getCypressConfig(configFilePath, context); - const namedInputs = getNamedInputs(projectRoot, context); - - const baseTargetConfig: TargetConfiguration = { - executor: '@nx/cypress:cypress', - options: { - cypressConfig: configFilePath, - }, + const cypressEnv = { + ...cypressConfig.env, + ...cypressConfig.e2e?.env, }; - const targets: Record< - string, - TargetConfiguration - > = {}; + const devServerTargets: Record = cypressEnv?.devServerTargets; - if ('e2e' in cypressConfig) { - const e2eTargetDefaults = readTargetDefaultsForTarget( - options.targetName, - context.nxJsonConfiguration.targetDefaults, - '@nx/cypress:cypress' - ); + const relativeConfigPath = relative(projectRoot, configFilePath); + + const namedInputs = getNamedInputs(projectRoot, context); + const targets: Record = {}; + + if ('e2e' in cypressConfig) { targets[options.targetName] = { - ...baseTargetConfig, + command: `cypress run --config-file ${relativeConfigPath} --e2e --env.devServerTarget ${devServerTargets?.default}`, options: { - ...baseTargetConfig.options, - testingType: 'e2e', + cwd: projectRoot, }, }; + const e2eTargetDefaults = readTargetDefaultsForTarget( + options.targetName, + context.nxJsonConfiguration.targetDefaults, + 'run-commands' + ); + if (e2eTargetDefaults?.cache === undefined) { - targets[options.targetName].cache = true; + targets[options.targetName].cache = false; } if (e2eTargetDefaults?.inputs === undefined) { - targets[options.targetName].inputs = - 'production' in namedInputs - ? ['default', '^production'] - : ['default', '^default']; + targets[options.targetName].inputs = getInputs(namedInputs); } if (e2eTargetDefaults?.outputs === undefined) { @@ -191,14 +187,6 @@ function buildCypressTargets( ); } - const cypressEnv = { - ...cypressConfig.env, - ...cypressConfig.e2e?.env, - }; - - const devServerTargets: Record = - cypressEnv?.devServerTargets; - if (devServerTargets?.default) { targets[options.targetName].options.devServerTarget = devServerTargets.default; @@ -211,7 +199,7 @@ function buildCypressTargets( devServerTargets ?? {} )) { targets[options.targetName].configurations[configuration] = { - devServerTarget, + command: `cypress run --config-file ${relativeConfigPath} --e2e --env.devServerTarget ${devServerTarget}`, }; } } @@ -236,22 +224,18 @@ function buildCypressTargets( const dependsOn: TargetConfiguration['dependsOn'] = []; const outputs = getOutputs(projectRoot, cypressConfig, 'e2e'); - const inputs = - 'production' in namedInputs - ? ['default', '^production'] - : ['default', '^default']; + const inputs = getInputs(namedInputs); for (const file of specFiles) { - const targetName = options.ciTargetName + '--' + file; + const relativeSpecFilePath = relative(projectRoot, file); + const targetName = options.ciTargetName + '--' + relativeSpecFilePath; targets[targetName] = { - ...targets[options.targetName], outputs, inputs, cache: true, + command: `cypress run --config-file ${relativeConfigPath} --e2e --env.devServerTarget ${ciDevServerTarget} --spec ${relativeSpecFilePath}`, configurations: undefined, options: { - ...targets[options.targetName].options, - devServerTarget: ciDevServerTarget, - spec: file, + ...targets['run-' + options.targetName].options, }, }; dependsOn.push({ @@ -281,10 +265,9 @@ function buildCypressTargets( // This will not override the e2e target if it is the same targets[options.componentTestingTargetName] ??= { - ...baseTargetConfig, + command: `cypress open --config-file ${relativeConfigPath} --component`, options: { - ...baseTargetConfig.options, - testingType: 'component', + cwd: projectRoot, }, }; @@ -294,9 +277,7 @@ function buildCypressTargets( if (componentTestingTargetDefaults?.inputs === undefined) { targets[options.componentTestingTargetName].inputs = - 'production' in namedInputs - ? ['default', '^production'] - : ['default', '^default']; + getInputs(namedInputs); } if (componentTestingTargetDefaults?.outputs === undefined) { @@ -344,3 +325,16 @@ function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions { options.ciTargetName ??= 'e2e-ci'; return options; } +function getInputs( + namedInputs: NxJsonConfiguration['namedInputs'] +): TargetConfiguration['inputs'] { + return [ + ...('production' in namedInputs + ? ['default', '^production'] + : ['default', '^default']), + + { + externalDependencies: ['cypress'], + }, + ]; +}