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.
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'
]
}
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.
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:
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.
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 themain.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 (defaultfalse
)storybook_url
– can be changed if Storybook is running on a different port/hostname (defaulthttp://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 totrue
will hide these warnings (default isfalse
)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
}
}
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.
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:
- Storybook interaction tests
- How to use the play() function
- Test hooks API
- Storybook accessibility testing
- Component story format (CSF)
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;
}
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
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
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)
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
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.
// 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
You can also use the integrated debug console to issue commands from 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:
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> │ │
│ │
╚═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
MIT