Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vulcan + Storybook Discussion #1939

Closed
jefflau opened this issue Mar 24, 2018 · 11 comments
Closed

Vulcan + Storybook Discussion #1939

jefflau opened this issue Mar 24, 2018 · 11 comments

Comments

@jefflau
Copy link

jefflau commented Mar 24, 2018

Problem

Storybook does not understand how to parse the meteor npm imports.

Possible Solution

After some playing around I managed to get it to work if I extract all Vulcan packages and higher order components that contain queries into a container component and the 'dumb' components just contain the UI. This component has no UI components inside it and it passes the Vulcan components to its children. This allows you to import the non-container components into Storybook and pass it mock functions and Components that are not from Vulcan as props into your dumb components.

The container components will not be imported into your stories. It gets a little more complicated with nested components as you need to mock even more components and then pass them through.

Thoughts

Components have to be more verbose and have separate files for each. Must avoid imports from Meteor/Vulcan in your storybook tested components. However promotes better testing practice as all testable components needs to be pure(ish), they can't even have a single Meteor import, so promotes separation of concerns of your components to container vs UI components.

Vulcan's registerComponent makes it quite easy to mock the components as all of the components you need to pass as prop are contained under one object. Makes me think about keeping components pure by using the recompose library to map props and not directly import them in to make it easier to mock them out.

Jest allows you to setup mocks for certain paths. I'm not that experienced with Storybook, so maybe there is a way to mock out modules such as Meteor in the way Jest mocks it out.

Things that need to be worked out

I haven't figured out how to import css in yet such as bootstrap, but that isn't Vulcan specific so should be worked out easily.

Code

// ClientItem.js

import React from 'react'

const ClientsItem = ({ client, currentUser, check, columns, Components }) => (
  <tr>
    {columns.map((column, i) => (
      <td key={column.field}>
        <column.component document={client} />
      </td>
    ))}

    <td>
      {check(currentUser, client) ? (
        <Components.ModalTrigger label="Edit Client">
          <Components.ClientsEditForm
            currentUser={currentUser}
            documentId={client._id}
          />
        </Components.ModalTrigger>
      ) : null}
    </td>
  </tr>
)

export default ClientsItem
// ClientsItemContainer.js
import React, { PropTypes, Component } from 'react'
import { registerComponent, Components } from 'meteor/vulcan:core'
import { withProps, compose } from 'recompose'
import { Link } from 'react-router'

import Clients from '../../modules/clients/collection.js'
import ClientsItem from './ClientsItem'

const enhance = compose(
  withProps(ownerProps => ({
    check: Clients.options.mutations.edit.check,
    Components
  }))
)

const ClientsItemContainer = enhance(ClientsItem)

registerComponent('ClientsItem', ClientsItemContainer)

export default ClientsItemContainer
// clientItem.stories.js

import React from 'react'

import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { linkTo } from '@storybook/addon-links'

import { Button, Welcome } from '@storybook/react/demo'
import ClientsItem from '../components/clients/ClientsItem'
import { Link } from 'react-router'
import moment from 'moment'

const columns = [
  {
    label: 'Name',
    field: 'firstName',
    order: 10,
    component: ({ document: d }) => (
      <Link to={`/client/${d._id}`}>{`${d.firstName} ${d.lastName}`}</Link>
    ),
    sort: true
  },
  {
    label: 'Last Interaction',
    field: 'lastInteraction',
    order: 10,
    component: ({ document: d }) =>
      d.lastInteraction ? (
        <div>
          {moment(d.lastInteraction).format('dddd, MMMM Do YYYY, hh:mm:ss')}
        </div>
      ) : (
        'N/A'
      ),
    sort: true
  }
]

const ComponentsMocks = {
  ModalTrigger: () => <div>Edit Client</div>,
  EditForm: () => <div />
}

storiesOf('Client Single', module).add('with Text', () => (
  <ClientsItem
    check={() => true}
    Components={ComponentsMocks}
    columns={columns}
    client={{
      _id: 123,
      firstName: 'Jeff',
      lastName: 'Lau',
      lastInteraction: new Date()
    }}
  />
))
@Discordius
Copy link
Contributor

