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..b6d3bf6c8cff5d 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": "Please use Storybook interaction tests instead." }, "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": "Please use Storybook interaction tests instead." }, "skipFormat": { "description": "Skip formatting files.", 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..bdc41eae362288 100644 --- a/packages/angular/src/generators/stories/schema.d.ts +++ b/packages/angular/src/generators/stories/schema.d.ts @@ -1,5 +1,6 @@ export interface StoriesGeneratorOptions { name: string; + interactionTests?: boolean; cypressProject?: string; generateCypressSpecs?: boolean; skipFormat?: boolean; diff --git a/packages/angular/src/generators/stories/schema.json b/packages/angular/src/generators/stories/schema.json index b97cfe83f5f194..a7e638e196f558 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": "Please use Storybook interaction tests instead." }, "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": "Please use Storybook interaction tests instead." }, "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..177f6fc5e96315 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(katerina): remove Cypress for Nx 18 + 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..ea0195213302f7 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(katerina): remove Cypress for Nx 18 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..37717879659150 100644 --- a/packages/angular/src/generators/storybook-configuration/schema.d.ts +++ b/packages/angular/src/generators/storybook-configuration/schema.d.ts @@ -1,9 +1,9 @@ import type { Linter } from '@nx/linter'; export interface StorybookConfigurationOptions { - configureCypress: boolean; + configureCypress?: boolean; configureStaticServe?: boolean; - generateCypressSpecs: boolean; + generateCypressSpecs?: boolean; generateStories: boolean; linter: Linter; name: string; 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..3bc83e2cc9da00 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(katerina): remove Cypress for Nx 18 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/stories.ts b/packages/react/src/generators/stories/stories.ts index 15be2f42562592..27de3918d9f455 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'); @@ -87,13 +92,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 +170,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/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..bd59dac312396c 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, @@ -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..19762aeb9df2b3 --- /dev/null +++ b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.spec.ts @@ -0,0 +1,50 @@ +import { Tree, joinPathFragments } 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, `.storybook/main.ts`); + 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, `.storybook/main.ts`); + 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; +}