nx/packages/react/src/generators/component-story/component-story.spec.ts

729 lines
22 KiB
TypeScript

import { getProjects, Tree, updateProjectConfiguration } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from '../library/library';
import componentStoryGenerator from './component-story';
import { Linter } from '@nrwl/linter';
import { formatFile } from '../../utils/format-file';
describe('react:component-story', () => {
let appTree: Tree;
let cmpPath = 'libs/test-ui-lib/src/lib/test-ui-lib.tsx';
let storyFilePath = 'libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx';
describe('default setup', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
});
describe('when file does not contain a component', () => {
beforeEach(() => {
appTree.write(
cmpPath,
`export const add = (a: number, b: number) => a + b;`
);
});
it('should fail with a descriptive error message', async () => {
try {
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
} catch (e) {
expect(e.message).toContain(
'Could not find any React component in file libs/test-ui-lib/src/lib/test-ui-lib.tsx'
);
}
});
});
describe('default component setup', () => {
beforeEach(async () => {
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should create the story file', () => {
expect(appTree.exists(storyFilePath)).toBeTruthy();
});
it('should properly set up the story', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { TestUiLib } from './test-ui-lib';
const Story: ComponentMeta<typeof TestUiLib> = {
component: TestUiLib,
title: 'TestUiLib'
};
export default Story;
const Template: ComponentStory<typeof TestUiLib> = (args) => <TestUiLib {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
});
});
describe('when using plain JS components', () => {
let storyFilePathPlain =
'libs/test-ui-lib/src/lib/test-ui-libplain.stories.jsx';
beforeEach(async () => {
appTree.write(
'libs/test-ui-lib/src/lib/test-ui-libplain.jsx',
`import React from 'react';
import './test.scss';
export const Test = () => {
return (
<div>
<h1>Welcome to test component</h1>
</div>
);
};
export default Test;
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-libplain.jsx',
project: 'test-ui-lib',
});
});
it('should create the story file', () => {
expect(appTree.exists(storyFilePathPlain)).toBeTruthy();
});
it('should properly set up the story', () => {
expect(formatFile`${appTree.read(storyFilePathPlain, 'utf-8')}`)
.toContain(formatFile`
import Test from './test-ui-libplain';
export default {
component: Test,
title: 'Test'
};
const Template = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
});
});
describe('component without any props defined', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export const Test = () => {
return (
<div>
<h1>Welcome to test component</h1>
</div>
);
};
export default Test;
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should create a story without controls', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Test } from './test-ui-lib';
const Story: ComponentMeta<typeof Test> = {
component: Test,
title: 'Test'
};
export default Story;
const Template: ComponentStory<typeof Test> = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
});
});
describe('component with props', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export interface TestProps {
name: string;
displayAge: boolean;
}
export const Test = (props: TestProps) => {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
export default Test;
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should setup controls based on the component props', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Test } from './test-ui-lib';
const Story: ComponentMeta<typeof Test> = {
component: Test,
title: 'Test'
};
export default Story;
const Template: ComponentStory<typeof Test> = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {
name: '',
displayAge: false,
};
`);
});
});
describe('component with props and actions', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export type ButtonStyle = 'default' | 'primary' | 'warning';
export interface TestProps {
name: string;
displayAge: boolean;
someAction: (e: unknown) => void;
style: ButtonStyle;
}
export const Test = (props: TestProps) => {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
<button onClick={props.someAction}>Click me!</button>
</div>
);
};
export default Test;
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should setup controls based on the component props', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Test } from './test-ui-lib';
const Story: ComponentMeta<typeof Test> = {
component: Test,
title: 'Test',
argTypes: {
someAction: { action: 'someAction executed!' },
}
};
export default Story;
const Template: ComponentStory<typeof Test> = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {
name: '',
displayAge: false,
};
`);
});
});
describe('Other types of component definitions', () => {
describe('Component files with DEFAULT export', () => {
const reactComponentDefinitions = [
{
name: 'default export function',
src: `export default function Test(props: TestProps) {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
`,
},
{
name: 'function and then export',
src: `
function Test(props: TestProps) {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
export default Test;
`,
},
{
name: 'arrow function',
src: `
const Test = (props: TestProps) => {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
export default Test;
`,
},
{
name: 'arrow function without {..}',
src: `
const Test = (props: TestProps) => <div><h1>Welcome to test component, {props.name}</h1></div>;
export default Test
`,
},
{
name: 'direct export of component class',
src: `
export default class Test extends React.Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'component class & then default export',
src: `
class Test extends React.Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
export default Test
`,
},
{
name: 'PureComponent class & then default export',
src: `
class Test extends React.PureComponent<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
export default Test
`,
},
{
name: 'direct export of component class new JSX transform',
src: `
export default class Test extends Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'component class & then default export new JSX transform',
src: `
class Test extends Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
export default Test
`,
},
{
name: 'PureComponent class & then default export new JSX transform',
src: `
class Test extends PureComponent<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
export default Test
`,
},
];
describe.each(reactComponentDefinitions)(
'React component defined as: $name',
({ src }) => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export interface TestProps {
name: string;
displayAge: boolean;
}
${src}
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should properly setup the controls based on the component props', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Test } from './test-ui-lib';
const Story: ComponentMeta<typeof Test> = {
component: Test,
title: 'Test'
};
export default Story;
const Template: ComponentStory<typeof Test> = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {
name: '',
displayAge: false,
};
`);
});
}
);
});
describe('Component files with NO DEFAULT export', () => {
const noDefaultExportComponents = [
{
name: 'no default simple export function',
src: `export function Test(props: TestProps) {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
`,
},
{
name: 'no default arrow function',
src: `
export const Test = (props: TestProps) => {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
`,
},
{
name: 'no default arrow function without {..}',
src: `
export const Test = (props: TestProps) => <div><h1>Welcome to test component, {props.name}</h1></div>;
`,
},
{
name: 'no default direct export of component class',
src: `
export class Test extends React.Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'no default component class',
src: `
export class Test extends React.Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'no default PureComponent class & then default export',
src: `
export class Test extends React.PureComponent<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'no default direct export of component class new JSX transform',
src: `
export class Test extends Component<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
{
name: 'no default PureComponent class & then default export new JSX transform',
src: `
export class Test extends PureComponent<TestProps> {
render() {
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
}
}
`,
},
];
describe.each(noDefaultExportComponents)(
'React component defined as: $name',
({ src }) => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export interface TestProps {
name: string;
displayAge: boolean;
}
${src}
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should properly setup the controls based on the component props', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Test } from './test-ui-lib';
const Story: ComponentMeta<typeof Test> = {
component: Test,
title: 'Test'
};
export default Story;
const Template: ComponentStory<typeof Test> = (args) => <Test {...args} />;
export const Primary = Template.bind({});
Primary.args = {
name: '',
displayAge: false,
};
`);
});
}
);
it('should create stories for all components in a file with no default export', async () => {
appTree.write(
cmpPath,
`import React from 'react';
function One() {
return <div>Hello one</div>;
}
function Two() {
return <div>Hello two</div>;
}
export interface ThreeProps {
name: string;
}
function Three(props: ThreeProps) {
return (
<div>
<h1>Welcome to Three {props.name}!</h1>
</div>
);
}
export { One, Two, Three };
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
const storyFilePathOne =
'libs/test-ui-lib/src/lib/test-ui-lib--One.stories.tsx';
const storyFilePathTwo =
'libs/test-ui-lib/src/lib/test-ui-lib--Two.stories.tsx';
const storyFilePathThree =
'libs/test-ui-lib/src/lib/test-ui-lib--Three.stories.tsx';
expect(formatFile`${appTree.read(storyFilePathOne, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { One } from './test-ui-lib';
const Story: ComponentMeta<typeof One> = {
component: One,
title: 'One'
};
export default Story;
const Template: ComponentStory<typeof One> = (args) => <One {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
expect(formatFile`${appTree.read(storyFilePathTwo, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Two } from './test-ui-lib';
const Story: ComponentMeta<typeof Two> = {
component: Two,
title: 'Two'
};
export default Story;
const Template: ComponentStory<typeof Two> = (args) => <Two {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
expect(formatFile`${appTree.read(storyFilePathThree, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Three } from './test-ui-lib';
const Story: ComponentMeta<typeof Three> = {
component: Three,
title: 'Three'
};
export default Story;
const Template: ComponentStory<typeof Three> = (args) => <Three {...args} />;
export const Primary = Template.bind({});
Primary.args = {
name: '',
};
`);
});
});
});
});
describe('using eslint', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should properly set up the story', () => {
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
.toContain(formatFile`
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { TestUiLib } from './test-ui-lib';
const Story: ComponentMeta<typeof TestUiLib> = {
component: TestUiLib,
title: 'TestUiLib'
};
export default Story;
const Template: ComponentStory<typeof TestUiLib> = (args) => <TestUiLib {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`);
});
});
});
export async function createTestUILib(libName: string): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await libraryGenerator(appTree, {
name: libName,
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'jest',
});
const currentWorkspaceJson = getProjects(appTree);
const projectConfig = currentWorkspaceJson.get(libName);
projectConfig.targets.lint.options.linter = 'eslint';
updateProjectConfiguration(appTree, libName, projectConfig);
return appTree;
}