diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 86b1c286b1..ce544edfe3 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2841,7 +2841,7 @@ "type": "generator" }, "/nx-api/storybook/generators/cypress-project": { - "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", + "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", "file": "generated/packages/storybook/generators/cypress-project.json", "hidden": false, "name": "cypress-project", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 487165933f..e508db9895 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2811,7 +2811,7 @@ "type": "generator" }, { - "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", + "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", "file": "generated/packages/storybook/generators/cypress-project.json", "hidden": false, "name": "cypress-project", diff --git a/docs/generated/packages/storybook/generators/cypress-project.json b/docs/generated/packages/storybook/generators/cypress-project.json index cf1579a86e..6ad6af0e5f 100644 --- a/docs/generated/packages/storybook/generators/cypress-project.json +++ b/docs/generated/packages/storybook/generators/cypress-project.json @@ -7,6 +7,7 @@ "$id": "cypress-configure", "title": "Cypress Configuration", "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", + "x-deprecated": "Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.", "type": "object", "properties": { "name": { @@ -53,7 +54,8 @@ "required": ["name"], "presets": [] }, - "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", + "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", + "x-deprecated": "Deprecated: Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.", "hidden": false, "implementation": "/packages/storybook/src/generators/cypress-project/cypress-project.ts", "aliases": [], diff --git a/packages/storybook/generators.json b/packages/storybook/generators.json index ad9d9cef04..ac3469bcfe 100644 --- a/packages/storybook/generators.json +++ b/packages/storybook/generators.json @@ -18,7 +18,8 @@ "cypress-project": { "factory": "./src/generators/cypress-project/cypress-project", "schema": "./src/generators/cypress-project/schema.json", - "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", + "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", + "x-deprecated": "Deprecated: Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.", "hidden": false }, "migrate-7": { diff --git a/packages/storybook/presets/cypress.ts b/packages/storybook/presets/cypress.ts index 30d5e9bf37..4c11545d5d 100644 --- a/packages/storybook/presets/cypress.ts +++ b/packages/storybook/presets/cypress.ts @@ -1,8 +1,14 @@ -import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { + NxCypressE2EPresetOptions, + nxE2EPreset, +} from '@nx/cypress/plugins/cypress-preset'; -export function nxE2EStorybookPreset(filePath: string) { +export function nxE2EStorybookPreset( + filePath: string, + options?: NxCypressE2EPresetOptions +) { return { - ...nxE2EPreset(filePath), + ...nxE2EPreset(filePath, options), baseUrl: 'http://localhost:4400', }; } diff --git a/packages/storybook/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap b/packages/storybook/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap index a7beee5475..a9cf97371d 100644 --- a/packages/storybook/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap +++ b/packages/storybook/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap @@ -1,5 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`@nx/storybook:cypress-project should generate a correct cypress.config.ts file when using inferred plugins 1`] = ` +"import { defineConfig } from 'cypress'; +import { nxE2EStorybookPreset } from '@nx/storybook/presets/cypress'; + +export default defineConfig({ + e2e: nxE2EStorybookPreset(__dirname, { + cypressDir: 'src', + webServerCommands: { + default: 'npx nx run test-ui-lib:storybook', + ci: 'npx nx run test-ui-lib:storybook:ci', + }, + ciWebServerCommand: 'npx nx run test-ui-lib:storybook:ci', + }), +}); +" +`; + exports[`@nx/storybook:cypress-project should generate files 1`] = ` "import { defineConfig } from 'cypress'; import { nxE2EStorybookPreset } from '@nx/storybook/presets/cypress'; diff --git a/packages/storybook/src/generators/cypress-project/cypress-project.spec.ts b/packages/storybook/src/generators/cypress-project/cypress-project.spec.ts index f30527fc88..6f903d3da2 100644 --- a/packages/storybook/src/generators/cypress-project/cypress-project.spec.ts +++ b/packages/storybook/src/generators/cypress-project/cypress-project.spec.ts @@ -1,4 +1,16 @@ -import { readJson, readProjectConfiguration, Tree } from '@nx/devkit'; +jest.mock('nx/src/project-graph/plugins/loader', () => ({ + ...jest.requireActual('nx/src/project-graph/plugins/loader'), + loadNxPlugin: jest.fn().mockImplementation(() => { + return [Promise.resolve({}), () => {}]; + }), +})); +import { + readJson, + readNxJson, + readProjectConfiguration, + Tree, + updateNxJson, +} from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; import { libraryGenerator } from '@nx/js'; @@ -60,4 +72,26 @@ describe('@nx/storybook:cypress-project', () => { tree.exists('apps/one/two/test-ui-lib-e2e/cypress.config.ts') ).toBeTruthy(); }); + + it('should generate a correct cypress.config.ts file when using inferred plugins', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push('@nx/cypress/plugin'); + updateNxJson(tree, nxJson); + + // ACT + await cypressProjectGenerator(tree, { + name: 'test-ui-lib', + linter: Linter.EsLint, + }); + + // ASSERT + expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeTruthy(); + const cypressConfig = tree.read( + 'apps/test-ui-lib-e2e/cypress.config.ts', + 'utf-8' + ); + expect(cypressConfig).toMatchSnapshot(); + }); }); diff --git a/packages/storybook/src/generators/cypress-project/cypress-project.ts b/packages/storybook/src/generators/cypress-project/cypress-project.ts index 0ce8ef1ffb..5a7d4e9103 100644 --- a/packages/storybook/src/generators/cypress-project/cypress-project.ts +++ b/packages/storybook/src/generators/cypress-project/cypress-project.ts @@ -5,7 +5,9 @@ import { formatFiles, generateFiles, joinPathFragments, + logger, readJson, + readNxJson, readProjectConfiguration, Tree, updateJson, @@ -43,6 +45,9 @@ export async function cypressProjectGeneratorInternal( tree: Tree, schema: CypressConfigureSchema ) { + logger.warn( + `Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.` + ); const { configurationGenerator } = ensurePackage< typeof import('@nx/cypress') >('@nx/cypress', nxVersion); @@ -73,7 +78,6 @@ export async function cypressProjectGeneratorInternal( project: projectName, js: schema.js, linter: schema.linter, - directory: projectRoot, devServerTarget: `${schema.name}:storybook`, skipFormat: true, }); @@ -84,12 +88,28 @@ export async function cypressProjectGeneratorInternal( schema.directory ); removeUnneededFiles(tree, generatedCypressProjectName, schema.js); - addBaseUrlToCypressConfig(tree, generatedCypressProjectName); - updateAngularJsonBuilder(tree, { - e2eProjectName: generatedCypressProjectName, - targetProjectName: schema.name, - ciTargetName: schema.ciTargetName, - }); + + const project = readProjectConfiguration(tree, generatedCypressProjectName); + if (project.targets.e2e && project.targets.e2e.options) { + addBaseUrlToCypressConfig(tree, generatedCypressProjectName, project.root); + updateAngularJsonBuilder(tree, { + e2eProjectName: generatedCypressProjectName, + targetProjectName: schema.name, + ciTargetName: schema.ciTargetName, + }); + } else if (hasCypressPlugin(tree)) { + generateCypressConfigForInferredPlugin( + tree, + generatedCypressProjectName, + project.root, + schema.name, + schema.ciTargetName + ); + } else { + throw new Error( + `Unable to generate Cypress Project for Storybook project. Please report this issue at https://github.com/nrwl/nx/issues/new/choose` + ); + } if (!schema.skipFormat) { await formatFiles(tree); @@ -98,10 +118,19 @@ export async function cypressProjectGeneratorInternal( return cypressTask; } +function hasCypressPlugin(tree: Tree) { + const nxJson = readNxJson(tree); + return nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ); +} + function removeUnneededFiles(tree: Tree, projectName: string, js: boolean) { const { sourceRoot, root } = readProjectConfiguration(tree, projectName); const fileType = js ? 'js' : 'ts'; - if (tree.exists(join(root, 'cypress.config.ts'))) { + if (tree.exists(joinPathFragments(root, 'cypress.config.ts'))) { safeFileDelete(tree, `${sourceRoot}/e2e/app.cy.${fileType}`); safeFileDelete(tree, `${sourceRoot}/support/app.po.${fileType}`); } else { @@ -110,10 +139,44 @@ function removeUnneededFiles(tree: Tree, projectName: string, js: boolean) { } } -function addBaseUrlToCypressConfig(tree: Tree, projectName: string) { - const projectRoot = readProjectConfiguration(tree, projectName).root; - const cypressJson = join(projectRoot, 'cypress.json'); - const cypressTs = join(projectRoot, 'cypress.config.ts'); +function generateCypressConfigForInferredPlugin( + tree: Tree, + projectName: string, + projectRoot: string, + targetProjectName: string, + ciTargetName?: string +) { + const cypressJson = joinPathFragments(projectRoot, 'cypress.json'); + const cypressTs = joinPathFragments(projectRoot, 'cypress.config.ts'); + + if (tree.exists(cypressJson)) { + tree.delete(cypressJson); + } + if (tree.exists(cypressTs)) { + // cypress >= v10 + tree.delete(cypressTs); + generateFiles( + tree, + join(__dirname, 'files', 'inferred-target'), + projectRoot, + { + defaultWebServerCommand: `npx nx run ${targetProjectName}:storybook`, + ciWebServerCommand: ciTargetName + ? `npx nx run ${targetProjectName}:${ciTargetName}:ci` + : `npx nx run ${targetProjectName}:storybook:ci`, + tpl: '', + } + ); + } +} + +function addBaseUrlToCypressConfig( + tree: Tree, + projectName: string, + projectRoot: string +) { + const cypressJson = joinPathFragments(projectRoot, 'cypress.json'); + const cypressTs = joinPathFragments(projectRoot, 'cypress.config.ts'); // TODO(caleb): remove this when cypress < v10 is deprecated if (tree.exists(cypressJson)) { @@ -125,9 +188,14 @@ function addBaseUrlToCypressConfig(tree: Tree, projectName: string) { } else if (tree.exists(cypressTs)) { // cypress >= v10 tree.delete(cypressTs); - generateFiles(tree, join(__dirname, 'files'), projectRoot, { - tpl: '', - }); + generateFiles( + tree, + join(__dirname, 'files', 'explicit-target'), + projectRoot, + { + tpl: '', + } + ); } } @@ -140,7 +208,7 @@ function updateAngularJsonBuilder( } ) { const project = readProjectConfiguration(tree, opts.e2eProjectName); - const e2eTarget = project.targets.e2e; + const e2eTarget = project.targets.e2e ?? {}; project.targets.e2e = { ...e2eTarget, options: { diff --git a/packages/storybook/src/generators/cypress-project/files/cypress.config.ts__tpl__ b/packages/storybook/src/generators/cypress-project/files/explicit-target/cypress.config.ts__tpl__ similarity index 100% rename from packages/storybook/src/generators/cypress-project/files/cypress.config.ts__tpl__ rename to packages/storybook/src/generators/cypress-project/files/explicit-target/cypress.config.ts__tpl__ diff --git a/packages/storybook/src/generators/cypress-project/files/inferred-target/cypress.config.ts__tpl__ b/packages/storybook/src/generators/cypress-project/files/inferred-target/cypress.config.ts__tpl__ new file mode 100644 index 0000000000..2a709f2653 --- /dev/null +++ b/packages/storybook/src/generators/cypress-project/files/inferred-target/cypress.config.ts__tpl__ @@ -0,0 +1,13 @@ +import { defineConfig } from 'cypress'; +import { nxE2EStorybookPreset } from '@nx/storybook/presets/cypress'; + +export default defineConfig({ + e2e: nxE2EStorybookPreset(__dirname, { + cypressDir: 'src', + webServerCommands: { + default: "<%= defaultWebServerCommand %>", + ci: "<%= ciWebServerCommand %>" + }, + ciWebServerCommand: "<%= ciWebServerCommand %>" + }), +}); diff --git a/packages/storybook/src/generators/cypress-project/schema.json b/packages/storybook/src/generators/cypress-project/schema.json index b9255fa84e..36c49ecc35 100644 --- a/packages/storybook/src/generators/cypress-project/schema.json +++ b/packages/storybook/src/generators/cypress-project/schema.json @@ -4,6 +4,7 @@ "$id": "cypress-configure", "title": "Cypress Configuration", "description": "Add cypress E2E app to test a ui library that is set up for Storybook.", + "x-deprecated": "Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.", "type": "object", "properties": { "name": {