Skip to content

nightwatchjs/nightwatch-storybook-plugin

Repository files navigation

@nightwatch/storybook

Nightwatch.js Logo  ️   React Logo

npm Node.js CI Discord Node Support

The official @nightwatch/storybook plugin provides seamless integration between Nightwatch and Storybook for React. Nightwatch supercharges your Storybook by providing several important capabilities for component testing.


Installation

The Storybook plugin for Nightwatch can be installed from NPM with:

npm i @nightwatch/storybook --save-dev

Then add the plugin in your nightwatch.conf.js:

module.exports = {
  plugins: [
    //...
    '@nightwatch/storybook'      
  ]
}

Prerequisites

The plugin can be used in an existing Storybook project for React. If you're starting from scratch and you'd just like to check out some examples quickly, head over to our storybook-example-project which has a few basic React components.

1. Setup Storybook

In an existing React project, run:

npx storybook init

Head over to the Storybook installation guide for more details.

We also recommend installing a few essential Storybook addons:

2. Install Nightwatch

Install Nightwatch in the same project. This plugin requires Nightwatch v2.4 or higher.

npm init nightwatch

Head over to the Nightwatch installation guide for more details.


Configuration

The @nightwatch/storybook plugin supports a few configuration options:

  • Nightwatch can start/stop the storybook server for you, if needed (which can be useful when running in CI).
  • Storybook url can be changed if storybook is running on a different hostname/port
  • you can configure the location(s) to where the stories are located in the Nightwatch src_folders

Edit your nightwatch.conf.js and configure it as follows:

  • src_folders By default Nightwatch tries to use the location defined in the main.js inside the storybook config folder. This can define the specific location(s) to where the stories are located.