The part where you have to mock out the child component that are referenced via Component.* seems worst to me. For my codebase, this would make refactoring components a giant pain, and generally require massive amount of mocking (and make it hard to actually test the UI, because you can't really see a single full Post or a Comment or a Toolbar, because they all have subcomponents).

@jefflau jefflau changed the title Vulcan + Storybook Vulcan + Storybook Discussiin Mar 25, 2018
@jefflau jefflau changed the title Vulcan + Storybook Discussiin Vulcan + Storybook Discussion Mar 25, 2018
@jefflau
Copy link
Author

jefflau commented Mar 25, 2018

@Discordius Hey thanks for the feedback. I ran into this issue and what I did was to import the subcomponents (the non-container versions of them). It was a reasonable amount of mocking, but not much more than i'd have to do if I was testing it with a normal test framework anyway.

In my normal app, my <ClientItem /> is wrapped like above, but in the mock test I'm not composing it and just mocking the data. This is the best solution I could come up with yesterday. There might be a better a way.

The only things I couldn't really mock were the Vulcan specific components, but if I just want to test my components, it's not so bad. So stuff like the Modal Trigger I haven't figured out a way to get that out yet, although I'm not too worried about that - I mainly wanted it for UI development of my own components.

import React from 'react'

import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { linkTo } from '@storybook/addon-links'

import { Button, Welcome } from '@storybook/react/demo'
import ClientsList from '../components/clients/ClientsList'
import ClientsItem from '../components/clients/ClientsItem'
import { Link } from 'react-router'
import moment from 'moment'

const columns = [
  {
    label: 'Name',
    field: 'firstName',
    order: 10,
    component: ({ document: d }) => (
      <Link to={`/client/${d._id}`}>{`${d.firstName} ${d.lastName}`}</Link>
    ),
    sort: true
  },
  {
    label: 'Last Interaction',
    field: 'lastInteraction',
    order: 10,
    component: ({ document: d }) =>
      d.lastInteraction ? (
        <div>
          {moment(d.lastInteraction).format('dddd, MMMM Do YYYY, hh:mm:ss')}
        </div>
      ) : (
        'N/A'
      ),
    sort: true
  }
]

const mockData = [
  {
    //...
  }
]

const ComponentsMocks = {
  ModalTrigger: () => <div>Edit Client</div>,
  EditForm: () => <div />,
  ClientsItem: ({ client }) => (
    <ClientsItem
      check={() => true}
      Components={ComponentsMocks}
      columns={columns}
      client={client}
    />
  ),
  ClientsNewForm: () => <div />
}

storiesOf('Clients List', module).add('with Text', () => (
  <ClientsList
    results={mockData}
    currentUser={{}}
    loading={false}
    loadMore={() => true}
    count={mockData.length}
    totalCount={mockData.length}
    terms={{}}
    setTerms={() => true}
    Components={ComponentsMocks}
  />
))

@eric-burel
Copy link
Contributor

For style import I use a main decorator that imports all needed libs.

import jquery from 'jquery';
global.$ = jquery
global.jQuery = jquery
require('bootstrap/dist/js/bootstrap');
import 'bootstrap/dist/css/bootstrap.min.css';
import React from 'react'
import { Grid } from 'react-bootstrap'
import styled, { ThemeProvider } from 'styled-components';

export const MainDecorator = (story) => (
  <Grid>
    {story()}
  </Grid>
)
export default MainDecorator

Then in your storybook config.js simply write addDecorator(MainDecorator).

Concerning Meteor + Storybook, another direction would be to replace Node by Meteor as the runtime executable.
I don't know if it is possible, but it would be far easier than trying to mock the whole world.

Right now I avoid Meteor stuffs in my React components too but that can be annoying. This is especially true in Vulcan, that exploits cleverly the Meteor features even for purely frontend stuffs, like registerComponent.

Also, Storybook should not be only a unit development interface, it should also enable people to test fully integrated components, including Meteor/React components. In other projects I use it with full fledged containers, and even allow API calls. That makes me gain an infinite amount of time and that is the desirable goal.
I think forking/modifying Storybook so that we get a real Meteor support is still the best direction here.

@Hypnosphi
Copy link

Storybook does not understand how to parse the meteor npm imports.

Can you please elaborate this part?

@SachaG
Copy link
Contributor

SachaG commented Apr 1, 2018

@Hypnosphi a sample Meteor import might be:

import { Components } from 'meteor/vulcan:core'

@Hypnosphi
Copy link

So what's the error when you try to do that?

@jefflau
Copy link
Author

jefflau commented Apr 5, 2018

@Hypnosphi

Meteor packages are kept instead the .meteor folder and to be compliant with certain tools allows you to use 'meteor/[package name]'. Storybook uses webpack so it doesn't have any way of resolving those paths to the actual location of the Meteor package. Therefore for that to work either Meteor packages and the core Meteor packages would have to be on npm or the storybook webpack would have to be able to resolve meteor packages somehow

@Hypnosphi
Copy link

does this work?

// .storybook/webpack.config.js
const path = require('path')

module.exports =  {
  resolve: {
    alias: {
      meteor: path.resolve('../.meteor')
    }
  }
}

@SachaG
Copy link
Contributor

SachaG commented Apr 5, 2018

No, because Meteor packages are just not NPM packages. They have a package.js instead of a package.json, they rely on Meteor globals, etc.

@stale
Copy link

stale bot commented Nov 23, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Nov 23, 2018
@eric-burel eric-burel removed the stale label Mar 28, 2019
@eric-burel
Copy link
Contributor

I think we can close this :) Storybook install is documented here: https://github.com/VulcanJS/vulcan-docs/blob/master/source/storybook.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants