Skip to content

Commit

Permalink
Add initial value proposition
Browse files Browse the repository at this point in the history
  • Loading branch information
NicholasBoll committed Feb 3, 2020
1 parent 0a4ca9a commit 9af3552
Show file tree
Hide file tree
Showing 19 changed files with 14,003 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
.vscode

node_modules
4 changes: 4 additions & 0 deletions .prettierrc.json
@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}
1 change: 1 addition & 0 deletions .storybook/config.js
@@ -0,0 +1 @@
import '../react'
4 changes: 4 additions & 0 deletions .storybook/main.js
@@ -0,0 +1,4 @@
module.exports = {
stories: ['../stories/**/*.stories.js'],
addons: ['@storybook/addon-actions', '@storybook/addon-links']
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
67 changes: 67 additions & 0 deletions README.md
@@ -0,0 +1,67 @@
## Cypress Storybook

This library contains helper methods to integrate Cypress and Storybook. It contains helpful Cypress commands for loading stories in a way that doesn't require a full reload of the application, allowing tests/specifications to be run much faster.

### Installation

```
npm install cypress-storybook --save-dev
```

Once installed, both Cypress and Storybook need to be configured in order to work. Storybook installation will be based on the type.

#### Cypress

The following will add the Cypress commands to be available to Cypress spec files:

```js
// cypress/cupport/commands.js or .ts
import 'cypress-storybook/cypress`
```
#### React Storybook
The following will set up the Storybook app to understand the Cypress commands. It will register hidden functions one the `window` of the iframe Storybook uses for stories:
```js
// .storybook/config.js
import 'cypress-storybook/react'
```
### Use
Storybook is a great tool for developing UI. It encourages separation of UI development from backend development. It also encourages building smaller components. Cypress can be used to test or specify behavior of these components. Many examples on the web show loading up the main Storybook application and using Cypress to click through the navigation to enable the proper story. The issue with this approach is the story is in an iframe, which is much more difficult to work with. Storybook comes with a router that allows you to visit the story directly. If you expand a story to a full screen, you'll see the URL. It contains something like `iframe.html?id=button--text`.

This library works by loading the `iframe.html` which is blank since no story has been specified. Stories are later mounted using the Storybook routing API to unmount and mount/remount stories by their identifiers. Loading stories does not require a refresh of the Story page (`iframe.html`). The previous story is unmounted from the DOM and the next story is requested from the Storybook router API. Mounting a story takes milliseconds compared to seconds of reloading the entire page. This allows for faster tests.

This library only works if Stories don't leave behind some global state. It is recommended that your stories provide their own state. If you use a global store like Redux, be sure that each story has its own store provider so that the store is created for each story.
An example Cypress file might look like this:
```js
describe('Button', () => {
before(() => {
// Visit the storybook iframe page
cy.visitStorybook()
})
beforeEach(() => {
// The first parameter is the category. This is the `title` in CSF or the value in `storiesOf`
// The second parameter is the name of the story. This is the name of the function in CSF or the value in the `add`
// This does not refresh the page, but will unmount any previous story and use the Storybook Router API to render a fresh new story
cy.loadStory('Button', 'Text')
})
})
```
### Typescript Support
This project contains type definitions. If your project uses Typescript and the `cypress/support/commands` file is a `*.ts` file and the `cypress/tsconfig.json` was set up to include all TS files in the `cypress` directory, nothing additional needs to be done to get type definitions in Cypress files. If the type definitions are not automatically set up for you, you'll have to add the following to the TS config file:

```json
{
"compilerOptions": {
"types": ["cypress", "cypress-storybook/cypress"]
}
}
```
22 changes: 22 additions & 0 deletions cypress.d.ts
@@ -0,0 +1,22 @@
/// <reference types="cypress" />

declare namespace Cypress {
interface Chainable<Subject> {
/**
* Visit the blank test page. This should be in a `before` block of every test page. This command will load the iframe.html
* file. It is meant to be used with `cy.loadStory`
*/
visitStorybook(): Cypress.Chainable<Window>

/**
* Load a story. This will invoke the storybook router,
* unmount a previous story, mount the current story and force a rerender
* This should be in a `beforeEach` block to force a fresh new story
* @param categorization Categorization information found in the `.storiesOf` function or `title` - usually used for menu navigation
* @param story Variant of the story example in the `.add` function or the export name
* @example
* cy.loadStory('Button', 'Primary')
*/
loadStory(categorization: string, story: string): Cypress.Chainable<JQuery>
}
}
30 changes: 30 additions & 0 deletions cypress.js
@@ -0,0 +1,30 @@
/// <reference types="cypress" />

Cypress.Commands.add('visitStorybook', () => {
return cy.visit('iframe.html')
})

Cypress.Commands.add('loadStory', (categorization, story) => {
const log = Cypress.log({
name: 'Load',
message: [categorization, story],
$el: Cypress.$('#root')
})
log.snapshot('before')

const win = cy.state('window')
const now = performance.now()
win.__setCurrentStory(
categorization.replace(/[|/]/g, '-').toLowerCase(),
story.replace(/\s/g, '-').toLowerCase()
)
log.set('consoleProps', () => ({
categorization,
story,
renderTime: performance.now() - now
}))
log.snapshot('after')
log.end()

return Cypress.$('#root')
})
3 changes: 3 additions & 0 deletions cypress.json
@@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:6006"
}
5 changes: 5 additions & 0 deletions cypress/fixtures/example.json
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
38 changes: 38 additions & 0 deletions cypress/integration/button.spec.js
@@ -0,0 +1,38 @@
// @ts-check
/// <reference path="../../cypress.d.ts" />

describe('Button', () => {
before(() => {
cy.visitStorybook()
})

context('given the Button/Text story is rendered', () => {
beforeEach(() => {
cy.loadStory('Button', 'Text')
})

it('should render a button', () => {
cy.get('button').should('exist')
})

context('when the button is clicked', () => {
beforeEach(() => {
cy.get('button').click()
})

it('should update the button text to include "clicked"', () => {
cy.get('button').should('contain', 'clicked')
})

context('when the Button/Text story is re-rendered', () => {
beforeEach(() => {
cy.loadStory('Button', 'Text')
})

it('should reset all state', () => {
cy.get('button').should('not.contain', 'clicked')
})
})
})
})
})
17 changes: 17 additions & 0 deletions cypress/plugins/index.js
@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
1 change: 1 addition & 0 deletions cypress/support/commands.js
@@ -0,0 +1 @@
import '../../cypress'
1 change: 1 addition & 0 deletions cypress/support/index.js
@@ -0,0 +1 @@
import './commands'

0 comments on commit 9af3552

Please sign in to comment.