From fe0f81bf596e3d972349598f7e45ff69d547343b Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Wed, 19 Jul 2023 16:44:25 +0300 Subject: [PATCH] feat(storybook): interaction tests generators for angular --- .../angular/generators/component-story.json | 7 + .../packages/angular/generators/stories.json | 13 +- .../generators/storybook-configuration.json | 6 +- .../packages/react/generators/stories.json | 4 +- .../generators/storybook-configuration.json | 6 +- .../storybook/generators/configuration.json | 4 +- .../src/storybook-angular.test.ts | 81 +---------- .../component-story.spec.ts.snap | 15 +- .../component-story/component-story.ts | 2 + .../__componentFileName__.stories.ts__tmpl__ | 35 +++-- .../generators/component-story/schema.d.ts | 1 + .../generators/component-story/schema.json | 7 + .../__snapshots__/stories-app.spec.ts.snap | 80 ++++++++--- .../__snapshots__/stories-lib.spec.ts.snap | 128 +++++++++++------- .../src/generators/stories/schema.d.ts | 11 +- .../src/generators/stories/schema.json | 13 +- .../generators/stories/stories-app.spec.ts | 35 ++--- .../generators/stories/stories-lib.spec.ts | 102 +------------- .../angular/src/generators/stories/stories.ts | 30 +++- .../storybook-configuration.spec.ts.snap | 122 ++++++++++------- .../lib/generate-stories.ts | 1 + .../storybook-configuration/schema.d.ts | 15 +- .../storybook-configuration/schema.json | 6 +- .../storybook-configuration.spec.ts | 67 ++------- .../storybook-configuration.ts | 13 +- .../utils/storybook-ast/storybook-inputs.ts | 16 +-- .../component-story.spec.ts.snap | 87 ++++++++++++ .../__componentFileName__.stories.tsx__tmpl__ | 3 + .../__snapshots__/stories.app.spec.ts.snap | 5 + .../__snapshots__/stories.lib.spec.ts.snap | 5 + .../react/src/generators/stories/schema.json | 4 +- .../react/src/generators/stories/stories.ts | 36 ++++- .../__snapshots__/configuration.spec.ts.snap | 1 + .../configuration.spec.ts | 2 +- .../storybook-configuration/configuration.ts | 2 +- .../storybook-configuration/schema.d.ts | 15 +- .../storybook-configuration/schema.json | 6 +- packages/react/src/utils/component-props.ts | 13 +- packages/storybook/index.ts | 4 + .../configuration-nested.spec.ts | 2 +- .../configuration/configuration.spec.ts | 7 +- .../generators/configuration/configuration.ts | 16 +-- .../interaction-testing.utils.spec.ts.snap | 31 +++++ .../lib/interaction-testing.utils.spec.ts | 70 ++++++++++ .../lib/interaction-testing.utils.ts | 90 ++++++++++++ .../src/generators/configuration/schema.d.ts | 10 +- .../src/generators/configuration/schema.json | 4 +- 47 files changed, 777 insertions(+), 456 deletions(-) create mode 100644 packages/storybook/src/generators/configuration/lib/__snapshots__/interaction-testing.utils.spec.ts.snap create mode 100644 packages/storybook/src/generators/configuration/lib/interaction-testing.utils.spec.ts create mode 100644 packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts diff --git a/docs/generated/packages/angular/generators/component-story.json b/docs/generated/packages/angular/generators/component-story.json index 5b82d4749c8710..9332a523820300 100644 --- a/docs/generated/packages/angular/generators/component-story.json +++ b/docs/generated/packages/angular/generators/component-story.json @@ -32,6 +32,13 @@ "examples": ["awesome.component"], "x-priority": "important" }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "x-prompt": "Do you want to set up Storybook interaction tests?", + "x-priority": "important", + "default": true + }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", diff --git a/docs/generated/packages/angular/generators/stories.json b/docs/generated/packages/angular/generators/stories.json index bd89a9bbee66ea..fe406177c53e09 100644 --- a/docs/generated/packages/angular/generators/stories.json +++ b/docs/generated/packages/angular/generators/stories.json @@ -18,15 +18,22 @@ "x-dropdown": "projects", "x-priority": "important" }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "x-prompt": "Do you want to set up Storybook interaction tests?", + "x-priority": "important", + "default": true + }, "generateCypressSpecs": { "type": "boolean", "description": "Specifies whether to automatically generate `*.spec.ts` files in the Cypress e2e app generated by the `cypress-configure` generator.", - "x-prompt": "Do you want to generate Cypress specs as well?", - "x-priority": "important" + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressProject": { "type": "string", - "description": "The Cypress project to generate the stories under. This is inferred from `name` by default." + "description": "The Cypress project to generate the stories under. This is inferred from `name` by default.", + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "skipFormat": { "description": "Skip formatting files.", diff --git a/docs/generated/packages/angular/generators/storybook-configuration.json b/docs/generated/packages/angular/generators/storybook-configuration.json index cda247f442ae76..9a067e211fca2b 100644 --- a/docs/generated/packages/angular/generators/storybook-configuration.json +++ b/docs/generated/packages/angular/generators/storybook-configuration.json @@ -29,7 +29,7 @@ "configureCypress": { "type": "boolean", "description": "Specifies whether to configure Cypress or not.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "generateStories": { "type": "boolean", @@ -41,7 +41,7 @@ "generateCypressSpecs": { "type": "boolean", "description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "configureStaticServe": { "type": "boolean", @@ -53,7 +53,7 @@ "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Placed at the root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "linter": { "description": "The tool to use for running lint checks.", diff --git a/docs/generated/packages/react/generators/stories.json b/docs/generated/packages/react/generators/stories.json index af888ff74c2292..09bcda11d50b88 100644 --- a/docs/generated/packages/react/generators/stories.json +++ b/docs/generated/packages/react/generators/stories.json @@ -20,12 +20,12 @@ "generateCypressSpecs": { "type": "boolean", "description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressProject": { "type": "string", "description": "The Cypress project to generate the stories under. This is inferred from `project` by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "interactionTests": { "type": "boolean", diff --git a/docs/generated/packages/react/generators/storybook-configuration.json b/docs/generated/packages/react/generators/storybook-configuration.json index decc8cfbd7ed77..f7999b30f68f11 100644 --- a/docs/generated/packages/react/generators/storybook-configuration.json +++ b/docs/generated/packages/react/generators/storybook-configuration.json @@ -29,7 +29,7 @@ "configureCypress": { "type": "boolean", "description": "Run the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "generateStories": { "type": "boolean", @@ -41,7 +41,7 @@ "generateCypressSpecs": { "type": "boolean", "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "configureStaticServe": { "type": "boolean", @@ -53,7 +53,7 @@ "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Placed at the root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "js": { "type": "boolean", diff --git a/docs/generated/packages/storybook/generators/configuration.json b/docs/generated/packages/storybook/generators/configuration.json index 7c3c8b104361e0..3f8f207e66c5d5 100644 --- a/docs/generated/packages/storybook/generators/configuration.json +++ b/docs/generated/packages/storybook/generators/configuration.json @@ -28,12 +28,12 @@ "configureCypress": { "type": "boolean", "description": "Run the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Added at root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "linter": { "description": "The tool to use for running lint checks.", diff --git a/e2e/storybook-angular/src/storybook-angular.test.ts b/e2e/storybook-angular/src/storybook-angular.test.ts index 01c008d9d55bb8..49b68a1366ee45 100644 --- a/e2e/storybook-angular/src/storybook-angular.test.ts +++ b/e2e/storybook-angular/src/storybook-angular.test.ts @@ -5,11 +5,8 @@ import { newProject, runCLI, runCommandUntil, - runCypressTests, - tmpProjPath, uniq, } from '@nx/e2e/utils'; -import { writeFileSync } from 'fs'; describe('Storybook executors for Angular', () => { const angularStorybookLib = uniq('test-ui-ng-lib'); @@ -17,7 +14,7 @@ describe('Storybook executors for Angular', () => { newProject(); runCLI(`g @nx/angular:library ${angularStorybookLib} --no-interactive`); runCLI( - `generate @nx/angular:storybook-configuration ${angularStorybookLib} --configureCypress --generateStories --generateCypressSpecs --no-interactive` + `generate @nx/angular:storybook-configuration ${angularStorybookLib} --generateStories --no-interactive` ); }); @@ -43,80 +40,4 @@ describe('Storybook executors for Angular', () => { checkFilesExist(`dist/storybook/${angularStorybookLib}/index.html`); }, 200_000); }); - - // However much I increase the timeout, this takes forever? - xdescribe('run cypress tests using storybook', () => { - it('should execute e2e tests using Cypress running against Storybook', async () => { - if (runCypressTests()) { - addTestButtonToUILib(angularStorybookLib); - writeFileSync( - tmpProjPath( - `apps/${angularStorybookLib}-e2e/src/e2e/test-button/test-button.component.cy.ts` - ), - ` - describe('${angularStorybookLib}, () => { - - it('should render the correct text', () => { - cy.visit( - '/iframe.html?id=testbuttoncomponent--primary&args=text:Click+me;color:#ddffdd;disabled:false;' - ) - cy.get('button').should('contain', 'Click me'); - cy.get('button').should('not.be.disabled'); - }); - - it('should adjust the controls', () => { - cy.visit( - '/iframe.html?id=testbuttoncomponent--primary&args=text:Click+me;color:#ddffdd;disabled:true;' - ) - cy.get('button').should('be.disabled'); - }); - }); - ` - ); - - const e2eResults = runCLI(`e2e ${angularStorybookLib}-e2e --no-watch`); - expect(e2eResults).toContain('All specs passed!'); - expect(await killPorts()).toBeTruthy(); - } - }, 1000_000); - }); }); - -function addTestButtonToUILib(libName: string): void { - runCLI( - `g @nx/angular:component test-button --project=${libName} --no-interactive` - ); - - writeFileSync( - tmpProjPath(`libs/${libName}/src/lib/test-button/test-button.component.ts`), - ` - import { Component, Input } from '@angular/core'; - - @Component({ - selector: 'proj-test-button', - templateUrl: './test-button.component.html', - styleUrls: ['./test-button.component.css'], - }) - export class TestButtonComponent { - @Input() text = 'Click me'; - @Input() color = '#ddffdd'; - @Input() disabled = false; - } - ` - ); - - writeFileSync( - tmpProjPath( - `libs/${libName}/src/lib/test-button/test-button.component.html` - ), - ` - - ` - ); -} diff --git a/packages/angular/src/generators/component-story/__snapshots__/component-story.spec.ts.snap b/packages/angular/src/generators/component-story/__snapshots__/component-story.spec.ts.snap index 2d774f7aa55eea..db470fa3e7d10b 100644 --- a/packages/angular/src/generators/component-story/__snapshots__/component-story.spec.ts.snap +++ b/packages/angular/src/generators/component-story/__snapshots__/component-story.spec.ts.snap @@ -1,18 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`componentStory generator should generate the right props 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { TestButtonComponent } from './test-button.component'; -export default { - title: 'TestButtonComponent', +const meta: Meta = { component: TestButtonComponent, -} as Meta; + title: 'TestButtonComponent', +}; +export default meta; +type Story = StoryObj; -export const Primary = { - render: (args: TestButtonComponent) => ({ - props: args, - }), +export const Primary: Story = { args: { buttonType: 'button', style: 'default', diff --git a/packages/angular/src/generators/component-story/component-story.ts b/packages/angular/src/generators/component-story/component-story.ts index f087b78a37948a..f24b12c452c07d 100644 --- a/packages/angular/src/generators/component-story/component-story.ts +++ b/packages/angular/src/generators/component-story/component-story.ts @@ -29,6 +29,8 @@ export async function componentStoryGenerator( generateFiles(tree, templatesDir, destinationDir, { componentFileName: componentFileName, componentName: componentName, + componentNameSimple: componentFileName.replace('.component', ''), + interactionTests: options.interactionTests, props: props.filter((p) => typeof p.defaultValue !== 'undefined'), tmpl: '', }); diff --git a/packages/angular/src/generators/component-story/files/__componentFileName__.stories.ts__tmpl__ b/packages/angular/src/generators/component-story/files/__componentFileName__.stories.ts__tmpl__ index 338a94bde0e8c4..66981e5d6be1c9 100644 --- a/packages/angular/src/generators/component-story/files/__componentFileName__.stories.ts__tmpl__ +++ b/packages/angular/src/generators/component-story/files/__componentFileName__.stories.ts__tmpl__ @@ -1,16 +1,31 @@ -import { Meta } from '@storybook/angular'; +import type { Meta, StoryObj } from '@storybook/angular'; import { <%=componentName%> } from './<%=componentFileName%>'; +<% if ( interactionTests ) { %> +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +<% } %> -export default { - title: '<%=componentName%>', - component: <%=componentName%> -} as Meta<<%=componentName%>>; +const meta: Meta<<%= componentName %>> = { + component: <%= componentName %>, + title: '<%= componentName %>', +}; +export default meta; +type Story = StoryObj<<%=componentName%>>; -export const Primary = { - render: (args: <%=componentName%>) => ({ - props: args, - }), +export const Primary: Story = { args: {<% for (let prop of props) { %> <%= prop.name %>: <%- prop.defaultValue %>,<% } %> }, -}; \ No newline at end of file +}; + +<% if ( interactionTests ) { %> +export const Heading: Story = { + args: {<% for (let prop of props) { %> + <%= prop.name %>: <%- prop.defaultValue %>,<% } %> + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/<%=componentNameSimple%> works!/gi)).toBeTruthy(); + }, +}; +<% } %> \ No newline at end of file diff --git a/packages/angular/src/generators/component-story/schema.d.ts b/packages/angular/src/generators/component-story/schema.d.ts index 05768fae96293e..31b965960878fc 100644 --- a/packages/angular/src/generators/component-story/schema.d.ts +++ b/packages/angular/src/generators/component-story/schema.d.ts @@ -1,5 +1,6 @@ export interface ComponentStoryGeneratorOptions { projectPath: string; + interactionTests?: boolean; componentName: string; componentPath: string; componentFileName: string; diff --git a/packages/angular/src/generators/component-story/schema.json b/packages/angular/src/generators/component-story/schema.json index e2591db714d359..a74cbeb20e4b96 100644 --- a/packages/angular/src/generators/component-story/schema.json +++ b/packages/angular/src/generators/component-story/schema.json @@ -29,6 +29,13 @@ "examples": ["awesome.component"], "x-priority": "important" }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "x-prompt": "Do you want to set up Storybook interaction tests?", + "x-priority": "important", + "default": true + }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", diff --git a/packages/angular/src/generators/stories/__snapshots__/stories-app.spec.ts.snap b/packages/angular/src/generators/stories/__snapshots__/stories-app.spec.ts.snap index 9a80b69d508766..19657b24dd13be 100644 --- a/packages/angular/src/generators/stories/__snapshots__/stories-app.spec.ts.snap +++ b/packages/angular/src/generators/stories/__snapshots__/stories-app.spec.ts.snap @@ -1,37 +1,85 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`angularStories generator: applications should generate stories file for inline scam component 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { MyScamComponent } from './my-scam.component'; -export default { - title: 'MyScamComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: MyScamComponent, -} as Meta; + title: 'MyScamComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; + +export const Heading: Story = { + args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/my-scam works!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`angularStories generator: applications should generate stories file with interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/angular'; +import { AppComponent } from './app.component'; -export const Primary = { - render: (args: MyScamComponent) => ({ - props: args, - }), +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: AppComponent, + title: 'AppComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { args: {}, }; + +export const Heading: Story = { + args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/app works!/gi)).toBeTruthy(); + }, +}; " `; exports[`angularStories generator: applications should ignore a path that has a nested component, but still generate nested component stories 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { ComponentBComponent } from './component-b.component'; -export default { - title: 'ComponentBComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: ComponentBComponent, -} as Meta; + title: 'ComponentBComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; -export const Primary = { - render: (args: ComponentBComponent) => ({ - props: args, - }), +export const Heading: Story = { args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/component-b works!/gi)).toBeTruthy(); + }, }; " `; diff --git a/packages/angular/src/generators/stories/__snapshots__/stories-lib.spec.ts.snap b/packages/angular/src/generators/stories/__snapshots__/stories-lib.spec.ts.snap index 396bf86c63c60f..8d236612dd3f76 100644 --- a/packages/angular/src/generators/stories/__snapshots__/stories-lib.spec.ts.snap +++ b/packages/angular/src/generators/stories/__snapshots__/stories-lib.spec.ts.snap @@ -1,68 +1,76 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`angularStories generator: libraries Stories for non-empty Angular library should generate cypress spec files 1`] = ` -"describe('test-ui-lib', () => { - beforeEach(() => - cy.visit( - '/iframe.html?id=testbuttoncomponent--primary&args=buttonType:button;style:default;age;isOn:false;' - ) - ); - it('should render the component', () => { - cy.get('proj-test-button').should('exist'); - }); -}); -" -`; - exports[`angularStories generator: libraries Stories for non-empty Angular library should generate stories file for standalone components 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { StandaloneComponent } from './standalone.component'; -export default { - title: 'StandaloneComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: StandaloneComponent, -} as Meta; + title: 'StandaloneComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; -export const Primary = { - render: (args: StandaloneComponent) => ({ - props: args, - }), +export const Heading: Story = { args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/standalone works!/gi)).toBeTruthy(); + }, }; " `; exports[`angularStories generator: libraries Stories for non-empty Angular library should generate stories file for standalone components 2`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { SecondaryStandaloneComponent } from './secondary-standalone.component'; -export default { - title: 'SecondaryStandaloneComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: SecondaryStandaloneComponent, -} as Meta; + title: 'SecondaryStandaloneComponent', +}; +export default meta; +type Story = StoryObj; -export const Primary = { - render: (args: SecondaryStandaloneComponent) => ({ - props: args, - }), +export const Primary: Story = { args: {}, }; + +export const Heading: Story = { + args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/secondary-standalone works!/gi)).toBeTruthy(); + }, +}; " `; exports[`angularStories generator: libraries Stories for non-empty Angular library should generate stories.ts files 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { TestButtonComponent } from './test-button.component'; -export default { - title: 'TestButtonComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: TestButtonComponent, -} as Meta; + title: 'TestButtonComponent', +}; +export default meta; +type Story = StoryObj; -export const Primary = { - render: (args: TestButtonComponent) => ({ - props: args, - }), +export const Primary: Story = { args: { buttonType: 'button', style: 'default', @@ -70,22 +78,37 @@ export const Primary = { isOn: false, }, }; + +export const Heading: Story = { + args: { + buttonType: 'button', + style: 'default', + age: 0, + isOn: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/test-button works!/gi)).toBeTruthy(); + }, +}; " `; exports[`angularStories generator: libraries Stories for non-empty Angular library should ignore paths 1`] = ` -"import { Meta } from '@storybook/angular'; +"import type { Meta, StoryObj } from '@storybook/angular'; import { TestButtonComponent } from './test-button.component'; -export default { - title: 'TestButtonComponent', +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: TestButtonComponent, -} as Meta; + title: 'TestButtonComponent', +}; +export default meta; +type Story = StoryObj; -export const Primary = { - render: (args: TestButtonComponent) => ({ - props: args, - }), +export const Primary: Story = { args: { buttonType: 'button', style: 'default', @@ -93,5 +116,18 @@ export const Primary = { isOn: false, }, }; + +export const Heading: Story = { + args: { + buttonType: 'button', + style: 'default', + age: 0, + isOn: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/test-button works!/gi)).toBeTruthy(); + }, +}; " `; diff --git a/packages/angular/src/generators/stories/schema.d.ts b/packages/angular/src/generators/stories/schema.d.ts index 88364762103963..bac80e8a50d94b 100644 --- a/packages/angular/src/generators/stories/schema.d.ts +++ b/packages/angular/src/generators/stories/schema.d.ts @@ -1,7 +1,14 @@ export interface StoriesGeneratorOptions { name: string; - cypressProject?: string; - generateCypressSpecs?: boolean; + interactionTests?: boolean; skipFormat?: boolean; ignorePaths?: string[]; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + cypressProject?: string; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + generateCypressSpecs?: boolean; } diff --git a/packages/angular/src/generators/stories/schema.json b/packages/angular/src/generators/stories/schema.json index b97cfe83f5f194..4d36fac672da81 100644 --- a/packages/angular/src/generators/stories/schema.json +++ b/packages/angular/src/generators/stories/schema.json @@ -18,15 +18,22 @@ "x-dropdown": "projects", "x-priority": "important" }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "x-prompt": "Do you want to set up Storybook interaction tests?", + "x-priority": "important", + "default": true + }, "generateCypressSpecs": { "type": "boolean", "description": "Specifies whether to automatically generate `*.spec.ts` files in the Cypress e2e app generated by the `cypress-configure` generator.", - "x-prompt": "Do you want to generate Cypress specs as well?", - "x-priority": "important" + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressProject": { "type": "string", - "description": "The Cypress project to generate the stories under. This is inferred from `name` by default." + "description": "The Cypress project to generate the stories under. This is inferred from `name` by default.", + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "skipFormat": { "description": "Skip formatting files.", diff --git a/packages/angular/src/generators/stories/stories-app.spec.ts b/packages/angular/src/generators/stories/stories-app.spec.ts index 39ade7427e65b1..37d74dca96831a 100644 --- a/packages/angular/src/generators/stories/stories-app.spec.ts +++ b/packages/angular/src/generators/stories/stories-app.spec.ts @@ -10,6 +10,8 @@ import { angularStoriesGenerator } from './stories'; // which is v9 while we are testing for the new v10 version jest.mock('@nx/cypress/src/utils/cypress-version'); +// TODO(v18): remove Cypress + describe('angularStories generator: applications', () => { let tree: Tree; const appName = 'test-app'; @@ -25,12 +27,12 @@ describe('angularStories generator: applications', () => { }); }); - it('should generate stories file', async () => { + it('should generate stories file with interaction tests', async () => { await angularStoriesGenerator(tree, { name: appName }); expect( - tree.exists(`apps/${appName}/src/app/app.component.stories.ts`) - ).toBeTruthy(); + tree.read(`apps/${appName}/src/app/app.component.stories.ts`, 'utf-8') + ).toMatchSnapshot(); }); it('should generate stories file for scam component', async () => { @@ -90,11 +92,10 @@ describe('angularStories generator: applications', () => { }); expect( - tree - .read( - `apps/${appName}/src/app/component-a/component-b/component-b.component.stories.ts` - ) - .toString() + tree.read( + `apps/${appName}/src/app/component-a/component-b/component-b.component.stories.ts`, + 'utf-8' + ) ).toMatchSnapshot(); expect( tree.exists( @@ -113,20 +114,10 @@ describe('angularStories generator: applications', () => { await angularStoriesGenerator(tree, { name: appName }); expect( - tree - .read(`apps/${appName}/src/app/my-scam/my-scam.component.stories.ts`) - .toString() + tree.read( + `apps/${appName}/src/app/my-scam/my-scam.component.stories.ts`, + 'utf-8' + ) ).toMatchSnapshot(); }); - - it('should generate cypress spec file', async () => { - await angularStoriesGenerator(tree, { - name: appName, - generateCypressSpecs: true, - }); - - expect( - tree.exists(`apps/${appName}-e2e/src/e2e/app.component.cy.ts`) - ).toBeTruthy(); - }); }); diff --git a/packages/angular/src/generators/stories/stories-lib.spec.ts b/packages/angular/src/generators/stories/stories-lib.spec.ts index ec1008480fbc2e..a465eee67c8956 100644 --- a/packages/angular/src/generators/stories/stories-lib.spec.ts +++ b/packages/angular/src/generators/stories/stories-lib.spec.ts @@ -2,7 +2,6 @@ import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { Tree } from '@nx/devkit'; import { writeJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { Linter } from '@nx/linter'; import { componentGenerator } from '../component/component'; import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point'; import { scamGenerator } from '../scam/scam'; @@ -15,6 +14,7 @@ import { angularStoriesGenerator } from './stories'; // need to mock cypress otherwise it'll use the nx installed version from package.json // which is v9 while we are testing for the new v10 version jest.mock('@nx/cypress/src/utils/cypress-version'); +// TODO(v18): remove Cypress describe('angularStories generator: libraries', () => { const libName = 'test-ui-lib'; @@ -39,7 +39,6 @@ describe('angularStories generator: libraries', () => { async () => await angularStoriesGenerator(tree, { name: libName, - generateCypressSpecs: false, }) ).not.toThrow(); }); @@ -47,13 +46,9 @@ describe('angularStories generator: libraries', () => { describe('Stories for non-empty Angular library', () => { let tree: Tree; - let cypressProjectGenerator; beforeEach(async () => { tree = await createStorybookTestWorkspaceForLib(libName); - cypressProjectGenerator = await ( - await import('@nx/storybook') - ).cypressProjectGenerator; }); it('should generate stories.ts files', async () => { @@ -105,56 +100,11 @@ describe('angularStories generator: libraries', () => { ).toBeTruthy(); }); - it('should generate cypress spec files', async () => { - await cypressProjectGenerator(tree, { - linter: Linter.EsLint, - name: libName, - }); - - await angularStoriesGenerator(tree, { - name: libName, - generateCypressSpecs: true, - }); - - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/nested-button/nested-button.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/test-button/test-button.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/test-other/test-other.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.read( - `apps/${libName}-e2e/src/e2e/test-button/test-button.component.cy.ts`, - 'utf-8' - ) - ).toMatchSnapshot(); - }); - it('should run twice without errors', async () => { - await cypressProjectGenerator(tree, { - linter: Linter.EsLint, - name: libName, - }); - try { await angularStoriesGenerator(tree, { name: libName }); await angularStoriesGenerator(tree, { name: libName, - generateCypressSpecs: true, }); } catch { fail('Should not fail when running it twice.'); @@ -162,14 +112,8 @@ describe('angularStories generator: libraries', () => { }); it('should handle modules with variable declarations rather than literals', async () => { - await cypressProjectGenerator(tree, { - linter: Linter.EsLint, - name: libName, - }); - await angularStoriesGenerator(tree, { name: libName, - generateCypressSpecs: true, }); expect( @@ -182,27 +126,11 @@ describe('angularStories generator: libraries', () => { `libs/${libName}/src/lib/variable-declare/variable-declare-view/variable-declare-view.component.stories.ts` ) ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts` - ) - ).toBeTruthy(); }); it('should handle modules with where components are spread into the declarations array', async () => { - await cypressProjectGenerator(tree, { - linter: Linter.EsLint, - name: libName, - }); - await angularStoriesGenerator(tree, { name: libName, - generateCypressSpecs: true, }); expect( @@ -220,33 +148,11 @@ describe('angularStories generator: libraries', () => { `libs/${libName}/src/lib/variable-spread-declare/variable-spread-declare-view/variable-spread-declare-view.component.stories.ts` ) ).toBeTruthy(); - - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts` - ) - ).toBeTruthy(); - expect( - tree.exists( - `apps/${libName}-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts` - ) - ).toBeTruthy(); }); it('should handle modules using static members for declarations rather than literals', async () => { - await cypressProjectGenerator(tree, { - linter: Linter.EsLint, - name: libName, - }); - await angularStoriesGenerator(tree, { name: libName, - generateCypressSpecs: true, }); expect( @@ -259,12 +165,6 @@ describe('angularStories generator: libraries', () => { `libs/${libName}/src/lib/static-member-declarations/cmp2/cmp2.component.stories.ts` ) ).toBeTruthy(); - expect( - tree.exists(`apps/${libName}-e2e/src/e2e/cmp1/cmp1.component.cy.ts`) - ).toBeTruthy(); - expect( - tree.exists(`apps/${libName}-e2e/src/e2e/cmp2/cmp2.component.cy.ts`) - ).toBeTruthy(); }); it('should generate stories file for scam component', async () => { diff --git a/packages/angular/src/generators/stories/stories.ts b/packages/angular/src/generators/stories/stories.ts index 3d1370bdda8b22..c8ee6aa93264ea 100644 --- a/packages/angular/src/generators/stories/stories.ts +++ b/packages/angular/src/generators/stories/stories.ts @@ -1,4 +1,14 @@ -import { formatFiles, joinPathFragments, logger, Tree } from '@nx/devkit'; +import { + addDependenciesToPackageJson, + ensurePackage, + formatFiles, + GeneratorCallback, + joinPathFragments, + logger, + readProjectConfiguration, + runTasksInSerial, + Tree, +} from '@nx/devkit'; import componentCypressSpecGenerator from '../component-cypress-spec/component-cypress-spec'; import componentStoryGenerator from '../component-story/component-story'; import type { ComponentInfo } from '../utils/storybook-ast/component-info'; @@ -11,11 +21,12 @@ import { getE2EProject } from './lib/get-e2e-project'; import { getModuleFilePaths } from '../utils/storybook-ast/module-info'; import type { StoriesGeneratorOptions } from './schema'; import minimatch = require('minimatch'); +import { nxVersion } from '../../utils/versions'; export async function angularStoriesGenerator( tree: Tree, options: StoriesGeneratorOptions -): Promise { +): Promise { const e2eProjectName = options.cypressProject ?? `${options.name}-e2e`; const e2eProject = getE2EProject(tree, e2eProjectName); const entryPoints = getProjectEntryPoints(tree, options.name); @@ -59,6 +70,7 @@ export async function angularStoriesGenerator( componentName: info.name, componentPath: info.path, componentFileName: info.componentFileName, + interactionTests: options.interactionTests ?? true, skipFormat: true, }); @@ -75,10 +87,24 @@ export async function angularStoriesGenerator( }); } } + const tasks: GeneratorCallback[] = []; + + if (options.interactionTests) { + const { interactionTestsDependencies, addInteractionsInAddons } = + ensurePackage('@nx/storybook', nxVersion); + + const projectConfiguration = readProjectConfiguration(tree, options.name); + addInteractionsInAddons(tree, projectConfiguration); + + tasks.push( + addDependenciesToPackageJson(tree, {}, interactionTestsDependencies()) + ); + } if (!options.skipFormat) { await formatFiles(tree); } + return runTasksInSerial(...tasks); } export default angularStoriesGenerator; diff --git a/packages/angular/src/generators/storybook-configuration/__snapshots__/storybook-configuration.spec.ts.snap b/packages/angular/src/generators/storybook-configuration/__snapshots__/storybook-configuration.spec.ts.snap index 88398ee8f08a46..4d1f9c5e47ae1a 100644 --- a/packages/angular/src/generators/storybook-configuration/__snapshots__/storybook-configuration.spec.ts.snap +++ b/packages/angular/src/generators/storybook-configuration/__snapshots__/storybook-configuration.spec.ts.snap @@ -1,9 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`StorybookConfiguration generator should configure everything at once - and interaction tests too 1`] = ` +"import type { Meta, StoryObj } from '@storybook/angular'; +import { TestButtonComponent } from './test-button.component'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: TestButtonComponent, + title: 'TestButtonComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + buttonType: 'button', + style: 'default', + age: 0, + isOn: false, + }, +}; + +export const Heading: Story = { + args: { + buttonType: 'button', + style: 'default', + age: 0, + isOn: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/test-button works!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`StorybookConfiguration generator should configure everything at once - and interaction tests too 2`] = ` +"import type { Meta, StoryObj } from '@storybook/angular'; +import { TestOtherComponent } from './test-other.component'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: TestOtherComponent, + title: 'TestOtherComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; + +export const Heading: Story = { + args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/test-other works!/gi)).toBeTruthy(); + }, +}; +" +`; + exports[`StorybookConfiguration generator should configure storybook to use webpack 5 1`] = ` -"const config = { +"import type { StorybookConfig } from '@storybook/angular'; + +const config: StorybookConfig = { stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-essentials'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], framework: { name: '@storybook/angular', options: {}, @@ -26,33 +94,12 @@ exports[`StorybookConfiguration generator should generate in the correct folder ".prettierignore", ".prettierrc", "apps/.gitignore", - "apps/one/two/test-ui-lib-e2e/.eslintrc.json", - "apps/one/two/test-ui-lib-e2e/cypress.config.ts", - "apps/one/two/test-ui-lib-e2e/project.json", - "apps/one/two/test-ui-lib-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/cmp1/cmp1.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/cmp2/cmp2.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/nested-button/nested-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-button/secondary-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-standalone/secondary-standalone.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/standalone/standalone.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts", - "apps/one/two/test-ui-lib-e2e/src/fixtures/example.json", - "apps/one/two/test-ui-lib-e2e/src/support/commands.ts", - "apps/one/two/test-ui-lib-e2e/src/support/e2e.ts", - "apps/one/two/test-ui-lib-e2e/tsconfig.json", "jest.config.ts", "jest.preset.js", "libs/.gitignore", "libs/test-ui-lib/.eslintrc.json", - "libs/test-ui-lib/.storybook/main.js", - "libs/test-ui-lib/.storybook/preview.js", + "libs/test-ui-lib/.storybook/main.ts", + "libs/test-ui-lib/.storybook/preview.ts", "libs/test-ui-lib/.storybook/tsconfig.json", "libs/test-ui-lib/jest.config.ts", "libs/test-ui-lib/package.json", @@ -158,33 +205,12 @@ exports[`StorybookConfiguration generator should generate the right files 1`] = ".prettierignore", ".prettierrc", "apps/.gitignore", - "apps/test-ui-lib-e2e/.eslintrc.json", - "apps/test-ui-lib-e2e/cypress.config.ts", - "apps/test-ui-lib-e2e/project.json", - "apps/test-ui-lib-e2e/src/e2e/barrel-button/barrel-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/cmp1/cmp1.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/cmp2/cmp2.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/nested-button/nested-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-button/secondary-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/secondary-entry-point/secondary-standalone/secondary-standalone.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/standalone/standalone.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/variable-declare-button/variable-declare-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/variable-declare-view/variable-declare-view.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-anotherview/variable-spread-declare-anotherview.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-button/variable-spread-declare-button.component.cy.ts", - "apps/test-ui-lib-e2e/src/e2e/variable-spread-declare-view/variable-spread-declare-view.component.cy.ts", - "apps/test-ui-lib-e2e/src/fixtures/example.json", - "apps/test-ui-lib-e2e/src/support/commands.ts", - "apps/test-ui-lib-e2e/src/support/e2e.ts", - "apps/test-ui-lib-e2e/tsconfig.json", "jest.config.ts", "jest.preset.js", "libs/.gitignore", "libs/test-ui-lib/.eslintrc.json", - "libs/test-ui-lib/.storybook/main.js", - "libs/test-ui-lib/.storybook/preview.js", + "libs/test-ui-lib/.storybook/main.ts", + "libs/test-ui-lib/.storybook/preview.ts", "libs/test-ui-lib/.storybook/tsconfig.json", "libs/test-ui-lib/jest.config.ts", "libs/test-ui-lib/package.json", diff --git a/packages/angular/src/generators/storybook-configuration/lib/generate-stories.ts b/packages/angular/src/generators/storybook-configuration/lib/generate-stories.ts index de21bbcc90ee3c..80db75de6d2556 100644 --- a/packages/angular/src/generators/storybook-configuration/lib/generate-stories.ts +++ b/packages/angular/src/generators/storybook-configuration/lib/generate-stories.ts @@ -21,6 +21,7 @@ export async function generateStories( options.configureCypress && options.generateCypressSpecs, cypressProject: e2eProjectName, ignorePaths: options.ignorePaths, + interactionTests: options.interactionTests, skipFormat: true, }); } diff --git a/packages/angular/src/generators/storybook-configuration/schema.d.ts b/packages/angular/src/generators/storybook-configuration/schema.d.ts index 9c8867f4627a62..b0db1df8227c5c 100644 --- a/packages/angular/src/generators/storybook-configuration/schema.d.ts +++ b/packages/angular/src/generators/storybook-configuration/schema.d.ts @@ -1,15 +1,24 @@ import type { Linter } from '@nx/linter'; export interface StorybookConfigurationOptions { - configureCypress: boolean; configureStaticServe?: boolean; - generateCypressSpecs: boolean; generateStories: boolean; linter: Linter; name: string; - cypressDirectory?: string; tsConfiguration?: boolean; skipFormat?: boolean; ignorePaths?: string[]; interactionTests?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + configureCypress?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + generateCypressSpecs?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + cypressDirectory?: string; } diff --git a/packages/angular/src/generators/storybook-configuration/schema.json b/packages/angular/src/generators/storybook-configuration/schema.json index 2d00d3817d3b70..dad6077a37717d 100644 --- a/packages/angular/src/generators/storybook-configuration/schema.json +++ b/packages/angular/src/generators/storybook-configuration/schema.json @@ -29,7 +29,7 @@ "configureCypress": { "type": "boolean", "description": "Specifies whether to configure Cypress or not.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "generateStories": { "type": "boolean", @@ -41,7 +41,7 @@ "generateCypressSpecs": { "type": "boolean", "description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "configureStaticServe": { "type": "boolean", @@ -53,7 +53,7 @@ "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Placed at the root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "linter": { "description": "The tool to use for running lint checks.", diff --git a/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts b/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts index cc4c2a8e40068d..ac1757a2a28f66 100644 --- a/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts +++ b/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts @@ -44,31 +44,16 @@ describe('StorybookConfiguration generator', () => { jest.resetModules(); }); - it('should throw when generateCypressSpecs is true and generateStories is false', async () => { - await expect( - storybookConfigurationGenerator(tree, { - name: libName, - generateCypressSpecs: true, - generateStories: false, - }) - ).rejects.toThrow( - 'Cannot set generateCypressSpecs to true when generateStories is set to false.' - ); - }); - it('should only configure storybook', async () => { await storybookConfigurationGenerator(tree, { name: libName, - configureCypress: false, - generateCypressSpecs: false, generateStories: false, }); - expect(tree.exists('libs/test-ui-lib/.storybook/main.js')).toBeTruthy(); + expect(tree.exists('libs/test-ui-lib/.storybook/main.ts')).toBeTruthy(); expect( tree.exists('libs/test-ui-lib/.storybook/tsconfig.json') ).toBeTruthy(); - expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeFalsy(); expect( tree.exists( 'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts' @@ -79,65 +64,42 @@ describe('StorybookConfiguration generator', () => { 'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts' ) ).toBeFalsy(); - expect( - tree.exists( - 'apps/test-ui-lib-e2e/src/integration/test-button/test-button.component.spec.ts' - ) - ).toBeFalsy(); - expect( - tree.exists( - 'apps/test-ui-lib-e2e/src/integration/test-other/test-other.component.spec.ts' - ) - ).toBeFalsy(); }); it('should configure storybook to use webpack 5', async () => { await storybookConfigurationGenerator(tree, { name: libName, - configureCypress: false, - generateCypressSpecs: false, generateStories: false, linter: Linter.None, }); expect( - tree.read('libs/test-ui-lib/.storybook/main.js', 'utf-8') + tree.read('libs/test-ui-lib/.storybook/main.ts', 'utf-8') ).toMatchSnapshot(); }); - it('should configure everything at once', async () => { + it('should configure everything at once - and interaction tests too', async () => { await storybookConfigurationGenerator(tree, { name: libName, - configureCypress: true, - generateCypressSpecs: true, generateStories: true, }); - expect(tree.exists('libs/test-ui-lib/.storybook/main.js')).toBeTruthy(); + expect(tree.exists('libs/test-ui-lib/.storybook/main.ts')).toBeTruthy(); expect( tree.exists('libs/test-ui-lib/.storybook/tsconfig.json') ).toBeTruthy(); - expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeTruthy(); - expect( - tree.exists( - 'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts' - ) - ).toBeTruthy(); expect( - tree.exists( - 'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts' + tree.read( + 'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts', + 'utf-8' ) - ).toBeTruthy(); - expect( - tree.exists( - 'apps/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts' - ) - ).toBeTruthy(); + ).toMatchSnapshot(); expect( - tree.exists( - 'apps/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts' + tree.read( + 'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); it('should generate the right files', async () => { @@ -171,8 +133,6 @@ describe('StorybookConfiguration generator', () => { await storybookConfigurationGenerator(tree, { name: libName, - configureCypress: true, - generateCypressSpecs: true, generateStories: true, }); @@ -210,10 +170,7 @@ describe('StorybookConfiguration generator', () => { await storybookConfigurationGenerator(tree, { name: libName, - configureCypress: true, - generateCypressSpecs: true, generateStories: true, - cypressDirectory: 'one/two', }); expect(listFiles(tree)).toMatchSnapshot(); diff --git a/packages/angular/src/generators/storybook-configuration/storybook-configuration.ts b/packages/angular/src/generators/storybook-configuration/storybook-configuration.ts index 6164f0215af50a..e0c264394b8ebf 100644 --- a/packages/angular/src/generators/storybook-configuration/storybook-configuration.ts +++ b/packages/angular/src/generators/storybook-configuration/storybook-configuration.ts @@ -5,6 +5,7 @@ import { generateStorybookConfiguration } from './lib/generate-storybook-configu import { validateOptions } from './lib/validate-options'; import type { StorybookConfigurationOptions } from './schema'; +// TODO(v18): remove Cypress export async function storybookConfigurationGenerator( tree: Tree, options: StorybookConfigurationOptions @@ -14,11 +15,19 @@ export async function storybookConfigurationGenerator( const storybookGeneratorInstallTask = await generateStorybookConfiguration( tree, - options + { + ...options, + interactionTests: options.interactionTests ?? true, // default is true + tsConfiguration: options.tsConfiguration ?? true, // default is true + } ); if (options.generateStories) { - await generateStories(tree, { ...options, skipFormat: true }); + await generateStories(tree, { + ...options, + interactionTests: options.interactionTests ?? true, + skipFormat: true, + }); } if (!options.skipFormat) { diff --git a/packages/angular/src/generators/utils/storybook-ast/storybook-inputs.ts b/packages/angular/src/generators/utils/storybook-ast/storybook-inputs.ts index fb9ecb2dd1a31f..35bd7575f9f03e 100644 --- a/packages/angular/src/generators/utils/storybook-ast/storybook-inputs.ts +++ b/packages/angular/src/generators/utils/storybook-ast/storybook-inputs.ts @@ -6,11 +6,11 @@ import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript' let tsModule: typeof import('typescript'); -export type KnobType = 'text' | 'boolean' | 'number' | 'select'; +export type ArgType = 'text' | 'boolean' | 'number' | 'select'; export interface InputDescriptor { name: string; - type: KnobType; + type: ArgType; defaultValue?: string; } @@ -62,7 +62,7 @@ export function getComponentProps( : node.name.getText() : node.name.getText(); - const type = getKnobType(node); + const type = getArgType(node); const defaultValue = getArgsDefaultValueFn(node); return { @@ -76,27 +76,27 @@ export function getComponentProps( return props; } -export function getKnobType(property: PropertyDeclaration): KnobType { +export function getArgType(property: PropertyDeclaration): ArgType { if (!tsModule) { tsModule = ensureTypescript(); } if (property.type) { const typeName = property.type.getText(); - const typeNameToKnobType: Record = { + const typeNameToArgType: Record = { string: 'text', number: 'number', boolean: 'boolean', }; - return typeNameToKnobType[typeName] || 'text'; + return typeNameToArgType[typeName] || 'text'; } if (property.initializer) { - const initializerKindToKnobType: Record = { + const initializerKindToArgType: Record = { [tsModule.SyntaxKind.StringLiteral]: 'text', [tsModule.SyntaxKind.NumericLiteral]: 'number', [tsModule.SyntaxKind.TrueKeyword]: 'boolean', [tsModule.SyntaxKind.FalseKeyword]: 'boolean', }; - return initializerKindToKnobType[property.initializer.kind] || 'text'; + return initializerKindToArgType[property.initializer.kind] || 'text'; } return 'text'; } diff --git a/packages/react/src/generators/component-story/__snapshots__/component-story.spec.ts.snap b/packages/react/src/generators/component-story/__snapshots__/component-story.spec.ts.snap index ef8678b685a41e..397b6af85630e2 100644 --- a/packages/react/src/generators/component-story/__snapshots__/component-story.spec.ts.snap +++ b/packages/react/src/generators/component-story/__snapshots__/component-story.spec.ts.snap @@ -22,6 +22,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -52,6 +56,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -82,6 +90,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -112,6 +124,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -142,6 +158,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -172,6 +192,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -202,6 +226,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -232,6 +260,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -262,6 +294,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -292,6 +328,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -322,6 +362,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -352,6 +396,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -382,6 +430,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -412,6 +464,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -442,6 +498,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -472,6 +532,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -502,6 +566,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -532,6 +600,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -559,6 +631,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to One!/gi)).toBeTruthy(); @@ -586,6 +659,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Two!/gi)).toBeTruthy(); @@ -615,6 +689,9 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Three!/gi)).toBeTruthy(); @@ -648,6 +725,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -678,6 +759,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -705,6 +790,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); @@ -732,6 +818,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); diff --git a/packages/react/src/generators/component-story/files/tsx/__componentFileName__.stories.tsx__tmpl__ b/packages/react/src/generators/component-story/files/tsx/__componentFileName__.stories.tsx__tmpl__ index 257b72934ad3b2..0045de48e0bb69 100644 --- a/packages/react/src/generators/component-story/files/tsx/__componentFileName__.stories.tsx__tmpl__ +++ b/packages/react/src/generators/component-story/files/tsx/__componentFileName__.stories.tsx__tmpl__ @@ -25,6 +25,9 @@ export const Primary = { <% if ( interactionTests ) { %> export const Heading: Story = { + args: {<% for (let prop of props) { %> + <%= prop.name %>: <%- prop.defaultValue %>,<% } %> + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy(); diff --git a/packages/react/src/generators/stories/__snapshots__/stories.app.spec.ts.snap b/packages/react/src/generators/stories/__snapshots__/stories.app.spec.ts.snap index 9c6abe28dbe8ab..cdbb8fc9ebd6b7 100644 --- a/packages/react/src/generators/stories/__snapshots__/stories.app.spec.ts.snap +++ b/packages/react/src/generators/stories/__snapshots__/stories.app.spec.ts.snap @@ -19,6 +19,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy(); @@ -49,6 +50,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); diff --git a/packages/react/src/generators/stories/__snapshots__/stories.lib.spec.ts.snap b/packages/react/src/generators/stories/__snapshots__/stories.lib.spec.ts.snap index 4b3b4afe271248..4606673a016f0b 100644 --- a/packages/react/src/generators/stories/__snapshots__/stories.lib.spec.ts.snap +++ b/packages/react/src/generators/stories/__snapshots__/stories.lib.spec.ts.snap @@ -19,6 +19,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); @@ -49,6 +50,10 @@ export const Primary = { }; export const Heading: Story = { + args: { + name: '', + displayAge: false, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); diff --git a/packages/react/src/generators/stories/schema.json b/packages/react/src/generators/stories/schema.json index 10749a54c4d710..bc2389ff142095 100644 --- a/packages/react/src/generators/stories/schema.json +++ b/packages/react/src/generators/stories/schema.json @@ -20,12 +20,12 @@ "generateCypressSpecs": { "type": "boolean", "description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressProject": { "type": "string", "description": "The Cypress project to generate the stories under. This is inferred from `project` by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "interactionTests": { "type": "boolean", diff --git a/packages/react/src/generators/stories/stories.ts b/packages/react/src/generators/stories/stories.ts index 15be2f42562592..968b082c859db7 100644 --- a/packages/react/src/generators/stories/stories.ts +++ b/packages/react/src/generators/stories/stories.ts @@ -5,18 +5,23 @@ import { getComponentNode, } from '../../utils/ast-utils'; import { + addDependenciesToPackageJson, convertNxGenerator, + ensurePackage, formatFiles, + GeneratorCallback, getProjects, joinPathFragments, logger, ProjectConfiguration, + runTasksInSerial, Tree, visitNotIgnoredFiles, } from '@nx/devkit'; import { basename, join } from 'path'; import minimatch = require('minimatch'); import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; +import { nxVersion } from '../../utils/versions'; let tsModule: typeof import('typescript'); @@ -24,10 +29,16 @@ export interface StorybookStoriesSchema { project: string; interactionTests?: boolean; js?: boolean; - cypressProject?: string; - generateCypressSpecs?: boolean; ignorePaths?: string[]; skipFormat?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + cypressProject?: string; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + generateCypressSpecs?: boolean; } export async function projectRootPath( @@ -87,13 +98,14 @@ export async function createAllStories( projectName: string, interactionTests: boolean, js: boolean, + projects: Map, + projectConfiguration: ProjectConfiguration, generateCypressSpecs?: boolean, cypressProject?: string, ignorePaths?: string[] ) { const { isTheFileAStory } = await import('@nx/storybook/src/utils/utilities'); - const projects = getProjects(tree); - const projectConfiguration = projects.get(projectName); + const { sourceRoot, root } = projectConfiguration; let componentPaths: string[] = []; @@ -164,19 +176,35 @@ export async function storiesGenerator( host: Tree, schema: StorybookStoriesSchema ) { + const projects = getProjects(host); + const projectConfiguration = projects.get(schema.project); await createAllStories( host, schema.project, schema.interactionTests ?? true, schema.js, + projects, + projectConfiguration, schema.generateCypressSpecs, schema.cypressProject, schema.ignorePaths ); + const tasks: GeneratorCallback[] = []; + + if (schema.interactionTests) { + const { interactionTestsDependencies, addInteractionsInAddons } = + ensurePackage('@nx/storybook', nxVersion); + tasks.push( + addDependenciesToPackageJson(host, {}, interactionTestsDependencies()) + ); + addInteractionsInAddons(host, projectConfiguration); + } + if (!schema.skipFormat) { await formatFiles(host); } + return runTasksInSerial(...tasks); } export default storiesGenerator; diff --git a/packages/react/src/generators/storybook-configuration/__snapshots__/configuration.spec.ts.snap b/packages/react/src/generators/storybook-configuration/__snapshots__/configuration.spec.ts.snap index 54b8e3f57a168a..051b1a82796331 100644 --- a/packages/react/src/generators/storybook-configuration/__snapshots__/configuration.spec.ts.snap +++ b/packages/react/src/generators/storybook-configuration/__snapshots__/configuration.spec.ts.snap @@ -43,6 +43,7 @@ export const Primary = { }; export const Heading: Story = { + args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Welcome to MyComponent!/gi)).toBeTruthy(); diff --git a/packages/react/src/generators/storybook-configuration/configuration.spec.ts b/packages/react/src/generators/storybook-configuration/configuration.spec.ts index 5c8917d3664b82..fc91fd88b19084 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.spec.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.spec.ts @@ -1,4 +1,4 @@ -// TODO(katerina): remove Cypress for Nx 18 +// TODO(v18): remove Cypress import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { logger, Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; diff --git a/packages/react/src/generators/storybook-configuration/configuration.ts b/packages/react/src/generators/storybook-configuration/configuration.ts index be92e63c057d6f..f1da53ccda7197 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.ts @@ -10,7 +10,7 @@ import { import { nxVersion } from '../../utils/versions'; async function generateStories(host: Tree, schema: StorybookConfigureSchema) { - // TODO(katerina): remove Cypress for Nx 18 + // TODO(v18): remove Cypress ensurePackage('@nx/cypress', nxVersion); const { getE2eProjectName } = await import( '@nx/cypress/src/utils/project-name' diff --git a/packages/react/src/generators/storybook-configuration/schema.d.ts b/packages/react/src/generators/storybook-configuration/schema.d.ts index 80aabb79f7ff7b..a3f6a007f6e6c4 100644 --- a/packages/react/src/generators/storybook-configuration/schema.d.ts +++ b/packages/react/src/generators/storybook-configuration/schema.d.ts @@ -4,12 +4,21 @@ export interface StorybookConfigureSchema { name: string; interactionTests?: boolean; generateStories?: boolean; - configureCypress?: boolean; - generateCypressSpecs?: boolean; js?: boolean; tsConfiguration?: boolean; linter?: Linter; - cypressDirectory?: string; ignorePaths?: string[]; configureStaticServe?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + configureCypress?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + generateCypressSpecs?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + cypressDirectory?: string; } diff --git a/packages/react/src/generators/storybook-configuration/schema.json b/packages/react/src/generators/storybook-configuration/schema.json index 599e0eb854e471..56e52eb072df4a 100644 --- a/packages/react/src/generators/storybook-configuration/schema.json +++ b/packages/react/src/generators/storybook-configuration/schema.json @@ -29,7 +29,7 @@ "configureCypress": { "type": "boolean", "description": "Run the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "generateStories": { "type": "boolean", @@ -41,7 +41,7 @@ "generateCypressSpecs": { "type": "boolean", "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "configureStaticServe": { "type": "boolean", @@ -53,7 +53,7 @@ "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Placed at the root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "js": { "type": "boolean", diff --git a/packages/react/src/utils/component-props.ts b/packages/react/src/utils/component-props.ts index c8c3bf8bd3c570..655be8b17d02d7 100644 --- a/packages/react/src/utils/component-props.ts +++ b/packages/react/src/utils/component-props.ts @@ -26,7 +26,18 @@ export function getArgsDefaultValue(property: ts.SyntaxKind): string { export function getDefaultsForComponent( sourceFile: ts.SourceFile, cmpDeclaration: ts.Node -) { +): { + propsTypeName: string; + props: { + name: string; + defaultValue: any; + }[]; + argTypes: { + name: string; + type: string; + actionText: string; + }[]; +} { if (!tsModule) { tsModule = ensureTypescript(); } diff --git a/packages/storybook/index.ts b/packages/storybook/index.ts index 731af7dde6584c..5ddc34d2cd395f 100644 --- a/packages/storybook/index.ts +++ b/packages/storybook/index.ts @@ -1,3 +1,7 @@ export { configurationGenerator } from './src/generators/configuration/configuration'; export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project'; export { storybookVersion } from './src/utils/versions'; +export { + interactionTestsDependencies, + addInteractionsInAddons, +} from './src/generators/configuration/lib/interaction-testing.utils'; diff --git a/packages/storybook/src/generators/configuration/configuration-nested.spec.ts b/packages/storybook/src/generators/configuration/configuration-nested.spec.ts index 124d6560b67b5e..95b00b59e02632 100644 --- a/packages/storybook/src/generators/configuration/configuration-nested.spec.ts +++ b/packages/storybook/src/generators/configuration/configuration-nested.spec.ts @@ -82,7 +82,7 @@ describe('@nx/storybook:configuration for workspaces with Root project', () => { })); }); - it('should generate files for root app', async () => { + it('should generate files for root app - js for tsConfiguration: false', async () => { await configurationGenerator(tree, { name: 'web', uiFramework: '@storybook/react-webpack5', diff --git a/packages/storybook/src/generators/configuration/configuration.spec.ts b/packages/storybook/src/generators/configuration/configuration.spec.ts index 681ddb8ec20b17..ae6ceb745cb8c9 100644 --- a/packages/storybook/src/generators/configuration/configuration.spec.ts +++ b/packages/storybook/src/generators/configuration/configuration.spec.ts @@ -56,11 +56,10 @@ describe('@nx/storybook:configuration for Storybook v7', () => { })); }); - it('should generate TypeScript Configuration files', async () => { + it('should generate TypeScript Configuration files by default', async () => { await configurationGenerator(tree, { name: 'test-ui-lib', standaloneConfig: false, - tsConfiguration: true, uiFramework: '@storybook/angular', }); const project = readProjectConfiguration(tree, 'test-ui-lib'); @@ -172,11 +171,10 @@ describe('@nx/storybook:configuration for Storybook v7', () => { ).toBeTruthy(); }); - it('should generate TS config for project if tsConfiguration true', async () => { + it('should generate TS config for project by default', async () => { await configurationGenerator(tree, { name: 'test-ui-lib', standaloneConfig: false, - tsConfiguration: true, uiFramework: '@storybook/angular', }); @@ -278,7 +276,6 @@ describe('@nx/storybook:configuration for Storybook v7', () => { }); await configurationGenerator(tree, { name: 'main-vite-ts', - tsConfiguration: true, uiFramework: '@storybook/react-vite', }); await configurationGenerator(tree, { diff --git a/packages/storybook/src/generators/configuration/configuration.ts b/packages/storybook/src/generators/configuration/configuration.ts index 1465d010bece20..68b95ed7aaa3a0 100644 --- a/packages/storybook/src/generators/configuration/configuration.ts +++ b/packages/storybook/src/generators/configuration/configuration.ts @@ -37,12 +37,10 @@ import { import { coreJsVersion, nxVersion, - storybookJestVersion, - storybookTestingLibraryVersion, - storybookTestRunnerVersion, storybookVersion, tsNodeVersion, } from '../../utils/versions'; +import { interactionTestsDependencies } from './lib/interaction-testing.utils'; export async function configurationGenerator( tree: Tree, @@ -151,7 +149,7 @@ export async function configurationGenerator( addStaticTarget(tree, schema); } - // TODO(katerina): remove Cypress for Nx 18 + // TODO(v18): remove Cypress if (schema.configureCypress) { const e2eProject = await getE2EProjectName(tree, schema.name); if (!e2eProject) { @@ -174,7 +172,7 @@ export async function configurationGenerator( } } - const devDeps = {}; + let devDeps = {}; if (schema.tsConfiguration) { devDeps['@storybook/core-common'] = storybookVersion; @@ -182,10 +180,10 @@ export async function configurationGenerator( } if (schema.interactionTests) { - devDeps['@storybook/test-runner'] = storybookTestRunnerVersion; - devDeps['@storybook/addon-interactions'] = storybookVersion; - devDeps['@storybook/testing-library'] = storybookTestingLibraryVersion; - devDeps['@storybook/jest'] = storybookJestVersion; + devDeps = { + ...devDeps, + ...interactionTestsDependencies(), + }; } if (schema.configureStaticServe) { diff --git a/packages/storybook/src/generators/configuration/lib/__snapshots__/interaction-testing.utils.spec.ts.snap b/packages/storybook/src/generators/configuration/lib/__snapshots__/interaction-testing.utils.spec.ts.snap new file mode 100644 index 00000000000000..15ce4a62e3b01d --- /dev/null +++ b/packages/storybook/src/generators/configuration/lib/__snapshots__/interaction-testing.utils.spec.ts.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Helper functions for the Storybook 7 migration generator should add addon-interactions in main.ts if it does not exist 1`] = ` +"import type { StorybookConfig } from '@storybook/angular'; + + const config: StorybookConfig = { + stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-interactions', '@storybook/addon-essentials'], + framework: { + name: '@storybook/angular', + options: {}, + }, + }; + + export default config;" +`; + +exports[`Helper functions for the Storybook 7 migration generator should do nothing if addon-interactions already exists in main.ts 1`] = ` +"import type { StorybookConfig } from '@storybook/angular'; + + const config: StorybookConfig = { + stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/angular', + options: {}, + }, + }; + + export default config;" +`; diff --git a/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.spec.ts b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.spec.ts new file mode 100644 index 00000000000000..ef4172e79f3c6b --- /dev/null +++ b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.spec.ts @@ -0,0 +1,70 @@ +import { ProjectConfiguration, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { addInteractionsInAddons } from './interaction-testing.utils'; +describe('Helper functions for the Storybook 7 migration generator', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should add addon-interactions in main.ts if it does not exist', () => { + tree.write( + `.storybook/main.ts`, + `import type { StorybookConfig } from '@storybook/angular'; + + const config: StorybookConfig = { + stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials'], + framework: { + name: '@storybook/angular', + options: {}, + }, + }; + + export default config;` + ); + addInteractionsInAddons(tree, { + name: 'my-proj', + targets: { + storybook: { + executor: '@nx/storybook:storybook', + options: { + configDir: `.storybook`, + }, + }, + }, + } as unknown as ProjectConfiguration); + expect(tree.read(`.storybook/main.ts`, 'utf-8')).toMatchSnapshot(); + }); + + it('should do nothing if addon-interactions already exists in main.ts', () => { + tree.write( + `.storybook/main.ts`, + `import type { StorybookConfig } from '@storybook/angular'; + + const config: StorybookConfig = { + stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/angular', + options: {}, + }, + }; + + export default config;` + ); + addInteractionsInAddons(tree, { + name: 'my-proj', + targets: { + storybook: { + executor: '@nx/storybook:storybook', + options: { + configDir: `.storybook`, + }, + }, + }, + } as unknown as ProjectConfiguration); + expect(tree.read(`.storybook/main.ts`, 'utf-8')).toMatchSnapshot(); + }); +}); diff --git a/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts new file mode 100644 index 00000000000000..da45bfa651cb8b --- /dev/null +++ b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts @@ -0,0 +1,90 @@ +import { + applyChangesToString, + ChangeType, + ProjectConfiguration, + Tree, +} from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { + storybookJestVersion, + storybookTestingLibraryVersion, + storybookTestRunnerVersion, + storybookVersion, +} from '../../../utils/versions'; + +export function interactionTestsDependencies(): { [key: string]: string } { + return { + '@storybook/test-runner': storybookTestRunnerVersion, + '@storybook/addon-interactions': storybookVersion, + '@storybook/testing-library': storybookTestingLibraryVersion, + '@storybook/jest': storybookJestVersion, + }; +} + +export function addInteractionsInAddons( + tree: Tree, + projectConfig: ProjectConfiguration +) { + const mainJsTsPath = getMainTsJsPath(tree, projectConfig); + if (mainJsTsPath) { + let mainJsTs = tree.read(mainJsTsPath, 'utf-8'); + if (mainJsTs) { + const addonsArray = tsquery.query( + mainJsTs, + `PropertyAssignment:has(Identifier:has([name="addons"]))` + ); + if (addonsArray?.length > 0) { + const addonsArrayHasAddonInteractions = tsquery.query( + addonsArray[0], + `StringLiteral:has([text="@storybook/addon-interactions"])` + ); + if (addonsArrayHasAddonInteractions?.length > 0) { + return; + } else { + const arrayLiteralExpression = tsquery.query( + addonsArray[0], + `ArrayLiteralExpression` + )?.[0]; + if (arrayLiteralExpression) { + // normally it should exist + mainJsTs = applyChangesToString(mainJsTs, [ + { + type: ChangeType.Insert, + index: arrayLiteralExpression.getStart() + 1, + text: `'@storybook/addon-interactions', `, + }, + ]); + tree.write(mainJsTsPath, mainJsTs); + } + } + } else { + // No addons array. + // Do nothing, because user may be importing stories in another project. + } + } + } +} + +function getMainTsJsPath( + host: Tree, + projectConfig: ProjectConfiguration +): string | undefined { + let mainJsTsPath: string | undefined = undefined; + Object.entries(projectConfig.targets).forEach( + ([_targetName, targetConfig]) => { + if ( + targetConfig.executor === '@nx/storybook:storybook' || + targetConfig.executor === '@storybook/angular:start-storybook' + ) { + const configDir = targetConfig.options?.configDir; + if (host.exists(`${configDir}/main.js`)) { + mainJsTsPath = `${configDir}/main.js`; + } + if (host.exists(`${configDir}/main.ts`)) { + mainJsTsPath = `${configDir}/main.ts`; + } + } + } + ); + return mainJsTsPath; +} diff --git a/packages/storybook/src/generators/configuration/schema.d.ts b/packages/storybook/src/generators/configuration/schema.d.ts index c41c2523a5c8dd..715148a762e190 100644 --- a/packages/storybook/src/generators/configuration/schema.d.ts +++ b/packages/storybook/src/generators/configuration/schema.d.ts @@ -4,13 +4,19 @@ import { UiFramework7, UiFramework } from '../../utils/models'; export interface StorybookConfigureSchema { name: string; uiFramework?: UiFramework7; - configureCypress?: boolean; linter?: Linter; js?: boolean; interactionTests?: boolean; tsConfiguration?: boolean; - cypressDirectory?: string; standaloneConfig?: boolean; configureStaticServe?: boolean; skipFormat?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + configureCypress?: boolean; + /** + * @deprecated Use interactionTests instead. This option will be removed in v18. + */ + cypressDirectory?: string; } diff --git a/packages/storybook/src/generators/configuration/schema.json b/packages/storybook/src/generators/configuration/schema.json index 34190a94b63936..07e60ea50a188b 100644 --- a/packages/storybook/src/generators/configuration/schema.json +++ b/packages/storybook/src/generators/configuration/schema.json @@ -28,12 +28,12 @@ "configureCypress": { "type": "boolean", "description": "Run the cypress-configure generator.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "cypressDirectory": { "type": "string", "description": "A directory where the Cypress project will be placed. Added at root by default.", - "x-deprecated": "Please use Storybook interaction tests instead." + "x-deprecated": "Use interactionTests instead. This option will be removed in v18." }, "linter": { "description": "The tool to use for running lint checks.",