feat(storybook): interaction tests generators for angular (#18166)

This commit is contained in:
Katerina Skroumpelou 2023-07-19 17:42:13 +03:00 committed by GitHub
parent 34cde20426
commit 29a9f8eba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 785 additions and 466 deletions

View File

@ -32,6 +32,13 @@
"examples": ["awesome.component"], "examples": ["awesome.component"],
"x-priority": "important" "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": { "skipFormat": {
"description": "Skip formatting files.", "description": "Skip formatting files.",
"type": "boolean", "type": "boolean",

View File

@ -18,15 +18,22 @@
"x-dropdown": "projects", "x-dropdown": "projects",
"x-priority": "important" "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": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to automatically generate `*.spec.ts` files in the Cypress e2e app generated by the `cypress-configure` generator.", "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-deprecated": "Use interactionTests instead. This option will be removed in v18."
"x-priority": "important"
}, },
"cypressProject": { "cypressProject": {
"type": "string", "type": "string",
"description": "The Cypress project to generate the stories under. This is inferred from `name` by default." "description": "The Cypress project to generate the stories under. This is inferred from `name` by default.",
"x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"skipFormat": { "skipFormat": {
"description": "Skip formatting files.", "description": "Skip formatting files.",

View File

@ -29,7 +29,7 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to configure Cypress or not.", "description": "Specifies whether to configure Cypress or not.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"generateStories": { "generateStories": {
"type": "boolean", "type": "boolean",
@ -41,7 +41,7 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.", "description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"configureStaticServe": { "configureStaticServe": {
"type": "boolean", "type": "boolean",
@ -53,7 +53,7 @@
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Placed at the root by default.", "description": "A directory where the Cypress project will be placed. Placed at the root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",

View File

@ -20,12 +20,12 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.", "description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"cypressProject": { "cypressProject": {
"type": "string", "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." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"interactionTests": { "interactionTests": {
"type": "boolean", "type": "boolean",

View File

@ -29,7 +29,7 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Run the cypress-configure generator.", "description": "Run the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"generateStories": { "generateStories": {
"type": "boolean", "type": "boolean",
@ -41,7 +41,7 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"configureStaticServe": { "configureStaticServe": {
"type": "boolean", "type": "boolean",
@ -53,7 +53,7 @@
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Placed at the root by default.", "description": "A directory where the Cypress project will be placed. Placed at the root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"js": { "js": {
"type": "boolean", "type": "boolean",

View File

@ -28,12 +28,12 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Run the cypress-configure generator.", "description": "Run the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Added at root by default.", "description": "A directory where the Cypress project will be placed. Added at root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",

View File

@ -5,11 +5,8 @@ import {
newProject, newProject,
runCLI, runCLI,
runCommandUntil, runCommandUntil,
runCypressTests,
tmpProjPath,
uniq, uniq,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { writeFileSync } from 'fs';
describe('Storybook executors for Angular', () => { describe('Storybook executors for Angular', () => {
const angularStorybookLib = uniq('test-ui-ng-lib'); const angularStorybookLib = uniq('test-ui-ng-lib');
@ -17,7 +14,7 @@ describe('Storybook executors for Angular', () => {
newProject(); newProject();
runCLI(`g @nx/angular:library ${angularStorybookLib} --no-interactive`); runCLI(`g @nx/angular:library ${angularStorybookLib} --no-interactive`);
runCLI( 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`); checkFilesExist(`dist/storybook/${angularStorybookLib}/index.html`);
}, 200_000); }, 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`
),
`
<button
class="my-btn"
[ngStyle]="{ backgroundColor: color }"
[disabled]="disabled"
>
{{ text }}
</button>
`
);
}

View File

@ -1,18 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`componentStory generator should generate the right props 1`] = ` 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'; import { TestButtonComponent } from './test-button.component';
export default { const meta: Meta<TestButtonComponent> = {
title: 'TestButtonComponent',
component: TestButtonComponent, component: TestButtonComponent,
} as Meta<TestButtonComponent>; title: 'TestButtonComponent',
};
export default meta;
type Story = StoryObj<TestButtonComponent>;
export const Primary = { export const Primary: Story = {
render: (args: TestButtonComponent) => ({
props: args,
}),
args: { args: {
buttonType: 'button', buttonType: 'button',
style: 'default', style: 'default',

View File

@ -29,6 +29,8 @@ export async function componentStoryGenerator(
generateFiles(tree, templatesDir, destinationDir, { generateFiles(tree, templatesDir, destinationDir, {
componentFileName: componentFileName, componentFileName: componentFileName,
componentName: componentName, componentName: componentName,
componentNameSimple: componentFileName.replace('.component', ''),
interactionTests: options.interactionTests,
props: props.filter((p) => typeof p.defaultValue !== 'undefined'), props: props.filter((p) => typeof p.defaultValue !== 'undefined'),
tmpl: '', tmpl: '',
}); });

View File

@ -1,16 +1,31 @@
import { Meta } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular';
import { <%=componentName%> } from './<%=componentFileName%>'; import { <%=componentName%> } from './<%=componentFileName%>';
<% if ( interactionTests ) { %>
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
<% } %>
export default { const meta: Meta<<%= componentName %>> = {
title: '<%=componentName%>', component: <%= componentName %>,
component: <%=componentName%> title: '<%= componentName %>',
} as Meta<<%=componentName%>>; };
export default meta;
type Story = StoryObj<<%=componentName%>>;
export const Primary = { export const Primary: Story = {
render: (args: <%=componentName%>) => ({
props: args,
}),
args: {<% for (let prop of props) { %> args: {<% for (let prop of props) { %>
<%= prop.name %>: <%- prop.defaultValue %>,<% } %> <%= prop.name %>: <%- prop.defaultValue %>,<% } %>
}, },
}; };
<% 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();
},
};
<% } %>

View File

@ -1,5 +1,6 @@
export interface ComponentStoryGeneratorOptions { export interface ComponentStoryGeneratorOptions {
projectPath: string; projectPath: string;
interactionTests?: boolean;
componentName: string; componentName: string;
componentPath: string; componentPath: string;
componentFileName: string; componentFileName: string;

View File

@ -29,6 +29,13 @@
"examples": ["awesome.component"], "examples": ["awesome.component"],
"x-priority": "important" "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": { "skipFormat": {
"description": "Skip formatting files.", "description": "Skip formatting files.",
"type": "boolean", "type": "boolean",

View File

@ -1,37 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`angularStories generator: applications should generate stories file for inline scam component 1`] = ` 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'; import { MyScamComponent } from './my-scam.component';
export default { import { within } from '@storybook/testing-library';
title: 'MyScamComponent', import { expect } from '@storybook/jest';
component: MyScamComponent,
} as Meta<MyScamComponent>;
export const Primary = { const meta: Meta<MyScamComponent> = {
render: (args: MyScamComponent) => ({ component: MyScamComponent,
props: args, title: 'MyScamComponent',
}), };
export default meta;
type Story = StoryObj<MyScamComponent>;
export const Primary: Story = {
args: {}, 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';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<AppComponent> = {
component: AppComponent,
title: 'AppComponent',
};
export default meta;
type Story = StoryObj<AppComponent>;
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`] = ` 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'; import { ComponentBComponent } from './component-b.component';
export default { import { within } from '@storybook/testing-library';
title: 'ComponentBComponent', import { expect } from '@storybook/jest';
component: ComponentBComponent,
} as Meta<ComponentBComponent>;
export const Primary = { const meta: Meta<ComponentBComponent> = {
render: (args: ComponentBComponent) => ({ component: ComponentBComponent,
props: args, title: 'ComponentBComponent',
}), };
export default meta;
type Story = StoryObj<ComponentBComponent>;
export const Primary: Story = {
args: {}, args: {},
}; };
export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText(/component-b works!/gi)).toBeTruthy();
},
};
" "
`; `;

View File

@ -1,68 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` 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'; import { StandaloneComponent } from './standalone.component';
export default { import { within } from '@storybook/testing-library';
title: 'StandaloneComponent', import { expect } from '@storybook/jest';
component: StandaloneComponent,
} as Meta<StandaloneComponent>;
export const Primary = { const meta: Meta<StandaloneComponent> = {
render: (args: StandaloneComponent) => ({ component: StandaloneComponent,
props: args, title: 'StandaloneComponent',
}), };
export default meta;
type Story = StoryObj<StandaloneComponent>;
export const Primary: Story = {
args: {}, 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`] = ` 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'; import { SecondaryStandaloneComponent } from './secondary-standalone.component';
export default { import { within } from '@storybook/testing-library';
title: 'SecondaryStandaloneComponent', import { expect } from '@storybook/jest';
component: SecondaryStandaloneComponent,
} as Meta<SecondaryStandaloneComponent>;
export const Primary = { const meta: Meta<SecondaryStandaloneComponent> = {
render: (args: SecondaryStandaloneComponent) => ({ component: SecondaryStandaloneComponent,
props: args, title: 'SecondaryStandaloneComponent',
}), };
export default meta;
type Story = StoryObj<SecondaryStandaloneComponent>;
export const Primary: Story = {
args: {}, 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`] = ` 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'; import { TestButtonComponent } from './test-button.component';
export default { import { within } from '@storybook/testing-library';
title: 'TestButtonComponent', import { expect } from '@storybook/jest';
component: TestButtonComponent,
} as Meta<TestButtonComponent>;
export const Primary = { const meta: Meta<TestButtonComponent> = {
render: (args: TestButtonComponent) => ({ component: TestButtonComponent,
props: args, title: 'TestButtonComponent',
}), };
export default meta;
type Story = StoryObj<TestButtonComponent>;
export const Primary: Story = {
args: { args: {
buttonType: 'button', buttonType: 'button',
style: 'default', style: 'default',
@ -70,22 +78,37 @@ export const Primary = {
isOn: false, 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`] = ` 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'; import { TestButtonComponent } from './test-button.component';
export default { import { within } from '@storybook/testing-library';
title: 'TestButtonComponent', import { expect } from '@storybook/jest';
component: TestButtonComponent,
} as Meta<TestButtonComponent>;
export const Primary = { const meta: Meta<TestButtonComponent> = {
render: (args: TestButtonComponent) => ({ component: TestButtonComponent,
props: args, title: 'TestButtonComponent',
}), };
export default meta;
type Story = StoryObj<TestButtonComponent>;
export const Primary: Story = {
args: { args: {
buttonType: 'button', buttonType: 'button',
style: 'default', style: 'default',
@ -93,5 +116,18 @@ export const Primary = {
isOn: false, 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();
},
};
" "
`; `;

View File

@ -1,7 +1,14 @@
export interface StoriesGeneratorOptions { export interface StoriesGeneratorOptions {
name: string; name: string;
cypressProject?: string; interactionTests?: boolean;
generateCypressSpecs?: boolean;
skipFormat?: boolean; skipFormat?: boolean;
ignorePaths?: string[]; ignorePaths?: string[];
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
cypressProject?: string;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
generateCypressSpecs?: boolean;
} }

View File

@ -18,15 +18,22 @@
"x-dropdown": "projects", "x-dropdown": "projects",
"x-priority": "important" "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": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to automatically generate `*.spec.ts` files in the Cypress e2e app generated by the `cypress-configure` generator.", "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-deprecated": "Use interactionTests instead. This option will be removed in v18."
"x-priority": "important"
}, },
"cypressProject": { "cypressProject": {
"type": "string", "type": "string",
"description": "The Cypress project to generate the stories under. This is inferred from `name` by default." "description": "The Cypress project to generate the stories under. This is inferred from `name` by default.",
"x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"skipFormat": { "skipFormat": {
"description": "Skip formatting files.", "description": "Skip formatting files.",

View File

@ -10,6 +10,8 @@ import { angularStoriesGenerator } from './stories';
// which is v9 while we are testing for the new v10 version // which is v9 while we are testing for the new v10 version
jest.mock('@nx/cypress/src/utils/cypress-version'); jest.mock('@nx/cypress/src/utils/cypress-version');
// TODO(v18): remove Cypress
describe('angularStories generator: applications', () => { describe('angularStories generator: applications', () => {
let tree: Tree; let tree: Tree;
const appName = 'test-app'; 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 }); await angularStoriesGenerator(tree, { name: appName });
expect( expect(
tree.exists(`apps/${appName}/src/app/app.component.stories.ts`) tree.read(`apps/${appName}/src/app/app.component.stories.ts`, 'utf-8')
).toBeTruthy(); ).toMatchSnapshot();
}); });
it('should generate stories file for scam component', async () => { it('should generate stories file for scam component', async () => {
@ -90,11 +92,10 @@ describe('angularStories generator: applications', () => {
}); });
expect( expect(
tree tree.read(
.read( `apps/${appName}/src/app/component-a/component-b/component-b.component.stories.ts`,
`apps/${appName}/src/app/component-a/component-b/component-b.component.stories.ts` 'utf-8'
) )
.toString()
).toMatchSnapshot(); ).toMatchSnapshot();
expect( expect(
tree.exists( tree.exists(
@ -113,20 +114,10 @@ describe('angularStories generator: applications', () => {
await angularStoriesGenerator(tree, { name: appName }); await angularStoriesGenerator(tree, { name: appName });
expect( expect(
tree tree.read(
.read(`apps/${appName}/src/app/my-scam/my-scam.component.stories.ts`) `apps/${appName}/src/app/my-scam/my-scam.component.stories.ts`,
.toString() 'utf-8'
)
).toMatchSnapshot(); ).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();
});
}); });

View File

@ -2,7 +2,6 @@ import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { Tree } from '@nx/devkit'; import { Tree } from '@nx/devkit';
import { writeJson } from '@nx/devkit'; import { writeJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { componentGenerator } from '../component/component'; import { componentGenerator } from '../component/component';
import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point'; import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point';
import { scamGenerator } from '../scam/scam'; 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 // 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 // which is v9 while we are testing for the new v10 version
jest.mock('@nx/cypress/src/utils/cypress-version'); jest.mock('@nx/cypress/src/utils/cypress-version');
// TODO(v18): remove Cypress
describe('angularStories generator: libraries', () => { describe('angularStories generator: libraries', () => {
const libName = 'test-ui-lib'; const libName = 'test-ui-lib';
@ -39,7 +39,6 @@ describe('angularStories generator: libraries', () => {
async () => async () =>
await angularStoriesGenerator(tree, { await angularStoriesGenerator(tree, {
name: libName, name: libName,
generateCypressSpecs: false,
}) })
).not.toThrow(); ).not.toThrow();
}); });
@ -47,13 +46,9 @@ describe('angularStories generator: libraries', () => {
describe('Stories for non-empty Angular library', () => { describe('Stories for non-empty Angular library', () => {
let tree: Tree; let tree: Tree;
let cypressProjectGenerator;
beforeEach(async () => { beforeEach(async () => {
tree = await createStorybookTestWorkspaceForLib(libName); tree = await createStorybookTestWorkspaceForLib(libName);
cypressProjectGenerator = await (
await import('@nx/storybook')
).cypressProjectGenerator;
}); });
it('should generate stories.ts files', async () => { it('should generate stories.ts files', async () => {
@ -105,56 +100,11 @@ describe('angularStories generator: libraries', () => {
).toBeTruthy(); ).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 () => { it('should run twice without errors', async () => {
await cypressProjectGenerator(tree, {
linter: Linter.EsLint,
name: libName,
});
try { try {
await angularStoriesGenerator(tree, { name: libName }); await angularStoriesGenerator(tree, { name: libName });
await angularStoriesGenerator(tree, { await angularStoriesGenerator(tree, {
name: libName, name: libName,
generateCypressSpecs: true,
}); });
} catch { } catch {
fail('Should not fail when running it twice.'); 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 () => { it('should handle modules with variable declarations rather than literals', async () => {
await cypressProjectGenerator(tree, {
linter: Linter.EsLint,
name: libName,
});
await angularStoriesGenerator(tree, { await angularStoriesGenerator(tree, {
name: libName, name: libName,
generateCypressSpecs: true,
}); });
expect( expect(
@ -182,27 +126,11 @@ describe('angularStories generator: libraries', () => {
`libs/${libName}/src/lib/variable-declare/variable-declare-view/variable-declare-view.component.stories.ts` `libs/${libName}/src/lib/variable-declare/variable-declare-view/variable-declare-view.component.stories.ts`
) )
).toBeTruthy(); ).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 () => { 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, { await angularStoriesGenerator(tree, {
name: libName, name: libName,
generateCypressSpecs: true,
}); });
expect( 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` `libs/${libName}/src/lib/variable-spread-declare/variable-spread-declare-view/variable-spread-declare-view.component.stories.ts`
) )
).toBeTruthy(); ).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 () => { it('should handle modules using static members for declarations rather than literals', async () => {
await cypressProjectGenerator(tree, {
linter: Linter.EsLint,
name: libName,
});
await angularStoriesGenerator(tree, { await angularStoriesGenerator(tree, {
name: libName, name: libName,
generateCypressSpecs: true,
}); });
expect( expect(
@ -259,12 +165,6 @@ describe('angularStories generator: libraries', () => {
`libs/${libName}/src/lib/static-member-declarations/cmp2/cmp2.component.stories.ts` `libs/${libName}/src/lib/static-member-declarations/cmp2/cmp2.component.stories.ts`
) )
).toBeTruthy(); ).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 () => { it('should generate stories file for scam component', async () => {

View File

@ -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 componentCypressSpecGenerator from '../component-cypress-spec/component-cypress-spec';
import componentStoryGenerator from '../component-story/component-story'; import componentStoryGenerator from '../component-story/component-story';
import type { ComponentInfo } from '../utils/storybook-ast/component-info'; 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 { getModuleFilePaths } from '../utils/storybook-ast/module-info';
import type { StoriesGeneratorOptions } from './schema'; import type { StoriesGeneratorOptions } from './schema';
import minimatch = require('minimatch'); import minimatch = require('minimatch');
import { nxVersion } from '../../utils/versions';
export async function angularStoriesGenerator( export async function angularStoriesGenerator(
tree: Tree, tree: Tree,
options: StoriesGeneratorOptions options: StoriesGeneratorOptions
): Promise<void> { ): Promise<GeneratorCallback> {
const e2eProjectName = options.cypressProject ?? `${options.name}-e2e`; const e2eProjectName = options.cypressProject ?? `${options.name}-e2e`;
const e2eProject = getE2EProject(tree, e2eProjectName); const e2eProject = getE2EProject(tree, e2eProjectName);
const entryPoints = getProjectEntryPoints(tree, options.name); const entryPoints = getProjectEntryPoints(tree, options.name);
@ -59,6 +70,7 @@ export async function angularStoriesGenerator(
componentName: info.name, componentName: info.name,
componentPath: info.path, componentPath: info.path,
componentFileName: info.componentFileName, componentFileName: info.componentFileName,
interactionTests: options.interactionTests ?? true,
skipFormat: true, skipFormat: true,
}); });
@ -75,10 +87,24 @@ export async function angularStoriesGenerator(
}); });
} }
} }
const tasks: GeneratorCallback[] = [];
if (options.interactionTests) {
const { interactionTestsDependencies, addInteractionsInAddons } =
ensurePackage<typeof import('@nx/storybook')>('@nx/storybook', nxVersion);
const projectConfiguration = readProjectConfiguration(tree, options.name);
addInteractionsInAddons(tree, projectConfiguration);
tasks.push(
addDependenciesToPackageJson(tree, {}, interactionTestsDependencies())
);
}
if (!options.skipFormat) { if (!options.skipFormat) {
await formatFiles(tree); await formatFiles(tree);
} }
return runTasksInSerial(...tasks);
} }
export default angularStoriesGenerator; export default angularStoriesGenerator;

View File

@ -1,9 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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<TestButtonComponent> = {
component: TestButtonComponent,
title: 'TestButtonComponent',
};
export default meta;
type Story = StoryObj<TestButtonComponent>;
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<TestOtherComponent> = {
component: TestOtherComponent,
title: 'TestOtherComponent',
};
export default meta;
type Story = StoryObj<TestOtherComponent>;
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`] = ` 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)'], stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials'], addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: { framework: {
name: '@storybook/angular', name: '@storybook/angular',
options: {}, options: {},
@ -26,33 +94,12 @@ exports[`StorybookConfiguration generator should generate in the correct folder
".prettierignore", ".prettierignore",
".prettierrc", ".prettierrc",
"apps/.gitignore", "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.config.ts",
"jest.preset.js", "jest.preset.js",
"libs/.gitignore", "libs/.gitignore",
"libs/test-ui-lib/.eslintrc.json", "libs/test-ui-lib/.eslintrc.json",
"libs/test-ui-lib/.storybook/main.js", "libs/test-ui-lib/.storybook/main.ts",
"libs/test-ui-lib/.storybook/preview.js", "libs/test-ui-lib/.storybook/preview.ts",
"libs/test-ui-lib/.storybook/tsconfig.json", "libs/test-ui-lib/.storybook/tsconfig.json",
"libs/test-ui-lib/jest.config.ts", "libs/test-ui-lib/jest.config.ts",
"libs/test-ui-lib/package.json", "libs/test-ui-lib/package.json",
@ -158,33 +205,12 @@ exports[`StorybookConfiguration generator should generate the right files 1`] =
".prettierignore", ".prettierignore",
".prettierrc", ".prettierrc",
"apps/.gitignore", "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.config.ts",
"jest.preset.js", "jest.preset.js",
"libs/.gitignore", "libs/.gitignore",
"libs/test-ui-lib/.eslintrc.json", "libs/test-ui-lib/.eslintrc.json",
"libs/test-ui-lib/.storybook/main.js", "libs/test-ui-lib/.storybook/main.ts",
"libs/test-ui-lib/.storybook/preview.js", "libs/test-ui-lib/.storybook/preview.ts",
"libs/test-ui-lib/.storybook/tsconfig.json", "libs/test-ui-lib/.storybook/tsconfig.json",
"libs/test-ui-lib/jest.config.ts", "libs/test-ui-lib/jest.config.ts",
"libs/test-ui-lib/package.json", "libs/test-ui-lib/package.json",

View File

@ -21,6 +21,7 @@ export async function generateStories(
options.configureCypress && options.generateCypressSpecs, options.configureCypress && options.generateCypressSpecs,
cypressProject: e2eProjectName, cypressProject: e2eProjectName,
ignorePaths: options.ignorePaths, ignorePaths: options.ignorePaths,
interactionTests: options.interactionTests,
skipFormat: true, skipFormat: true,
}); });
} }

View File

@ -1,15 +1,24 @@
import type { Linter } from '@nx/linter'; import type { Linter } from '@nx/linter';
export interface StorybookConfigurationOptions { export interface StorybookConfigurationOptions {
configureCypress: boolean;
configureStaticServe?: boolean; configureStaticServe?: boolean;
generateCypressSpecs: boolean;
generateStories: boolean; generateStories: boolean;
linter: Linter; linter: Linter;
name: string; name: string;
cypressDirectory?: string;
tsConfiguration?: boolean; tsConfiguration?: boolean;
skipFormat?: boolean; skipFormat?: boolean;
ignorePaths?: string[]; ignorePaths?: string[];
interactionTests?: boolean; interactionTests?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
configureCypress?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
generateCypressSpecs?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
cypressDirectory?: string;
} }

View File

@ -29,7 +29,7 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to configure Cypress or not.", "description": "Specifies whether to configure Cypress or not.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"generateStories": { "generateStories": {
"type": "boolean", "type": "boolean",
@ -41,7 +41,7 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.", "description": "Specifies whether to automatically generate test files in the generated Cypress e2e app.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"configureStaticServe": { "configureStaticServe": {
"type": "boolean", "type": "boolean",
@ -53,7 +53,7 @@
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Placed at the root by default.", "description": "A directory where the Cypress project will be placed. Placed at the root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",

View File

@ -44,31 +44,16 @@ describe('StorybookConfiguration generator', () => {
jest.resetModules(); jest.resetModules();
}); });
it('should throw when generateCypressSpecs is true and generateStories is false', async () => {
await expect(
storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{
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 () => { it('should only configure storybook', async () => {
await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{ await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{
name: libName, name: libName,
configureCypress: false,
generateCypressSpecs: false,
generateStories: 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( expect(
tree.exists('libs/test-ui-lib/.storybook/tsconfig.json') tree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
).toBeTruthy(); ).toBeTruthy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeFalsy();
expect( expect(
tree.exists( tree.exists(
'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts' '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' 'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts'
) )
).toBeFalsy(); ).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 () => { it('should configure storybook to use webpack 5', async () => {
await storybookConfigurationGenerator(tree, { await storybookConfigurationGenerator(tree, {
name: libName, name: libName,
configureCypress: false,
generateCypressSpecs: false,
generateStories: false, generateStories: false,
linter: Linter.None, linter: Linter.None,
}); });
expect( expect(
tree.read('libs/test-ui-lib/.storybook/main.js', 'utf-8') tree.read('libs/test-ui-lib/.storybook/main.ts', 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should configure everything at once', async () => { it('should configure everything at once - and interaction tests too', async () => {
await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{ await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{
name: libName, name: libName,
configureCypress: true,
generateCypressSpecs: true,
generateStories: 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( expect(
tree.exists('libs/test-ui-lib/.storybook/tsconfig.json') tree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
).toBeTruthy(); ).toBeTruthy();
expect(tree.exists('apps/test-ui-lib-e2e/cypress.config.ts')).toBeTruthy();
expect( expect(
tree.exists( tree.read(
'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts' 'libs/test-ui-lib/src/lib/test-button/test-button.component.stories.ts',
'utf-8'
) )
).toBeTruthy(); ).toMatchSnapshot();
expect( expect(
tree.exists( tree.read(
'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts' 'libs/test-ui-lib/src/lib/test-other/test-other.component.stories.ts',
'utf-8'
) )
).toBeTruthy(); ).toMatchSnapshot();
expect(
tree.exists(
'apps/test-ui-lib-e2e/src/e2e/test-button/test-button.component.cy.ts'
)
).toBeTruthy();
expect(
tree.exists(
'apps/test-ui-lib-e2e/src/e2e/test-other/test-other.component.cy.ts'
)
).toBeTruthy();
}); });
it('should generate the right files', async () => { it('should generate the right files', async () => {
@ -171,8 +133,6 @@ describe('StorybookConfiguration generator', () => {
await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{ await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{
name: libName, name: libName,
configureCypress: true,
generateCypressSpecs: true,
generateStories: true, generateStories: true,
}); });
@ -210,10 +170,7 @@ describe('StorybookConfiguration generator', () => {
await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{ await storybookConfigurationGenerator(tree, <StorybookConfigurationOptions>{
name: libName, name: libName,
configureCypress: true,
generateCypressSpecs: true,
generateStories: true, generateStories: true,
cypressDirectory: 'one/two',
}); });
expect(listFiles(tree)).toMatchSnapshot(); expect(listFiles(tree)).toMatchSnapshot();

View File

@ -5,6 +5,7 @@ import { generateStorybookConfiguration } from './lib/generate-storybook-configu
import { validateOptions } from './lib/validate-options'; import { validateOptions } from './lib/validate-options';
import type { StorybookConfigurationOptions } from './schema'; import type { StorybookConfigurationOptions } from './schema';
// TODO(v18): remove Cypress
export async function storybookConfigurationGenerator( export async function storybookConfigurationGenerator(
tree: Tree, tree: Tree,
options: StorybookConfigurationOptions options: StorybookConfigurationOptions
@ -14,11 +15,19 @@ export async function storybookConfigurationGenerator(
const storybookGeneratorInstallTask = await generateStorybookConfiguration( const storybookGeneratorInstallTask = await generateStorybookConfiguration(
tree, tree,
options {
...options,
interactionTests: options.interactionTests ?? true, // default is true
tsConfiguration: options.tsConfiguration ?? true, // default is true
}
); );
if (options.generateStories) { if (options.generateStories) {
await generateStories(tree, { ...options, skipFormat: true }); await generateStories(tree, {
...options,
interactionTests: options.interactionTests ?? true,
skipFormat: true,
});
} }
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -6,11 +6,11 @@ import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
export type KnobType = 'text' | 'boolean' | 'number' | 'select'; export type ArgType = 'text' | 'boolean' | 'number' | 'select';
export interface InputDescriptor { export interface InputDescriptor {
name: string; name: string;
type: KnobType; type: ArgType;
defaultValue?: string; defaultValue?: string;
} }
@ -62,7 +62,7 @@ export function getComponentProps(
: node.name.getText() : node.name.getText()
: node.name.getText(); : node.name.getText();
const type = getKnobType(node); const type = getArgType(node);
const defaultValue = getArgsDefaultValueFn(node); const defaultValue = getArgsDefaultValueFn(node);
return { return {
@ -76,27 +76,27 @@ export function getComponentProps(
return props; return props;
} }
export function getKnobType(property: PropertyDeclaration): KnobType { export function getArgType(property: PropertyDeclaration): ArgType {
if (!tsModule) { if (!tsModule) {
tsModule = ensureTypescript(); tsModule = ensureTypescript();
} }
if (property.type) { if (property.type) {
const typeName = property.type.getText(); const typeName = property.type.getText();
const typeNameToKnobType: Record<string, KnobType> = { const typeNameToArgType: Record<string, ArgType> = {
string: 'text', string: 'text',
number: 'number', number: 'number',
boolean: 'boolean', boolean: 'boolean',
}; };
return typeNameToKnobType[typeName] || 'text'; return typeNameToArgType[typeName] || 'text';
} }
if (property.initializer) { if (property.initializer) {
const initializerKindToKnobType: Record<number, KnobType> = { const initializerKindToArgType: Record<number, ArgType> = {
[tsModule.SyntaxKind.StringLiteral]: 'text', [tsModule.SyntaxKind.StringLiteral]: 'text',
[tsModule.SyntaxKind.NumericLiteral]: 'number', [tsModule.SyntaxKind.NumericLiteral]: 'number',
[tsModule.SyntaxKind.TrueKeyword]: 'boolean', [tsModule.SyntaxKind.TrueKeyword]: 'boolean',
[tsModule.SyntaxKind.FalseKeyword]: 'boolean', [tsModule.SyntaxKind.FalseKeyword]: 'boolean',
}; };
return initializerKindToKnobType[property.initializer.kind] || 'text'; return initializerKindToArgType[property.initializer.kind] || 'text';
} }
return 'text'; return 'text';
} }

View File

@ -22,6 +22,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -52,6 +56,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -82,6 +90,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -112,6 +124,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -142,6 +158,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -172,6 +192,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -202,6 +226,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -232,6 +260,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -262,6 +294,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -292,6 +328,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -322,6 +362,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -352,6 +396,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -382,6 +430,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -412,6 +464,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -442,6 +498,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -472,6 +532,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -502,6 +566,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -532,6 +600,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -559,6 +631,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to One!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to One!/gi)).toBeTruthy();
@ -586,6 +659,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Two!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Two!/gi)).toBeTruthy();
@ -615,6 +689,9 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Three!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Three!/gi)).toBeTruthy();
@ -648,6 +725,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -678,6 +759,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -705,6 +790,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
@ -732,6 +818,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy();

View File

@ -25,6 +25,9 @@ export const Primary = {
<% if ( interactionTests ) { %> <% if ( interactionTests ) { %>
export const Heading: Story = { export const Heading: Story = {
args: {<% for (let prop of props) { %>
<%= prop.name %>: <%- prop.defaultValue %>,<% } %>
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy();

View File

@ -19,6 +19,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy();
@ -49,6 +50,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();

View File

@ -19,6 +19,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to TestUiLib!/gi)).toBeTruthy();
@ -49,6 +50,10 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {
name: '',
displayAge: false,
},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();

View File

@ -20,12 +20,12 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.", "description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"cypressProject": { "cypressProject": {
"type": "string", "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." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"interactionTests": { "interactionTests": {
"type": "boolean", "type": "boolean",

View File

@ -5,18 +5,23 @@ import {
getComponentNode, getComponentNode,
} from '../../utils/ast-utils'; } from '../../utils/ast-utils';
import { import {
addDependenciesToPackageJson,
convertNxGenerator, convertNxGenerator,
ensurePackage,
formatFiles, formatFiles,
GeneratorCallback,
getProjects, getProjects,
joinPathFragments, joinPathFragments,
logger, logger,
ProjectConfiguration, ProjectConfiguration,
runTasksInSerial,
Tree, Tree,
visitNotIgnoredFiles, visitNotIgnoredFiles,
} from '@nx/devkit'; } from '@nx/devkit';
import { basename, join } from 'path'; import { basename, join } from 'path';
import minimatch = require('minimatch'); import minimatch = require('minimatch');
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { nxVersion } from '../../utils/versions';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
@ -24,10 +29,16 @@ export interface StorybookStoriesSchema {
project: string; project: string;
interactionTests?: boolean; interactionTests?: boolean;
js?: boolean; js?: boolean;
cypressProject?: string;
generateCypressSpecs?: boolean;
ignorePaths?: string[]; ignorePaths?: string[];
skipFormat?: boolean; skipFormat?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
cypressProject?: string;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
generateCypressSpecs?: boolean;
} }
export async function projectRootPath( export async function projectRootPath(
@ -87,13 +98,14 @@ export async function createAllStories(
projectName: string, projectName: string,
interactionTests: boolean, interactionTests: boolean,
js: boolean, js: boolean,
projects: Map<string, ProjectConfiguration>,
projectConfiguration: ProjectConfiguration,
generateCypressSpecs?: boolean, generateCypressSpecs?: boolean,
cypressProject?: string, cypressProject?: string,
ignorePaths?: string[] ignorePaths?: string[]
) { ) {
const { isTheFileAStory } = await import('@nx/storybook/src/utils/utilities'); const { isTheFileAStory } = await import('@nx/storybook/src/utils/utilities');
const projects = getProjects(tree);
const projectConfiguration = projects.get(projectName);
const { sourceRoot, root } = projectConfiguration; const { sourceRoot, root } = projectConfiguration;
let componentPaths: string[] = []; let componentPaths: string[] = [];
@ -164,19 +176,35 @@ export async function storiesGenerator(
host: Tree, host: Tree,
schema: StorybookStoriesSchema schema: StorybookStoriesSchema
) { ) {
const projects = getProjects(host);
const projectConfiguration = projects.get(schema.project);
await createAllStories( await createAllStories(
host, host,
schema.project, schema.project,
schema.interactionTests ?? true, schema.interactionTests ?? true,
schema.js, schema.js,
projects,
projectConfiguration,
schema.generateCypressSpecs, schema.generateCypressSpecs,
schema.cypressProject, schema.cypressProject,
schema.ignorePaths schema.ignorePaths
); );
const tasks: GeneratorCallback[] = [];
if (schema.interactionTests) {
const { interactionTestsDependencies, addInteractionsInAddons } =
ensurePackage<typeof import('@nx/storybook')>('@nx/storybook', nxVersion);
tasks.push(
addDependenciesToPackageJson(host, {}, interactionTestsDependencies())
);
addInteractionsInAddons(host, projectConfiguration);
}
if (!schema.skipFormat) { if (!schema.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }
return runTasksInSerial(...tasks);
} }
export default storiesGenerator; export default storiesGenerator;

View File

@ -43,6 +43,7 @@ export const Primary = {
}; };
export const Heading: Story = { export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(canvas.getByText(/Welcome to MyComponent!/gi)).toBeTruthy(); expect(canvas.getByText(/Welcome to MyComponent!/gi)).toBeTruthy();

View File

@ -1,4 +1,4 @@
// TODO(katerina): remove Cypress for Nx 18 // TODO(v18): remove Cypress
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { logger, Tree } from '@nx/devkit'; import { logger, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -10,7 +10,7 @@ import {
import { nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
async function generateStories(host: Tree, schema: StorybookConfigureSchema) { async function generateStories(host: Tree, schema: StorybookConfigureSchema) {
// TODO(katerina): remove Cypress for Nx 18 // TODO(v18): remove Cypress
ensurePackage('@nx/cypress', nxVersion); ensurePackage('@nx/cypress', nxVersion);
const { getE2eProjectName } = await import( const { getE2eProjectName } = await import(
'@nx/cypress/src/utils/project-name' '@nx/cypress/src/utils/project-name'

View File

@ -4,12 +4,21 @@ export interface StorybookConfigureSchema {
name: string; name: string;
interactionTests?: boolean; interactionTests?: boolean;
generateStories?: boolean; generateStories?: boolean;
configureCypress?: boolean;
generateCypressSpecs?: boolean;
js?: boolean; js?: boolean;
tsConfiguration?: boolean; tsConfiguration?: boolean;
linter?: Linter; linter?: Linter;
cypressDirectory?: string;
ignorePaths?: string[]; ignorePaths?: string[];
configureStaticServe?: boolean; configureStaticServe?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
configureCypress?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
generateCypressSpecs?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
cypressDirectory?: string;
} }

View File

@ -29,7 +29,7 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Run the cypress-configure generator.", "description": "Run the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"generateStories": { "generateStories": {
"type": "boolean", "type": "boolean",
@ -41,7 +41,7 @@
"generateCypressSpecs": { "generateCypressSpecs": {
"type": "boolean", "type": "boolean",
"description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"configureStaticServe": { "configureStaticServe": {
"type": "boolean", "type": "boolean",
@ -53,7 +53,7 @@
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Placed at the root by default.", "description": "A directory where the Cypress project will be placed. Placed at the root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"js": { "js": {
"type": "boolean", "type": "boolean",

View File

@ -26,7 +26,18 @@ export function getArgsDefaultValue(property: ts.SyntaxKind): string {
export function getDefaultsForComponent( export function getDefaultsForComponent(
sourceFile: ts.SourceFile, sourceFile: ts.SourceFile,
cmpDeclaration: ts.Node cmpDeclaration: ts.Node
) { ): {
propsTypeName: string;
props: {
name: string;
defaultValue: any;
}[];
argTypes: {
name: string;
type: string;
actionText: string;
}[];
} {
if (!tsModule) { if (!tsModule) {
tsModule = ensureTypescript(); tsModule = ensureTypescript();
} }

View File

@ -1,3 +1,7 @@
export { configurationGenerator } from './src/generators/configuration/configuration'; export { configurationGenerator } from './src/generators/configuration/configuration';
export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project'; export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project';
export { storybookVersion } from './src/utils/versions'; export { storybookVersion } from './src/utils/versions';
export {
interactionTestsDependencies,
addInteractionsInAddons,
} from './src/generators/configuration/lib/interaction-testing.utils';

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TS config for project if tsConfiguration true 1`] = ` exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TS config for project by default 1`] = `
"import type { StorybookConfig } from '@storybook/angular'; "import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = { const config: StorybookConfig = {
@ -20,7 +20,7 @@ export default config;
" "
`; `;
exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files 1`] = ` exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files by default 1`] = `
{ {
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "test-ui-lib", "name": "test-ui-lib",
@ -97,9 +97,9 @@ exports[`@nx/storybook:configuration for Storybook v7 basic functionalities shou
} }
`; `;
exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files 2`] = `null`; exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files by default 2`] = `null`;
exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files 3`] = ` exports[`@nx/storybook:configuration for Storybook v7 basic functionalities should generate TypeScript Configuration files by default 3`] = `
"import type { StorybookConfig } from '@storybook/angular'; "import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = { const config: StorybookConfig = {

View File

@ -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, { await configurationGenerator(tree, {
name: 'web', name: 'web',
uiFramework: '@storybook/react-webpack5', uiFramework: '@storybook/react-webpack5',

View File

@ -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, { await configurationGenerator(tree, {
name: 'test-ui-lib', name: 'test-ui-lib',
standaloneConfig: false, standaloneConfig: false,
tsConfiguration: true,
uiFramework: '@storybook/angular', uiFramework: '@storybook/angular',
}); });
const project = readProjectConfiguration(tree, 'test-ui-lib'); const project = readProjectConfiguration(tree, 'test-ui-lib');
@ -172,11 +171,10 @@ describe('@nx/storybook:configuration for Storybook v7', () => {
).toBeTruthy(); ).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, { await configurationGenerator(tree, {
name: 'test-ui-lib', name: 'test-ui-lib',
standaloneConfig: false, standaloneConfig: false,
tsConfiguration: true,
uiFramework: '@storybook/angular', uiFramework: '@storybook/angular',
}); });
@ -278,7 +276,6 @@ describe('@nx/storybook:configuration for Storybook v7', () => {
}); });
await configurationGenerator(tree, { await configurationGenerator(tree, {
name: 'main-vite-ts', name: 'main-vite-ts',
tsConfiguration: true,
uiFramework: '@storybook/react-vite', uiFramework: '@storybook/react-vite',
}); });
await configurationGenerator(tree, { await configurationGenerator(tree, {

View File

@ -37,12 +37,10 @@ import {
import { import {
coreJsVersion, coreJsVersion,
nxVersion, nxVersion,
storybookJestVersion,
storybookTestingLibraryVersion,
storybookTestRunnerVersion,
storybookVersion, storybookVersion,
tsNodeVersion, tsNodeVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { interactionTestsDependencies } from './lib/interaction-testing.utils';
export async function configurationGenerator( export async function configurationGenerator(
tree: Tree, tree: Tree,
@ -151,7 +149,7 @@ export async function configurationGenerator(
addStaticTarget(tree, schema); addStaticTarget(tree, schema);
} }
// TODO(katerina): remove Cypress for Nx 18 // TODO(v18): remove Cypress
if (schema.configureCypress) { if (schema.configureCypress) {
const e2eProject = await getE2EProjectName(tree, schema.name); const e2eProject = await getE2EProjectName(tree, schema.name);
if (!e2eProject) { if (!e2eProject) {
@ -174,17 +172,17 @@ export async function configurationGenerator(
} }
} }
const devDeps = {}; let devDeps = {};
if (schema.tsConfiguration) { if (schema.tsConfiguration) {
devDeps['ts-node'] = tsNodeVersion; devDeps['ts-node'] = tsNodeVersion;
} }
if (schema.interactionTests) { if (schema.interactionTests) {
devDeps['@storybook/test-runner'] = storybookTestRunnerVersion; devDeps = {
devDeps['@storybook/addon-interactions'] = storybookVersion; ...devDeps,
devDeps['@storybook/testing-library'] = storybookTestingLibraryVersion; ...interactionTestsDependencies(),
devDeps['@storybook/jest'] = storybookJestVersion; };
} }
if (schema.configureStaticServe) { if (schema.configureStaticServe) {

View File

@ -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;"
`;

View File

@ -0,0 +1,70 @@
import { ProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { addInteractionsInAddons } from './interaction-testing.utils';
describe('Helper functions for the Storybook 7 migration generator', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add addon-interactions in main.ts if it does not exist', () => {
tree.write(
`.storybook/main.ts`,
`import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/angular',
options: {},
},
};
export default config;`
);
addInteractionsInAddons(tree, {
name: 'my-proj',
targets: {
storybook: {
executor: '@nx/storybook:storybook',
options: {
configDir: `.storybook`,
},
},
},
} as unknown as ProjectConfiguration);
expect(tree.read(`.storybook/main.ts`, 'utf-8')).toMatchSnapshot();
});
it('should do nothing if addon-interactions already exists in main.ts', () => {
tree.write(
`.storybook/main.ts`,
`import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/angular',
options: {},
},
};
export default config;`
);
addInteractionsInAddons(tree, {
name: 'my-proj',
targets: {
storybook: {
executor: '@nx/storybook:storybook',
options: {
configDir: `.storybook`,
},
},
},
} as unknown as ProjectConfiguration);
expect(tree.read(`.storybook/main.ts`, 'utf-8')).toMatchSnapshot();
});
});

