import { getProjects, Tree, updateProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import libraryGenerator from '../library/library';
import componentStoryGenerator from './component-story';
import { Linter } from '@nx/linter';
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
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 (
Welcome to test component
);
};
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(appTree.read(storyFilePathPlain, 'utf-8')).toMatchSnapshot();
});
});
describe('component without any props defined', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`import React from 'react';
import './test.scss';
export const Test = () => {
return (
Welcome to test component
);
};
export default Test;
`
);
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
});
});
it('should create a story without controls', () => {
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
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 (
Welcome to test component, {props.name}
);
};
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
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 (
Welcome to test component, {props.name}
Click me!
);
};
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
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 (
Welcome to test component, {props.name}
);
};
`,
},
{
name: 'function and then export',
src: `
function Test(props: TestProps) {
return (
Welcome to test component, {props.name}
);
};
export default Test;
`,
},
{
name: 'arrow function',
src: `
const Test = (props: TestProps) => {
return (
Welcome to test component, {props.name}
);
};
export default Test;
`,
},
{
name: 'arrow function without {..}',
src: `
const Test = (props: TestProps) =>
Welcome to test component, {props.name} ;
export default Test
`,
},
{
name: 'direct export of component class',
src: `
export default class Test extends React.Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'component class & then default export',
src: `
class Test extends React.Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
export default Test
`,
},
{
name: 'PureComponent class & then default export',
src: `
class Test extends React.PureComponent {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
export default Test
`,
},
{
name: 'direct export of component class new JSX transform',
src: `
export default class Test extends Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'component class & then default export new JSX transform',
src: `
class Test extends Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
export default Test
`,
},
{
name: 'PureComponent class & then default export new JSX transform',
src: `
class Test extends PureComponent {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
}
);
});
describe('Component files with NO DEFAULT export', () => {
const noDefaultExportComponents = [
{
name: 'no default simple export function',
src: `export function Test(props: TestProps) {
return (
Welcome to test component, {props.name}
);
};
`,
},
{
name: 'no default arrow function',
src: `
export const Test = (props: TestProps) => {
return (
Welcome to test component, {props.name}
);
};
`,
},
{
name: 'no default arrow function without {..}',
src: `
export const Test = (props: TestProps) =>
Welcome to test component, {props.name} ;
`,
},
{
name: 'no default direct export of component class',
src: `
export class Test extends React.Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'no default component class',
src: `
export class Test extends React.Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'no default PureComponent class & then default export',
src: `
export class Test extends React.PureComponent {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'no default direct export of component class new JSX transform',
src: `
export class Test extends Component {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
{
name: 'no default PureComponent class & then default export new JSX transform',
src: `
export class Test extends PureComponent {
render() {
return
Welcome to test component, {this.props.name} ;
}
}
`,
},
];
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
}
);
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 Hello one
;
}
function Two() {
return Hello two
;
}
export interface ThreeProps {
name: string;
}
function Three(props: ThreeProps) {
return (
Welcome to Three {props.name}!
);
}
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(appTree.read(storyFilePathOne, 'utf-8')).toMatchSnapshot();
expect(appTree.read(storyFilePathTwo, 'utf-8')).toMatchSnapshot();
expect(appTree.read(storyFilePathThree, 'utf-8')).toMatchSnapshot();
});
});
});
});
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(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
});
export async function createTestUILib(libName: string): Promise {
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;
}