The following options need to be set under the specific '@nightwatch/storybook' dictionary:

  • start_storybook – whether Nightwatch should manage the Storybook server automatically (default false)
  • storybook_url – can be changed if Storybook is running on a different port/hostname (default http://localhost:6006/)
  • storybook_config_dir - default is .storybook
  • hide_csf_errors - Nightwatch tries to ignore the CSF parsing errors and displays a warning; setting this to true will hide these warnings (default is false)
  • show_browser_console - By default when using Chrome or Edge browsers, the browser console logs will be displayed in the Nightwatch console (using the [browser] prefix); this options disables this functionality.

Examples:

module.exports = {
  src_folders: ['src/stories/*.stories.jsx'],
  
  '@nightwatch/storybook': {
    start_storybook: false,
    storybook_url: 'http://localhost:6006/',
    storybook_config_dir: '.storybook', // default storybook config directory
    hide_csf_errors: false,
    show_browser_console: true
  }
}

Usage

→ Run your existing stories in Nightwatch

There is no need to start writing additional tests and import stories in them. Nightwatch supports the Component Story Format (CSF) so it is able to run the stories directly.

→ Extend component stories with new testing capabilities

Nightwatch is able to detect and run any existing interaction tests (using the play() function) and accessibility tests which are defined in the component story.

In addition, it provides the ability to extend the component story with its own testing capabilities, as follows:

  • define a story-bound test() function;
  • support the test hooks API, defined in the default story export:
    • setup (browser)
    • teardown (browser)
    • preRender (browser, {id, title, name})
    • postRender (browser, {id, title, name})

All test hooks are async.

Read more on:

Example

Considering a basic Form.jsx component, here's how its Form.stories.jsx story would look like, written in CSF and extended with Nightwatch functionality:

// Form.stories.jsx
import { userEvent, within } from '@storybook/testing-library';
import Form from './Form.jsx';

export default {
  title: 'Form',
  component: Form,

  async setup(browser) {
    console.log('setup hook', browser.capabilities)
  },

  async preRender(browser) {
    console.log('preRender hook')
  },

  async postRender(browser) {
    console.log('postRender hook')
  },
  
  async teardown(browser) {
    console.log('teardown hook')
  },
}

const Template = (args) => <Form {...args} />;

// Component story for an empty form
export const EmptyForm = Template.bind({});

// Component story simulating filling in the form
export const FilledForm = Template.bind({});

FilledForm.play = async ({ canvasElement }) => {

  // Starts querying the component from its root element
  const canvas = within(canvasElement);

  // 👇 Simulate interactions with the component
  await userEvent.type(canvas.getByTestId('new-todo-input'), 'outdoors hike');
  await userEvent.click(canvas.getByRole('button'));
};

FilledForm.test = async (browser, { component }) => {
  // 👇 Run commands and assertions in the Nightwatch context
  await expect(component).to.be.visible;
}

Run stories with Nightwatch

The example contains two stories and it can be run by Nightwatch as a regular test.

For the best developer experience available at the moment, we recommend to use Chrome, however you can use any of the other browsers that Nightwatch supports as well.

npx nightwatch src/stories/Form.stories.jsx --env chrome

Running a specific story

You can run a specific story from a given .stories.jsx file by using the --story CLI argument.

Say you want to run only the FilledForm story. This will mount it and also execute the play() and test() functions accordingly:

npx nightwatch src/stories/Form.stories.jsx --env chrome --story=FilledForm

Run stories in parallel

It may be useful to run the stories in parallel for optimizing the speed of execution using the existing Nightwatch option of running in parallel using test workers. In fact, running in parallel using test workers is enabled by default in Nightwatch v2.4.

To run, for example, using 4 test worker processes (in headless mode):

npx nightwatch ./src/stories/**.stories.jsx --env chrome --workers=4 --headless

The output should look as follows:

Launching up to 4 concurrent test worker processes...

 Running:  *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx 
 Running:  *.stories.@(js|jsx|ts|tsx)/Form.stories.jsx 
 Running:  *.stories.@(js|jsx|ts|tsx)/Header.stories.jsx 
 Running:  *.stories.@(js|jsx|ts|tsx)/Input.stories.jsx 

┌ ────────────────── ✔  *.stories.@(js|jsx|ts|tsx)/Form.stories.jsx  ──────────────────────────────────────────────────────┐
│                                                                                                                          │
│                                                                                                                          │
│    [Form.stories.jsx component] Test Suite                                                                               │
│    ──────────────────────────────────────────────────────────────────────────────                                        │
│    Using: chrome (105.0.5195.125) on MAC OS X.                                                                           │
│                                                                                                                          │
│    – "Filled Form" story                                                                                                 │
│    ✔ Passed [ok]: "form--filled-form.FilledForm" story was rendered successfully.                                        │
│    ✔ Expected element <form--filled-form.FilledForm> to be visible (8ms)                                                 │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Form.stories.jsx [Form.stories.jsx component] "Filled Form" story (715ms)                │
│                                                                                                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

 Running:  *.stories.@(js|jsx|ts|tsx)/Page.stories.jsx 

┌ ────────────────── ✔  *.stories.@(js|jsx|ts|tsx)/Header.stories.jsx  ───────────────────────────────────────────┐
│                                                                                                                 │
│                                                                                                                 │
│    [Header.stories.jsx component] Test Suite                                                                    │
│    ───────────────────────────────────────────────────────────────────────────────                              │
│    Using: chrome (105.0.5195.125) on MAC OS X.                                                                  │
│    – "Logged In" story                                                                                          │
│    ✔ Passed [ok]: "example-header--logged-in.LoggedIn" story was rendered successfully.                         │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Header.stories.jsx [Header.stories.jsx component] "Logged In" story (764ms)     │
│                                                                                                                 │
│    – "Logged Out" story                                                                                         │
│    ✔ Passed [ok]: "example-header--logged-out.LoggedOut" story was rendered successfully.                       │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Header.stories.jsx [Header.stories.jsx component] "Logged Out" story (403ms)    │
│                                                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌ ────────────────── ✔  *.stories.@(js|jsx|ts|tsx)/Input.stories.jsx  ───────────────────────────────────────────────────────┐
│                                                                                                                            │
│                                                                                                                            │
│    [Input.stories.jsx component] Test Suite                                                                                │
│    ───────────────────────────────────────────────────────────────────────────────                                         │
│    Using: chrome (105.0.5195.125) on MAC OS X.                                                                             │
│                                                                                                                            │
│    – "Input With Common Value" story                                                                                       │
│    ✔ Passed [ok]: "input--input-with-common-value.InputWithCommonValue" story was rendered successfully.                   │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Input.stories.jsx [Input.stories.jsx component] "Input With Common Value" story (855ms)    │
│    – "Input With Scoped Value" story                                                                                       │
│    ✔ Passed [ok]: "input--input-with-scoped-value.InputWithScopedValue" story was rendered successfully.                   │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Input.stories.jsx [Input.stories.jsx component] "Input With Scoped Value" story (303ms)    │
│    – "Input With Inline Value" story                                                                                       │
│    ✔ Passed [ok]: "input--input-with-inline-value.InputWithInlineValue" story was rendered successfully.                   │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Input.stories.jsx [Input.stories.jsx component] "Input With Inline Value" story (406ms)    │
│                                                                                                                            │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌ ────────────────── ✔  *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx  ──────────────────────────────────────────┐
│                                                                                                                │
│                                                                                                                │
│    [Button.stories.jsx component] Test Suite                                                                   │
│    ───────────────────────────────────────────────────────────────────────────────                             │
│    Using: chrome (105.0.5195.125) on MAC OS X.                                                                 │
│                                                                                                                │
│    – "Primary" story                                                                                           │
│    ✔ Passed [ok]: "example-button--primary.Primary" story was rendered successfully.                           │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx [Button.stories.jsx component] "Primary" story (840ms)      │
│    – "Secondary" story                                                                                         │
│    ✔ Passed [ok]: "example-button--secondary.Secondary" story was rendered successfully.                       │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx [Button.stories.jsx component] "Secondary" story (384ms)    │
│    – "Large" story                                                                                             │
│    ✔ Passed [ok]: "example-button--large.Large" story was rendered successfully.                               │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx [Button.stories.jsx component] "Large" story (361ms)        │
│    – "Small" story                                                                                             │
│    ✔ Passed [ok]: "example-button--small.Small" story was rendered successfully.                               │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Button.stories.jsx [Button.stories.jsx component] "Small" story (320ms)        │
│                                                                                                                │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌ ────────────────── ✔  *.stories.@(js|jsx|ts|tsx)/Page.stories.jsx  ─────────────────────────────────────────┐
│                                                                                                             │
│                                                                                                             │
│    [Page.stories.jsx component] Test Suite                                                                  │
│    ──────────────────────────────────────────────────────────────────────────────                           │
│    Using: chrome (105.0.5195.125) on MAC OS X.                                                              │
│    – "Logged Out" story                                                                                     │
│    ✔ Passed [ok]: "example-page--logged-out.LoggedOut" story was rendered successfully.                     │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Page.stories.jsx [Page.stories.jsx component] "Logged Out" story (489ms)    │
│                                                                                                             │
│    – "Logged In" story                                                                                      │
│    ✔ Passed [ok]: "example-page--logged-in.LoggedIn" story was rendered successfully.                       │
│    ✔ *.stories.@(js|jsx|ts|tsx)/Page.stories.jsx [Page.stories.jsx component] "Logged In" story (437ms)     │
│                                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

  ✨ PASSED. 13 total assertions (4.483s)

Preview stories using Nightwatch

Nightwatch provides the ability to run a .stories.jsx file in preview mode (using the --preview CLI argument) which would only open the Storybook renderer and pause the execution indefinitely.

This can be useful during development, since the Storybook renderer has the ability to automatically reload the component via its built-in Hot Module Replacement (HMR) functionality.

To launch the FilledForm story in preview mode, run:

npx nightwatch src/stories/Form.stories.jsx --env chrome --story=FilledForm --preview 

Pass the --devtools flag to open the Chrome Devtools:

npx nightwatch src/stories/Form.stories.jsx --env chrome --story=FilledForm --preview --devtools

You can of course use the Nightwatch built-in parallelism to open the story in both Firefox and Chrome:

npx nightwatch src/stories/Form.stories.jsx --env chrome,firefox --story=FilledForm --preview 

Debugging stories using Nightwatch

In addition to previewing the story, it's also possible to use Nightwatch to debug the story. To do this, enable the --debug and --devtools CLI flags and use the debugger to add breakpoints inside the play() function.

Example:

// Form.stories.jsx
import { userEvent, within } from '@storybook/testing-library';
import Form from './Form.jsx';

export default {
  title: 'Form',
  component: Form,
}

const Template = (args) => <Form {...args} />;

// Component story for an empty form
export const EmptyForm = Template.bind({});

// Component story simulating filling in the form
export const FilledForm = Template.bind({});

FilledForm.play = async ({ canvasElement }) => {

  // Starts querying the component from its root element
  const canvas = within(canvasElement);
  
  debugger;
  
  // 👇 Simulate interactions with the component
  await userEvent.type(canvas.getByTestId('new-todo-input'), 'outdoors hike');
  await userEvent.click(canvas.getByRole('button'));
};

FilledForm.test = async (browser, { component }) => {
  // 👇 Run commands and assertions in the Nightwatch context
  await expect(component).to.be.visible;
}

Run the example and observe the breakpoint in the Chrome devtools console.

npx nightwatch src/stories/Form.stories.jsx --env chrome --devtools --debug --story=FilledForm 

Screenshot of the Chrome Devtools debugger paused at a breakpoint

You can also use the integrated debug console to issue commands from Nightwatch.


Accessibility testing (A11y) with Storybook + Nightwatch

Both Storybook and Nightwatch rely internally on the same accessibility testing tools developed by Deque Systems and published in NPM as the axe-core library.

To get started with in A11y testing in Storybook, install the addon:

npm i @storybook/addon-a11y --save-dev 

Add this line to your main.js file (create this file inside your Storybook config directory if needed).

module.exports = {
  addons: ['@storybook/addon-a11y'],
};

More details can be found on Storybook docs:

Example

Consider the bundled example Button.jsx component and Button.stories.jsx which come pre-installed when you setup Storybook.

Add the following rules for accessibility tests:

// Button.stories.jsx

import React from 'react';
import { Button } from './Button';

export default {
  title: "Example/Button",
  component: Button,
  argTypes: {
    backgroundColor: { control: "color" },
  },
  /**
   * BEGINNING OF NEW A11Y RULES
   * 
   */
  parameters: {
    a11y: {
      // Optional selector to inspect
      element: '#root',
      
      // Show the individual axe-rules as Nightwatch assertions (can be verbose if there are many violations)
      runAssertions: false,
      
      // Show the complete Acccessibilty test report (by default, only rule violations will be shown)
      verbose: false,
      config: {
        rules: [
          {
            // The autocomplete rule will not run based on the CSS selector provided
            id: 'autocomplete-valid',
            selector: '*:not([autocomplete="nope"])',
          },
          {
            // Setting the enabled option to false will disable checks for this particular rule on all stories.
            id: 'image-alt',
            enabled: false,
          },
          {
            id: 'input-button-name',
            enabled: true
          },
          {
            id: 'color-contrast',
            enabled: true
          }
        ],
      },
      options: {},
      manual: true,
    },
  }
  /**
   * 
   * END OF NEW A11Y RULES
   */
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

Nightwatch will automatically pick up the A11y rules from the story config and use them to run its own accessibility test commands.

One of the Button component story will fail the "color-contrast" accessibility rule as defined by the Axe-core library.

Run the following to see the result:

npx nightwatch src/stories/Button.stories.jsx -e chrome

The output from Nightwatch should be:

  ️TEST FAILURE (2.947s):  
   - 1 assertions failed; 4 passed

   ✖ 1) Button.stories
   – "Primary" story (733ms)

   → ✖ NightwatchAssertError
   There are accessibility violations; please see the complete report for details.

    Read More : 
        https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md 