View File

@ -0,0 +1,88 @@
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) return;
let mainJsTs = tree.read(mainJsTsPath, 'utf-8');
if (!mainJsTs) return;
// Find addons array
const addonsArray = tsquery.query(
mainJsTs,
`PropertyAssignment:has(Identifier:has([name="addons"]))`
)?.[0];
// if there's no addons array don't do anything
// they may be setting up storybook in another project
if (!addonsArray) return;
// Check if addons array already has addon-interactions
const addonsArrayHasAddonInteractions = tsquery.query(
addonsArray,
`StringLiteral:has([text="@storybook/addon-interactions"])`
)?.[0];
if (addonsArrayHasAddonInteractions) return;
// get the array of the addons
const arrayLiteralExpression = tsquery.query(
addonsArray,
`ArrayLiteralExpression`
)?.[0];
if (!arrayLiteralExpression) return;
mainJsTs = applyChangesToString(mainJsTs, [
{
type: ChangeType.Insert,
index: arrayLiteralExpression.getStart() + 1,
text: `'@storybook/addon-interactions', `,
},
]);
tree.write(mainJsTsPath, mainJsTs);
}
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;
}

View File

@ -4,13 +4,19 @@ import { UiFramework7, UiFramework } from '../../utils/models';
export interface StorybookConfigureSchema { export interface StorybookConfigureSchema {
name: string; name: string;
uiFramework?: UiFramework7; uiFramework?: UiFramework7;
configureCypress?: boolean;
linter?: Linter; linter?: Linter;
js?: boolean; js?: boolean;
interactionTests?: boolean; interactionTests?: boolean;
tsConfiguration?: boolean; tsConfiguration?: boolean;
cypressDirectory?: string;
standaloneConfig?: boolean; standaloneConfig?: boolean;
configureStaticServe?: boolean; configureStaticServe?: boolean;
skipFormat?: boolean; skipFormat?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
configureCypress?: boolean;
/**
* @deprecated Use interactionTests instead. This option will be removed in v18.
*/
cypressDirectory?: string;
} }

View File

@ -28,12 +28,12 @@
"configureCypress": { "configureCypress": {
"type": "boolean", "type": "boolean",
"description": "Run the cypress-configure generator.", "description": "Run the cypress-configure generator.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"cypressDirectory": { "cypressDirectory": {
"type": "string", "type": "string",
"description": "A directory where the Cypress project will be placed. Added at root by default.", "description": "A directory where the Cypress project will be placed. Added at root by default.",
"x-deprecated": "Please use Storybook interaction tests instead." "x-deprecated": "Use interactionTests instead. This option will be removed in v18."
}, },
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",