diff --git a/docs/generated/packages/react/generators/component-story.json b/docs/generated/packages/react/generators/component-story.json index 1a959e4cac315e..5122138cb60da4 100644 --- a/docs/generated/packages/react/generators/component-story.json +++ b/docs/generated/packages/react/generators/component-story.json @@ -30,6 +30,12 @@ "type": "boolean", "default": false, "x-priority": "internal" + }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "default": true, + "x-priority": "important" } }, "required": ["project", "componentPath"], diff --git a/docs/generated/packages/react/generators/stories.json b/docs/generated/packages/react/generators/stories.json index 76138b57a15aae..af888ff74c2292 100644 --- a/docs/generated/packages/react/generators/stories.json +++ b/docs/generated/packages/react/generators/stories.json @@ -20,12 +20,19 @@ "generateCypressSpecs": { "type": "boolean", "description": "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 `project` by default." + "description": "The Cypress project to generate the stories under. This is inferred from `project` by default.", + "x-deprecated": "Please use Storybook interaction tests instead." + }, + "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 }, "js": { "type": "boolean", 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 8cf084cfe3722d..ef8678b685a41e 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 @@ -1,14 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: PureComponent class & then default export new JSX transform should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -16,18 +20,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: PureComponent class & then default export should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -35,18 +50,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: arrow function should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -54,18 +80,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: arrow function without {..} should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -73,18 +110,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: component class & then default export new JSX transform should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -92,18 +140,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: component class & then default export should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -111,18 +170,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: default export function should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -130,18 +200,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: direct export of component class new JSX transform should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -149,18 +230,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: direct export of component class should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -168,18 +260,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with DEFAULT export React component defined as: function and then export should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -187,18 +290,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default PureComponent class & then default export new JSX transform should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -206,18 +320,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default PureComponent class & then default export should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -225,18 +350,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default arrow function should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -244,18 +380,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default arrow function without {..} should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -263,18 +410,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default component class should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -282,18 +440,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default direct export of component class new JSX transform should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -301,18 +470,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default direct export of component class should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -320,18 +500,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export React component defined as: no default simple export function should properly setup the controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -339,71 +530,115 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export should create stories for all components in a file with no default export 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { One } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: One, title: 'One', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: {}, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to One!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export should create stories for all components in a file with no default export 2`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Two } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Two, title: 'Two', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: {}, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Two!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup Other types of component definitions Component files with NO DEFAULT export should create stories for all components in a file with no default export 3`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Three } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Three, title: 'Three', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { name: '', }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Three!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup component with props and actions should setup controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', argTypes: { someAction: { action: 'someAction executed!' }, }, }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -411,18 +646,29 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup component with props should setup controls based on the component props 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: { @@ -430,43 +676,75 @@ export const Primary = { displayAge: false, }, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup component without any props defined should create a story without controls 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { Test } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: Test, title: 'Test', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: {}, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup default component setup should properly set up the story 1`] = ` -"import type { Meta } from '@storybook/react'; +"import type { Meta, StoryObj } from '@storybook/react'; import { TestUiLib } from './test-ui-lib'; -const Story: Meta = { +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { component: TestUiLib, title: 'TestUiLib', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: {}, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); + }, +}; " `; exports[`react:component-story default setup when using plain JS components should properly set up the story 1`] = ` -"import Test from './test-ui-libplain'; +"import componentName from './test-ui-libplain'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; export default { component: Test, @@ -476,18 +754,26 @@ export default { export const Primary = { args: {}, }; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; " `; -exports[`react:component-story using eslint should properly set up the story 1`] = ` -"import type { Meta } from '@storybook/react'; +exports[`react:component-story using eslint - not using interaction tests should properly set up the story 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; import { TestUiLib } from './test-ui-lib'; -const Story: Meta = { +const meta: Meta = { component: TestUiLib, title: 'TestUiLib', }; -export default Story; +export default meta; +type Story = StoryObj; export const Primary = { args: {}, diff --git a/packages/react/src/generators/component-story/component-story.spec.ts b/packages/react/src/generators/component-story/component-story.spec.ts index 9a77bdd4dde4e4..8c8f6e87ef35d9 100644 --- a/packages/react/src/generators/component-story/component-story.spec.ts +++ b/packages/react/src/generators/component-story/component-story.spec.ts @@ -27,6 +27,7 @@ describe('react:component-story', () => { await componentStoryGenerator(appTree, { componentPath: 'lib/test-ui-lib.tsx', project: 'test-ui-lib', + interactionTests: true, }); } catch (e) { expect(e.message).toContain( @@ -506,12 +507,13 @@ describe('react:component-story', () => { }); }); - describe('using eslint', () => { + describe('using eslint - not using interaction tests', () => { beforeEach(async () => { appTree = await createTestUILib('test-ui-lib'); await componentStoryGenerator(appTree, { componentPath: 'lib/test-ui-lib.tsx', project: 'test-ui-lib', + interactionTests: false, }); }); diff --git a/packages/react/src/generators/component-story/component-story.ts b/packages/react/src/generators/component-story/component-story.ts index 6c1f494f962624..2507e1081120e7 100644 --- a/packages/react/src/generators/component-story/component-story.ts +++ b/packages/react/src/generators/component-story/component-story.ts @@ -20,12 +20,13 @@ let tsModule: typeof import('typescript'); export interface CreateComponentStoriesFileSchema { project: string; componentPath: string; + interactionTests?: boolean; skipFormat?: boolean; } export function createComponentStoriesFile( host: Tree, - { project, componentPath }: CreateComponentStoriesFileSchema + { project, componentPath, interactionTests }: CreateComponentStoriesFileSchema ) { if (!tsModule) { tsModule = ensureTypescript(); @@ -42,12 +43,6 @@ export function createComponentStoriesFile( const isPlainJs = componentFilePath.endsWith('.jsx') || componentFilePath.endsWith('.js'); - let fileExt = 'tsx'; - if (componentFilePath.endsWith('.jsx')) { - fileExt = 'jsx'; - } else if (componentFilePath.endsWith('.js')) { - fileExt = 'js'; - } const componentFileName = componentFilePath .slice(componentFilePath.lastIndexOf('/') + 1) @@ -81,8 +76,8 @@ export function createComponentStoriesFile( declaration, componentDirectory, name, + interactionTests, isPlainJs, - fileExt, componentNodes.length > 1 ); }); @@ -98,8 +93,8 @@ export function createComponentStoriesFile( cmpDeclaration, componentDirectory, name, - isPlainJs, - fileExt + interactionTests, + isPlainJs ); } } @@ -110,8 +105,8 @@ export function findPropsAndGenerateFile( cmpDeclaration: ts.Node, componentDirectory: string, name: string, + interactionTests: boolean, isPlainJs: boolean, - fileExt: string, fromNodeArray?: boolean ) { const { propsTypeName, props, argTypes } = getDefaultsForComponent( @@ -121,9 +116,10 @@ export function findPropsAndGenerateFile( generateFiles( host, - joinPathFragments(__dirname, './files'), + joinPathFragments(__dirname, `./files${isPlainJs ? '/jsx' : '/tsx'}`), normalizePath(componentDirectory), { + tmpl: '', componentFileName: fromNodeArray ? `${name}--${(cmpDeclaration as any).name.text}` : name, @@ -132,8 +128,7 @@ export function findPropsAndGenerateFile( props, argTypes, componentName: (cmpDeclaration as any).name.text, - isPlainJs, - fileExt, + interactionTests, } ); } @@ -142,7 +137,10 @@ export async function componentStoryGenerator( host: Tree, schema: CreateComponentStoriesFileSchema ) { - createComponentStoriesFile(host, schema); + createComponentStoriesFile(host, { + ...schema, + interactionTests: schema.interactionTests ?? true, + }); if (!schema.skipFormat) { await formatFiles(host); diff --git a/packages/react/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ b/packages/react/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ deleted file mode 100644 index 12b9ccd9b1f5cf..00000000000000 --- a/packages/react/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ +++ /dev/null @@ -1,32 +0,0 @@ -<% if ( !isPlainJs ) { %>import type { Meta } from '@storybook/react';<% } %> -import<% if ( !isPlainJs ) { %> { <% } %> <%= componentName %> <% if ( !isPlainJs ) { %> } <% } %> from './<%= componentImportFileName %>'; - -<% if ( isPlainJs ) { %> -export default { - component: <%= componentName %>, - title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> - argTypes: {<% for (let argType of argTypes) { %> - <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> -} - <% } %> -}; -<% } %> - - -<% if ( !isPlainJs ) { %> -const Story: Meta> = { - component: <%= componentName %>, - title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> - argTypes: {<% for (let argType of argTypes) { %> - <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> -} - <% } %> -}; -export default Story; -<% } %> - -export const Primary = { - args: {<% for (let prop of props) { %> - <%= prop.name %>: <%- prop.defaultValue %>,<% } %> - }, -}; \ No newline at end of file diff --git a/packages/react/src/generators/component-story/files/jsx/__componentFileName__.stories.jsx__tmpl__ b/packages/react/src/generators/component-story/files/jsx/__componentFileName__.stories.jsx__tmpl__ new file mode 100644 index 00000000000000..5750a734eab0a4 --- /dev/null +++ b/packages/react/src/generators/component-story/files/jsx/__componentFileName__.stories.jsx__tmpl__ @@ -0,0 +1,29 @@ +import componentName from './<%= componentImportFileName %>'; +<% if ( interactionTests ) { %> +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +<% } %> + +export default { + component: <%= componentName %>, + title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> + argTypes: {<% for (let argType of argTypes) { %> + <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> +} + <% } %> +}; + +export const Primary = { + args: {<% for (let prop of props) { %> + <%= prop.name %>: <%- prop.defaultValue %>,<% } %> + }, +}; + +<% if ( interactionTests ) { %> +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy(); + }, +}; +<% } %> \ No newline at end of file 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__ new file mode 100644 index 00000000000000..257b72934ad3b2 --- /dev/null +++ b/packages/react/src/generators/component-story/files/tsx/__componentFileName__.stories.tsx__tmpl__ @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { <%= componentName %> } from './<%= componentImportFileName %>'; +<% if ( interactionTests ) { %> +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +<% } %> + +const meta: Meta> = { + component: <%= componentName %>, + title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> + argTypes: {<% for (let argType of argTypes) { %> + <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> +} + <% } %> +}; +export default meta; +type Story = StoryObj>; + + +export const Primary = { + args: {<% for (let prop of props) { %> + <%= prop.name %>: <%- prop.defaultValue %>,<% } %> + }, +}; + +<% if ( interactionTests ) { %> +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy(); + }, +}; +<% } %> diff --git a/packages/react/src/generators/component-story/schema.json b/packages/react/src/generators/component-story/schema.json index df34160896a388..d7e48a93d92c4e 100644 --- a/packages/react/src/generators/component-story/schema.json +++ b/packages/react/src/generators/component-story/schema.json @@ -30,6 +30,12 @@ "type": "boolean", "default": false, "x-priority": "internal" + }, + "interactionTests": { + "type": "boolean", + "description": "Set up Storybook interaction tests.", + "default": true, + "x-priority": "important" } }, "required": ["project", "componentPath"] 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 new file mode 100644 index 00000000000000..9c6abe28dbe8ab --- /dev/null +++ b/packages/react/src/generators/stories/__snapshots__/stories.app.spec.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react:stories for applications should create the stories with interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { NxWelcome } from './nx-welcome'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: NxWelcome, + title: 'NxWelcome', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`react:stories for applications should create the stories with interaction tests 2`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { Test } from './another-cmp'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: Test, + title: 'Test', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: { + name: '', + displayAge: false, + }, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`react:stories for applications should create the stories without interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { NxWelcome } from './nx-welcome'; + +const meta: Meta = { + component: NxWelcome, + title: 'NxWelcome', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; +" +`; + +exports[`react:stories for applications should create the stories without interaction tests 2`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { Test } from './another-cmp'; + +const meta: Meta = { + component: Test, + title: 'Test', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: { + name: '', + displayAge: false, + }, +}; +" +`; + +exports[`react:stories for applications should not update existing stories 1`] = ` +"import { ComponentStory, ComponentMeta } from '@storybook/react'; +" +`; 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 new file mode 100644 index 00000000000000..4b3b4afe271248 --- /dev/null +++ b/packages/react/src/generators/stories/__snapshots__/stories.lib.spec.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react:stories for libraries should create the stories with interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { TestUiLib } from './test-ui-lib'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: TestUiLib, + title: 'TestUiLib', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`react:stories for libraries should create the stories with interaction tests 2`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { Test } from './another-cmp'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: Test, + title: 'Test', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: { + name: '', + displayAge: false, + }, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`react:stories for libraries should create the stories without interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { TestUiLib } from './test-ui-lib'; + +const meta: Meta = { + component: TestUiLib, + title: 'TestUiLib', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; +" +`; + +exports[`react:stories for libraries should create the stories without interaction tests 2`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { Test } from './another-cmp'; + +const meta: Meta = { + component: Test, + title: 'Test', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: { + name: '', + displayAge: false, + }, +}; +" +`; diff --git a/packages/react/src/generators/stories/schema.json b/packages/react/src/generators/stories/schema.json index 3fdd31177510f5..10749a54c4d710 100644 --- a/packages/react/src/generators/stories/schema.json +++ b/packages/react/src/generators/stories/schema.json @@ -20,12 +20,19 @@ "generateCypressSpecs": { "type": "boolean", "description": "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 `project` by default." + "description": "The Cypress project to generate the stories under. This is inferred from `project` by default.", + "x-deprecated": "Please use Storybook interaction tests instead." + }, + "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 }, "js": { "type": "boolean", diff --git a/packages/react/src/generators/stories/stories.app.spec.ts b/packages/react/src/generators/stories/stories.app.spec.ts index 4f581029ffc37b..762c95f8453bbd 100644 --- a/packages/react/src/generators/stories/stories.app.spec.ts +++ b/packages/react/src/generators/stories/stories.app.spec.ts @@ -41,36 +41,37 @@ describe('react:stories for applications', () => { ); }); - it('should create the stories', async () => { + it('should create the stories with interaction tests', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, }); expect( - appTree.exists('apps/test-ui-app/src/app/nx-welcome.stories.tsx') - ).toBeTruthy(); + appTree.read('apps/test-ui-app/src/app/nx-welcome.stories.tsx', 'utf-8') + ).toMatchSnapshot(); expect( - appTree.exists( - 'apps/test-ui-app/src/app/anothercmp/another-cmp.stories.tsx' + appTree.read( + 'apps/test-ui-app/src/app/anothercmp/another-cmp.stories.tsx', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); - it('should generate Cypress specs', async () => { + it('should create the stories without interaction tests', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: true, + interactionTests: false, }); expect( - appTree.exists('apps/test-ui-app-e2e/src/e2e/app.cy.ts') - ).toBeTruthy(); + appTree.read('apps/test-ui-app/src/app/nx-welcome.stories.tsx', 'utf-8') + ).toMatchSnapshot(); expect( - appTree.exists( - 'apps/test-ui-app-e2e/src/e2e/another-cmp/another-cmp.cy.ts' + appTree.read( + 'apps/test-ui-app/src/app/anothercmp/another-cmp.stories.tsx', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); it('should ignore files that do not contain components', async () => { @@ -82,7 +83,6 @@ describe('react:stories for applications', () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, }); // should just create the story and not error, even though there's a js file @@ -93,24 +93,18 @@ describe('react:stories for applications', () => { }); it('should not update existing stories', async () => { - // ARRANGE appTree.write( 'apps/test-ui-app/src/app/nx-welcome.stories.tsx', `import { ComponentStory, ComponentMeta } from '@storybook/react'` ); - // ACT await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, }); - // ASSERT expect( appTree.read('apps/test-ui-app/src/app/nx-welcome.stories.tsx', 'utf-8') - ).toEqual( - `import { ComponentStory, ComponentMeta } from '@storybook/react';\n` - ); + ).toMatchSnapshot(); }); describe('ignore paths', () => { @@ -164,7 +158,6 @@ describe('react:stories for applications', () => { it('should generate stories for all if no ignorePaths', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, }); expect( @@ -192,7 +185,6 @@ describe('react:stories for applications', () => { it('should ignore entire paths', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, ignorePaths: [ `apps/test-ui-app/src/app/anothercmp/**`, `**/**/src/**/test-path/ignore-it/**`, @@ -224,7 +216,6 @@ describe('react:stories for applications', () => { it('should ignore path or a pattern', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, ignorePaths: [ 'apps/test-ui-app/src/app/anothercmp/**/*.skip.*', '**/**/src/**/test-path/**', @@ -256,7 +247,6 @@ describe('react:stories for applications', () => { it('should ignore direct path to component', async () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, ignorePaths: ['apps/test-ui-app/src/app/anothercmp/**/*.skip.tsx'], }); @@ -308,7 +298,6 @@ describe('react:stories for applications', () => { await storiesGenerator(appTree, { project: 'test-ui-app', - generateCypressSpecs: false, ignorePaths: [ 'apps/test-ui-app/src/app/anothercmp/another-cmp-test.skip.tsx', ], diff --git a/packages/react/src/generators/stories/stories.lib.spec.ts b/packages/react/src/generators/stories/stories.lib.spec.ts index 93eb95768d887d..009e63d7a2c299 100644 --- a/packages/react/src/generators/stories/stories.lib.spec.ts +++ b/packages/react/src/generators/stories/stories.lib.spec.ts @@ -36,42 +36,38 @@ describe('react:stories for libraries', () => { ); }); - it('should create the stories', async () => { + it('should create the stories with interaction tests', async () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: false, }); expect( - appTree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx') - ).toBeTruthy(); + appTree.read('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx', 'utf-8') + ).toMatchSnapshot(); expect( - appTree.exists( - 'libs/test-ui-lib/src/lib/anothercmp/another-cmp.stories.tsx' + appTree.read( + 'libs/test-ui-lib/src/lib/anothercmp/another-cmp.stories.tsx', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); - it('should generate Cypress specs', async () => { + it('should create the stories without interaction tests', async () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: true, + interactionTests: false, }); - expect( - appTree.exists( - 'apps/test-ui-lib-e2e/src/integration/test-ui-lib/test-ui-lib.spec.ts' - ) - ).toBeTruthy(); + appTree.read('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx', 'utf-8') + ).toMatchSnapshot(); expect( - appTree.exists( - 'apps/test-ui-lib-e2e/src/integration/another-cmp/another-cmp.spec.ts' + appTree.read( + 'libs/test-ui-lib/src/lib/anothercmp/another-cmp.stories.tsx', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); - it('should not overwrite existing stories', () => {}); - describe('ignore paths', () => { beforeEach(() => { appTree.write( @@ -119,7 +115,6 @@ describe('react:stories for libraries', () => { it('should generate stories for all if no ignorePaths', async () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: false, }); expect( @@ -144,7 +139,6 @@ describe('react:stories for libraries', () => { it('should ignore entire paths', async () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: false, ignorePaths: [ 'libs/test-ui-lib/src/lib/anothercmp/**', '**/**/src/**/test-path/ignore-it/**', @@ -173,7 +167,6 @@ describe('react:stories for libraries', () => { it('should ignore path or a pattern', async () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: false, ignorePaths: [ 'libs/test-ui-lib/src/lib/anothercmp/**/*.skip.*', '**/test-ui-lib/src/**/test-path/**', @@ -209,7 +202,6 @@ describe('react:stories for libraries', () => { await storiesGenerator(appTree, { project: 'test-ui-lib', - generateCypressSpecs: false, }); // should just create the story and not error, even though there's a js file diff --git a/packages/react/src/generators/stories/stories.nextjs.spec.ts b/packages/react/src/generators/stories/stories.nextjs.spec.ts index 39e95f889da37c..aac9b1b96a8a44 100644 --- a/packages/react/src/generators/stories/stories.nextjs.spec.ts +++ b/packages/react/src/generators/stories/stories.nextjs.spec.ts @@ -32,10 +32,20 @@ describe('nextjs:stories for applications', () => { ); }); - it('should create the stories', async () => { + it('should create the stories with interaction tests', async () => { await storiesGenerator(tree, { project: 'test-ui-app', - generateCypressSpecs: false, + }); + + expect( + tree.exists('apps/test-ui-app/components/test.stories.tsx') + ).toBeTruthy(); + }); + + it('should create the stories without interaction tests', async () => { + await storiesGenerator(tree, { + project: 'test-ui-app', + interactionTests: false, }); expect( @@ -46,7 +56,6 @@ describe('nextjs:stories for applications', () => { it('should ignore paths', async () => { await storiesGenerator(tree, { project: 'test-ui-app', - generateCypressSpecs: false, ignorePaths: ['apps/test-ui-app/components/**'], }); diff --git a/packages/react/src/generators/stories/stories.ts b/packages/react/src/generators/stories/stories.ts index 30afa8be0d42b6..15be2f42562592 100644 --- a/packages/react/src/generators/stories/stories.ts +++ b/packages/react/src/generators/stories/stories.ts @@ -22,9 +22,10 @@ let tsModule: typeof import('typescript'); export interface StorybookStoriesSchema { project: string; - generateCypressSpecs: boolean; + interactionTests?: boolean; js?: boolean; cypressProject?: string; + generateCypressSpecs?: boolean; ignorePaths?: string[]; skipFormat?: boolean; } @@ -84,8 +85,9 @@ export function containsComponentDeclaration( export async function createAllStories( tree: Tree, projectName: string, - generateCypressSpecs: boolean, + interactionTests: boolean, js: boolean, + generateCypressSpecs?: boolean, cypressProject?: string, ignorePaths?: string[] ) { @@ -142,6 +144,7 @@ export async function createAllStories( componentPath: relativeCmpDir, project: projectName, skipFormat: true, + interactionTests, }); if (generateCypressSpecs && e2eProject) { @@ -164,8 +167,9 @@ export async function storiesGenerator( await createAllStories( host, schema.project, - schema.generateCypressSpecs, + schema.interactionTests ?? true, schema.js, + schema.generateCypressSpecs, schema.cypressProject, schema.ignorePaths ); 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 new file mode 100644 index 00000000000000..54b8e3f57a168a --- /dev/null +++ b/packages/react/src/generators/storybook-configuration/__snapshots__/configuration.spec.ts.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react:storybook-configuration should configure everything at once 1`] = ` +"import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: { + builder: { + viteConfigPath: '', + }, + }, + }, +}; + +export default config; + +// To customize your Vite configuration you can use the viteFinal field. +// Check https://storybook.js.org/docs/react/builders/vite#configuration +// and https://nx.dev/packages/storybook/documents/custom-builder-configs +" +`; + +exports[`react:storybook-configuration should generate stories for components 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { MyComponent } from './my-component'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +const meta: Meta = { + component: MyComponent, + title: 'MyComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to MyComponent!/gi)).toBeTruthy(); + }, +}; +" +`; + +exports[`react:storybook-configuration should generate stories for components without interaction tests 1`] = ` +"import type { Meta, StoryObj } from '@storybook/react'; +import { MyComponent } from './my-component'; + +const meta: Meta = { + component: MyComponent, + title: 'MyComponent', +}; +export default meta; +type Story = StoryObj; + +export const Primary = { + args: {}, +}; +" +`; + +exports[`react:storybook-configuration should generate stories for components written in plain JS 1`] = ` +"import componentName from './test-ui-libplain'; + +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +export default { + component: Test, + title: 'Test', +}; + +export const Primary = { + args: {}, +}; + +export const Heading: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Welcome to Test!/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 78b4b435b6330f..f3e6f4c9588ca8 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.spec.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.spec.ts @@ -1,3 +1,4 @@ +// TODO(katerina): remove Cypress for Nx 17? import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { logger, Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -37,15 +38,13 @@ describe('react:storybook-configuration', () => { appTree = await createTestUILib('test-ui-lib'); await storybookConfigurationGenerator(appTree, { name: 'test-ui-lib', - configureCypress: true, }); - expect(appTree.exists('libs/test-ui-lib/.storybook/main.js')).toBeTruthy(); expect( - appTree.exists('libs/test-ui-lib/tsconfig.storybook.json') - ).toBeTruthy(); + appTree.read('libs/test-ui-lib/.storybook/main.ts', 'utf-8') + ).toMatchSnapshot(); expect( - appTree.exists('apps/test-ui-lib-e2e/cypress.config.ts') + appTree.exists('libs/test-ui-lib/tsconfig.storybook.json') ).toBeTruthy(); }); @@ -54,7 +53,6 @@ describe('react:storybook-configuration', () => { await storybookConfigurationGenerator(appTree, { name: 'test-ui-lib', generateStories: true, - configureCypress: false, }); expect( @@ -84,37 +82,28 @@ describe('react:storybook-configuration', () => { ); await storybookConfigurationGenerator(appTree, { name: 'test-ui-lib', - generateCypressSpecs: true, generateStories: true, - configureCypress: false, js: true, }); expect( - appTree.exists('libs/test-ui-lib/src/lib/test-ui-libplain.stories.js') - ).toBeTruthy(); + appTree.read( + 'libs/test-ui-lib/src/lib/test-ui-libplain.stories.jsx', + 'utf-8' + ) + ).toMatchSnapshot(); }); it('should configure everything at once', async () => { appTree = await createTestAppLib('test-ui-app'); await storybookConfigurationGenerator(appTree, { name: 'test-ui-app', - configureCypress: true, }); - expect(appTree.exists('apps/test-ui-app/.storybook/main.js')).toBeTruthy(); + expect(appTree.exists('apps/test-ui-app/.storybook/main.ts')).toBeTruthy(); expect( appTree.exists('apps/test-ui-app/tsconfig.storybook.json') ).toBeTruthy(); - - /** - * Note on the removal of - * expect(tree.exists('apps/test-ui-app-e2e/cypress.json')).toBeTruthy(); - * - * When calling createTestAppLib() we do not generate an e2e suite. - * The storybook schematic for apps does not generate e2e test. - * So, there exists no test-ui-app-e2e! - */ }); it('should generate stories for components', async () => { @@ -122,45 +111,33 @@ describe('react:storybook-configuration', () => { await storybookConfigurationGenerator(appTree, { name: 'test-ui-app', generateStories: true, - configureCypress: false, }); // Currently the auto-generate stories feature only picks up components under the 'lib' directory. // In our 'createTestAppLib' function, we call @nx/react:component to generate a component // under the specified 'lib' directory expect( - appTree.exists( - 'apps/test-ui-app/src/app/my-component/my-component.stories.tsx' + appTree.read( + 'apps/test-ui-app/src/app/my-component/my-component.stories.tsx', + 'utf-8' ) - ).toBeTruthy(); + ).toMatchSnapshot(); }); - it('should generate cypress tests in the correct folder', async () => { - appTree = await createTestUILib('test-ui-lib'); - await componentGenerator(appTree, { - name: 'my-component', - project: 'test-ui-lib', - style: 'css', - }); + it('should generate stories for components without interaction tests', async () => { + appTree = await createTestAppLib('test-ui-app'); await storybookConfigurationGenerator(appTree, { - name: 'test-ui-lib', + name: 'test-ui-app', generateStories: true, - configureCypress: true, - generateCypressSpecs: true, - cypressDirectory: 'one/two', - }); - [ - 'apps/one/two/test-ui-lib-e2e/cypress.config.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', - 'apps/one/two/test-ui-lib-e2e/.eslintrc.json', - 'apps/one/two/test-ui-lib-e2e/src/e2e/test-ui-lib/test-ui-lib.cy.ts', - 'apps/one/two/test-ui-lib-e2e/src/e2e/my-component/my-component.cy.ts', - ].forEach((file) => { - expect(appTree.exists(file)).toBeTruthy(); + interactionTests: false, }); + + expect( + appTree.read( + 'apps/test-ui-app/src/app/my-component/my-component.stories.tsx', + 'utf-8' + ) + ).toMatchSnapshot(); }); }); diff --git a/packages/react/src/generators/storybook-configuration/configuration.ts b/packages/react/src/generators/storybook-configuration/configuration.ts index 16d9f81d33aa93..f69f1ce9385f66 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.ts @@ -29,6 +29,7 @@ async function generateStories(host: Tree, schema: StorybookConfigureSchema) { cypressProject, ignorePaths: schema.ignorePaths, skipFormat: true, + interactionTests: schema.interactionTests ?? true, }); } @@ -57,8 +58,8 @@ export async function storybookConfigurationGenerator( js: schema.js, linter: schema.linter, cypressDirectory: schema.cypressDirectory, - tsConfiguration: schema.tsConfiguration, - interactionTests: schema.interactionTests, + tsConfiguration: schema.tsConfiguration ?? true, // default is true + interactionTests: schema.interactionTests ?? true, // default is true configureStaticServe: schema.configureStaticServe, uiFramework: bundler === 'vite' diff --git a/packages/react/src/generators/storybook-configuration/schema.d.ts b/packages/react/src/generators/storybook-configuration/schema.d.ts index 0094e3b6560970..80aabb79f7ff7b 100644 --- a/packages/react/src/generators/storybook-configuration/schema.d.ts +++ b/packages/react/src/generators/storybook-configuration/schema.d.ts @@ -2,14 +2,14 @@ import { Linter } from '@nx/linter'; export interface StorybookConfigureSchema { name: string; - configureCypress: boolean; + interactionTests?: boolean; generateStories?: boolean; + configureCypress?: boolean; generateCypressSpecs?: boolean; js?: boolean; tsConfiguration?: boolean; linter?: Linter; cypressDirectory?: string; ignorePaths?: string[]; - interactionTests?: boolean; configureStaticServe?: boolean; }