Accessibility report for: example-button--primary.Primary

Accessibility violations for: example-button--primary.Primary
┌───────────────────────┬────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐
│ ID                    │ Impact     │ Description                                                                                                       │ Nodes      │
│ ───────────────────── │ ────────── │                                                                                                                   │ ────────── │
│ color-contrast        │ serious    │ Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds           │ 1          │
│ ───────────────────── │ ────────── │                                                                                                                   │ ────────── │
│ Target                             │ Html                                                                                                              │ Violations │
│ [".storybook-button"]              │ <button type="button" class="storybook-button storybook-button--medium storybook-button--primary">Button</button> │            │
│                                                                                                                                                                     │
╚═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝

To view the entire report (which includes all the evaluated rules), pass verbose: true in the story parameters:

// Button.stories.jsx

import React from 'react';
import { Button } from './Button';

export default {
  parameters: {
    a11y: {
      // Show the complete Acccessibilty test report (by default, only rule violations will be shown)
      verbose: false,
      // ...
    }
  }
}

Example output:

Accessibility report for: example-button--primary.Primary
┌───────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────┐
│ Rule                  │ Description                                                                                                                                           │ Nodes   │
│ ───────────────────── │ ──────────                                                                                                                                            │ ─────── │
│ aria-hidden-body      │ Ensures aria-hidden='true' is not present on the document body.                                                                                       │ 1       │
│ aria-hidden-focus     │ Ensures aria-hidden elements are not focusable nor contain focusable elements                                                                         │ 1       │
│ button-name           │ Ensures buttons have discernible text                                                                                                                 │ 1       │
│ duplicate-id          │ Ensures every id attribute value is unique                                                                                                            │ 4       │
│ nested-interactive    │ Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies │ 1       │
│ region                │ Ensures all page content is contained by landmarks                                                                                                    │ 2       │
│ ───────────────────── │ ────────────────────────                                                                                                                              │ ─────── │
│ Target                │ Html                                                                                                                                                            │
│ ["body"]              │ <body class="sb-main-padded sb-show-main">                                                                                                                      │
│ ["table"]             │ <table aria-hidden="true" class="sb-argstableBlock">                                                                                                            │
│ [".storybook-button"] │ <button type="button" class="storybook-button storybook-button--medium storybook-button--primary">Button</button>                                               │
│ ["#error-message"]    │ <pre id="error-message" class="sb-heading"></pre>                                                                                                               │
│ ["#error-stack"]      │ <code id="error-stack"></code>                                                                                                                                  │
│ ["#root"]             │ <div id="root"><button type="button" class="storybook-button storybook-button--medium storybook-button--primary">Button</button></div>                          │
│ ["#docs-root"]        │ <div id="docs-root" hidden="true"></div>                                                                                                                        │
╚═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝

Accessibility violations for: example-button--primary.Primary
┌───────────────────────┬────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐
│ ID                    │ Impact     │ Description                                                                                                       │ Nodes      │
│ ───────────────────── │ ────────── │                                                                                                                   │ ────────── │
│ color-contrast        │ serious    │ Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds           │ 1          │
│ ───────────────────── │ ────────── │                                                                                                                   │ ────────── │
│ Target                             │ Html                                                                                                              │ Violations │
│ [".storybook-button"]              │ <button type="button" class="storybook-button storybook-button--medium storybook-button--primary">Button</button> │            │
│                                                                                                                                                                     │
╚═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝

License

MIT