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}

); }; 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; }