From 74e3b1b879e62bb0b6e94aeed22f9a8b50c90e9f Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 2 Aug 2024 16:47:14 +0100 Subject: [PATCH] fix(testing): add migration for playwright e2e-ci --- .../add-e2e-ci-target-defaults.ts | 24 +- .../add-e2e-ci-target-defaults.spec.ts | 333 ++++++++++++++++++ .../add-e2e-ci-target-defaults.ts | 112 ++++++ .../use-serve-static-preview-for-command.ts | 2 + 4 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts create mode 100644 packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts index 2d53c1506f2f18..ae01f7061be12e 100644 --- a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -78,23 +78,17 @@ export default async function addE2eCiTargetDefaults(tree: Tree) { ); const serveStaticTarget = graph.nodes[project].data.targets[target]; - if ( - !serveStaticTarget.options.buildTarget && - configuration && - !serveStaticTarget.configurations?.[configuration]?.buildTarget - ) { - continue; + let resolvedBuildTarget: string; + if (serveStaticTarget.dependsOn) { + resolvedBuildTarget = serveStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (configuration + ? serveStaticTarget.configurations[configuration].buildTarget + : serveStaticTarget.options.buildTarget) ?? 'build'; } - const serveStaticBuildTarget = configuration - ? serveStaticTarget.configurations[configuration].buildTarget - : serveStaticTarget.options.buildTarget; - - const buildTarget = - '^' + - (serveStaticBuildTarget.includes(':') - ? parseTargetString(serveStaticBuildTarget, graph).target - : serveStaticBuildTarget); + const buildTarget = `^${resolvedBuildTarget}`; await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); } diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts new file mode 100644 index 00000000000000..ba65c8f32bf68c --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts @@ -0,0 +1,333 @@ +import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('add-e2e-ci-target-defaults', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing when the plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = []; + updateNxJson(tree, nxJson); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['app-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + include: ['shop-e2e/**'], + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['cart-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'bundle', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + "^bundle", + ], + }, + } + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const playwrightConfig = `import { defineConfig, devices } from '@playwright/test'; +import { nxE2EPreset } from '@nx/playwright/preset'; + +import { workspaceRoot } from '@nx/devkit'; + +const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; + +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run ${overrides.appName}:serve-static', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});`; + + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/playwright.config.ts`, playwrightConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + tempFs.createFilesSync({ + [`${overrides.appName}/vite.config.ts`]: viteConfig, + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/playwright.config.ts`]: playwrightConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + dependsOn: [overrides.buildTargetName], + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts new file mode 100644 index 00000000000000..d46a5c57955b68 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -0,0 +1,112 @@ +import { + type Tree, + type CreateNodesV2, + formatFiles, + readNxJson, + createProjectGraphAsync, + parseTargetString, +} from '@nx/devkit'; +import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; +import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { + ProjectConfigurationsError, + retrieveProjectConfigurations, +} from 'nx/src/devkit-internals'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { type PlaywrightPluginOptions } from '../../plugins/plugin'; + +export default async function addE2eCiTargetDefaults(tree: Tree) { + const pluginName = '@nx/playwright/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + let serveStaticProject: string; + let serveStaticTarget: string; + let serveStaticConfiguration: string; + if (ciWebServerCommand.includes('nx run')) { + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + serveStaticProject = project; + serveStaticTarget = target; + serveStaticConfiguration = configuration; + } else { + const NX_PROJECT_REGEX = 'nx\\s+([^ ]+)\\s+([^ ]+)'; + const matches = ciWebServerCommand.match(NX_PROJECT_REGEX); + if (!matches) { + return; + } + serveStaticTarget = matches[1]; + serveStaticProject = matches[2]; + } + + const resolvedServeStaticTarget = + graph.nodes[serveStaticProject].data.targets[serveStaticTarget]; + + let resolvedBuildTarget: string; + if (resolvedServeStaticTarget.dependsOn) { + resolvedBuildTarget = resolvedServeStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (serveStaticConfiguration + ? resolvedServeStaticTarget.configurations[serveStaticConfiguration] + .buildTarget + : resolvedServeStaticTarget.options.buildTarget) ?? 'build'; + } + + const buildTarget = `^${resolvedBuildTarget}`; + + await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); + } + } +} diff --git a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts index c98bfff41a0313..88b58a6e6ddfc2 100644 --- a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts +++ b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts @@ -8,6 +8,7 @@ import { visitNotIgnoredFiles, } from '@nx/devkit'; import { tsquery } from '@phenomnomnominal/tsquery'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; export default async function (tree: Tree) { const graph = await createProjectGraphAsync(); @@ -138,5 +139,6 @@ export default async function (tree: Tree) { } }); + await addE2eCiTargetDefaults(tree); await formatFiles(tree); }