Merge branch 'release-8-0' into new-icon-library

This commit is contained in:
Charles de Dreuille 2023-10-19 10:03:06 +01:00
commit 117bfbdfe2
207 changed files with 1291 additions and 6448 deletions

View File

@ -26,8 +26,6 @@
/code/addons/links/ @yannbf @JReinhold
/code/addons/measure/ @yannbf @valentinpalkovic
/code/addons/outline/ @yannbf @valentinpalkovic
/code/addons/storyshots-core/ @ndelangen
/code/addons/storyshots-puppeteer/ @ndelangen
/code/addons/storysource/ @ndelangen
/code/addons/toolbars/ @ndelangen @JReinhold
/code/addons/viewport/ @yannbf @ndelangen

View File

@ -94,13 +94,7 @@ module.exports = {
},
},
{
files: [
'**/__tests__/**',
'**/__testfixtures__/**',
'**/*.test.*',
'**/*.stories.*',
'**/storyshots-*/**/stories/**',
],
files: ['**/__tests__/**', '**/__testfixtures__/**', '**/*.test.*', '**/*.stories.*'],
rules: {
'@typescript-eslint/no-empty-function': 'off',
'import/no-extraneous-dependencies': 'off',

View File

@ -7,8 +7,6 @@ logFilters:
level: discard
- code: YN0076
level: discard
- level: discard
pattern: '@workspace:addons/storyshots-*/'
nodeLinker: node-modules

View File

@ -82,7 +82,7 @@
"devDependencies": {
"@testing-library/react": "^11.2.2",
"resize-observer-polyfill": "^1.5.1",
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -101,7 +101,7 @@
"devDependencies": {
"@types/lodash": "^4.14.167",
"@types/uuid": "^9.0.1",
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -90,7 +90,7 @@
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -78,6 +78,5 @@ export const Defined = {
// parameters: {
// docs: { disable: true },
// chromatic: { disable: true },
// storyshots: { disable: true },
// },
// };

View File

@ -122,7 +122,7 @@
"@rollup/pluginutils": "^5.0.2",
"react": "^16.14.0",
"react-dom": "^16.8.0",
"typescript": "~4.9.3",
"typescript": "~5.2.2",
"vite": "^4.0.4"
},
"peerDependencies": {

View File

@ -57,7 +57,7 @@
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"publishConfig": {
"access": "public"

View File

@ -68,7 +68,7 @@
},
"devDependencies": {
"@types/webpack-env": "^1.16.0",
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"publishConfig": {
"access": "public"

View File

@ -94,7 +94,7 @@
"@storybook/testing-library": "next",
"@types/node": "^18.0.0",
"formik": "^2.2.9",
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -83,7 +83,7 @@
"upath": "^2.0.1"
},
"devDependencies": {
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -94,7 +94,7 @@
},
"devDependencies": {
"fs-extra": "^11.1.0",
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -87,7 +87,7 @@
"tiny-invariant": "^1.3.1"
},
"devDependencies": {
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -90,7 +90,7 @@
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"typescript": "~4.9.3"
"typescript": "~5.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",

View File

@ -1,12 +0,0 @@
module.exports = {
settings: {
'import/core-modules': [
'@storybook/angular',
'@storybook/html',
'@storybook/react',
'@storybook/preact',
'@storybook/vue',
'@storybook/svelte',
],
},
};

View File

@ -1,795 +0,0 @@
# StoryShots
StoryShots adds automatic Jest Snapshot Testing for [Storybook](https://storybook.js.org/).
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
![StoryShots In Action](https://raw.githubusercontent.com/storybookjs/storybook/next/code/addons/storyshots/storyshots-core/docs/storyshots-fail.png)
To use StoryShots, you must use your existing Storybook stories as the input for Jest Snapshot Testing.
## Getting Started
Add the following module into your app.
```sh
yarn add @storybook/addon-storyshots --dev
```
## Configure Storyshots for HTML snapshots
Create a new test file with the name `Storyshots.test.js`. (Or whatever the name you prefer, as long as it matches Jest's config [`testMatch`](http://facebook.github.io/jest/docs/en/configuration.html#testmatch-array-string)).
Then add following content to it:
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();
```
That's all.
Now run your Jest test command. (Usually, `npm test`.) Then you can see all of your stories are converted as Jest snapshot tests.
![Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/next/code/addons/storyshots/storyshots-core/docs/storyshots.png)
### Testing stories that rely on addon-added decorators
If you have stories in your Storybook that can only render inside a decorator (for instance the [`apollo-storybook-decorator`](https://github.com/abhiaiyer91/apollo-storybook-decorator)), you'll need to ensure those decorators are applied in Storyshots.
If you export those decorators from your `.storybook/preview.js` then Storyshots will apply those decorators for you in the same way that Storybook does. However if the addon _automatically_ adds the decorator for you (which is a new feature in Storybook 6.0), you will find the decorator does not get added in Storyshots. This is a limitation in Storyshots currently.
To ensure such decorators get added, export them from `.storybook/preview.js`:
```js
import addonDecorator from 'some-addon';
export const decorators = [addonDecorator];
```
## Configure your app for Jest
In many cases, for example Create React App, it's already configured for Jest. You need to create a filename with the extension `.test.js`.
If you still need to configure jest you can use the resources mentioned below:
- [Getting Started - Jest Official Documentation](https://facebook.github.io/jest/docs/en/getting-started.html)
- [Javascript Testing with Jest - Egghead](https://egghead.io/lessons/javascript-test-javascript-with-jest). **_paid content_**
> Note: If you use React 16, you'll need to follow [these additional instructions](https://github.com/facebook/react/issues/9102#issuecomment-283873039).
>
> Note: Make sure you have added the `json` extension to `moduleFileExtensions` in `jest.config.json`. If this is missing it leads to the [following error](https://github.com/storybookjs/storybook/issues/3728): `Cannot find module 'spdx-license-ids' from 'scan.js'`.
>
> Note: Please make sure you are using `jsdom` as the testEnvironment on your jest config file.
### Configure Jest to work with Webpack's [require.context()](https://webpack.js.org/guides/dependency-management/#require-context)
**NOTE**: if you are using Storybook 5.3's `main.js` to list story files, this is no longer needed.
Sometimes it's useful to configure Storybook with Webpack's require.context feature. You could be loading stories [one of two ways](https://github.com/storybookjs/storybook/blob/release/5.3/docs/src/pages/basics/writing-stories/index.md#loading-stories).
1. If you're using the `storiesOf` API, you can integrate it this way:
```js
import { configure } from '@storybook/react';
const req = require.context('../stories', true, /\.stories\.js$/); // <- import all the stories at once
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
configure(loadStories, module);
```
2. If you're using Component Story Format (CSF), you'll integrate it like so:
```js
import { configure } from '@storybook/react';
const req = require.context('../stories', true, /\.stories\.js$/); // <- import all the stories at once
configure(req, module);
```
The problem here is that it will work only during the build with webpack,
other tools may lack this feature. Since Storyshot is running under Jest,
we need to polyfill this functionality to work with Jest. The easiest
way is to integrate it to babel.
You can do this with a Babel [plugin](https://github.com/smrq/babel-plugin-require-context-hook) or [macro](https://github.com/storybookjs/require-context.macro). If you're using `create-react-app` (v2 or above), use the macro.
#### Option 1: Plugin
First, install it:
```sh
yarn add babel-plugin-require-context-hook --dev
```
Next, it needs to be registered and loaded before each test. To register it, create a file with the following register function `.jest/register-context.js`:
```js
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
registerRequireContextHook();
```
That file needs to be added as a setup file for Jest. To do that, add (or create) a property in Jest's config called [`setupFiles`](https://jestjs.io/docs/en/configuration.html#setupfiles-array). Add the file name and path to this array.
```json
setupFiles: ['<rootDir>/.jest/register-context.js']
```
Finally, add the plugin to `.babelrc`:
```json
{
"presets": ["..."],
"plugins": ["..."],
"env": {
"test": {
"plugins": ["require-context-hook"]
}
}
}
```
The plugin is only added to the test environment otherwise it could replace webpack's version of it.
#### Option 2: Macro
First, install it:
```sh
yarn add require-context.macro --dev
```
Now, inside of your Storybook config file, import the macro and run it in place of `require.context`, like so:
```javascript
import requireContext from 'require-context.macro';
// const req = require.context('../stories', true, /\.stories\.js$/); <-- replaced
const req = requireContext('../stories', true, /\.stories\.js$/);
```
### Configure Jest for React
StoryShots addon for React is dependent on [react-test-renderer](https://github.com/facebook/react/tree/master/packages/react-test-renderer), but
[doesn't](#deps-issue) install it, so you need to install it separately.
```sh
yarn add react-test-renderer --dev
```
### Configure Jest for Angular
StoryShots addon for Angular is dependent on [jest-preset-angular](https://github.com/thymikee/jest-preset-angular), but
[doesn't](#deps-issue) install it, so you need to install it separately.
```sh
yarn add jest-preset-angular
```
If you already use Jest for testing your angular app - probably you already have the needed jest configuration.
Anyway you can add these lines to your jest config:
```js
module.exports = {
globals: {
__TRANSFORM_HTML__: true,
},
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.(ts|html)$': '<rootDir>/node_modules/jest-preset-angular/preprocessor.js',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', '.html'],
};
```
### Configure Jest for Vue
StoryShots addon for Vue is dependent on [jest-vue-preprocessor](https://github.com/vire/jest-vue-preprocessor), but
[doesn't](#deps-issue) install it, so you need to install it separately.
```sh
yarn add jest-vue-preprocessor
```
If you already use Jest for testing your vue app - probably you already have the needed jest configuration.
Anyway you can add these lines to your jest config:
```js
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor',
},
transformIgnorePatterns: ['/node_modules/(?!(@storybook/.*\\.vue$))'],
moduleFileExtensions: ['vue', 'js', 'jsx', 'json', 'node'],
};
```
### Configure Jest for Vue 3
StoryShots addon for Vue is dependent on [vue-jest v5](https://www.npmjs.com/package/vue-jest/v/5.0.0-alpha.8), but
[doesn't](#deps-issue) install it, so you need to install it separately.
```sh
yarn add vue-jest@5.0.0-alpha.8
```
If you already use Jest for testing your vue app - probably you already have the needed jest configuration.
Anyway you can add these lines to your jest config:
```js
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
},
transformIgnorePatterns: ['/node_modules/(?!(@storybook/.*\\.vue$))'],
moduleFileExtensions: ['vue', 'js', 'jsx', 'json', 'node'],
};
```
### Configure Jest for Preact
StoryShots addon for Preact is dependent on [preact-render-to-string](https://github.com/preactjs/preact-render-to-string), but
[doesn't](#deps-issue) install it, so you need to install it separately.
```sh
yarn add preact-render-to-string --dev
```
### Configure Jest for Web Components
StoryShots addon for Web Components requires [jsdom](https://github.com/jsdom/jsdom) 16 or later to fully support the
web component shadow dom. To use jsdom 16 or later you can set the Jest `testEnvironment` configuration key to
`jest-environment-jsdom-sixteen`. This should work back to Jest 24 and is the default in Jest 26 and later.
### Configure Jest for MDX Docs Add-On Stories
If using the [Docs add-on](../../docs/README.md) with
[MDX stories](../../docs/docs/mdx.md) you will need
to configure Jest to transform MDX stories into something Storyshots can understand:
Add the following to your Jest configuration:
```json
{
"transform": {
"^.+\\.[tj]sx?$": "babel-jest",
"^.+\\.mdx?$": "@storybook/addon-docs/jest-transform-mdx"
}
}
```
### <a name="deps-issue"></a>Why don't we install dependencies of each framework ?
Storyshots addon is currently supporting React, Angular and Vue. Each framework needs its own packages to be integrated with Jest. We don't want people that use only React will need to bring other dependencies that do not make sense for them.
`dependencies` - will installed an exact version of the particular dep - Storyshots can work with different versions of the same framework (let's say React v16 and React v15), that have to be compatible with a version of its plugin (react-test-renderer).
`optionalDependencies` - behaves like a regular dependency, but do not fail the installation in case there is a problem to bring the dep.
`peerDependencies` - listing all the deps in peer will trigger warnings during the installation - we don't want users to install unneeded deps by hand.
`optionalPeerDependencies` - unfortunately there is nothing like this =(
For more information read npm [docs](https://docs.npmjs.com/files/package.json#dependencies)
### Using `createNodeMock` to mock refs
`react-test-renderer` doesn't provide refs for rendered components. By
default, it returns null when the refs are referenced. In order to mock
out elements that rely on refs, you will have to use the
`createNodeMock` option [added to React](https://reactjs.org/blog/2016/11/16/react-v15.4.0.html#mocking-refs-for-snapshot-testing) starting with version 15.4.0.
Here is an example of how to specify the `createNodeMock` option in Storyshots:
```js
import initStoryshots, { snapshotWithOptions } from '@storybook/addon-storyshots';
import TextareaThatUsesRefs from '../component/TextareaThatUsesRefs';
initStoryshots({
test: snapshotWithOptions({
createNodeMock: (element) => {
if (element.type === TextareaThatUsesRefs) {
return document.createElement('textarea');
}
},
}),
});
```
Provide a function to have story-specific options:
```js
initStoryshots({
test: snapshotWithOptions((story) => ({
createNodeMock: (element) => {
if (story.name == 'foobar') {
return null;
}
return element;
},
})),
});
```
### Using a custom renderer
By design, [`react-test-renderer` doesn't use a browser environment or JSDOM](https://github.com/facebook/react/issues/20589). Because of this difference, some stories might render in your browser, but not in Storyshots. If you encounter this problem, you may want to switch for an higher level renderer such as `mount` from Enzyme or `render` from React Testing Library.
#### Example with React Testing Library
```js
import initStoryshots from '@storybook/addon-storyshots';
import { render } from '@testing-library/react';
const reactTestingLibrarySerializer = {
print: (val, serialize, indent) => serialize(val.container.firstChild),
test: (val) => val && val.hasOwnProperty('container'),
};
initStoryshots({
renderer: render,
snapshotSerializers: [reactTestingLibrarySerializer],
});
```
#### Example with Enzyme
```js
import initStoryshots from '@storybook/addon-storyshots';
import { mount } from 'enzyme';
initStoryshots({
renderer: mount,
});
```
If you are using enzyme, you need to make sure jest knows how to serialize rendered components.
For that, you can pass an enzyme-compatible snapshotSerializer (like [enzyme-to-json](https://github.com/adriantoine/enzyme-to-json), [jest-serializer-enzyme](https://github.com/rogeliog/jest-serializer-enzyme) etc.) with the `snapshotSerializer` option (see below).
### StoryShots for async rendered components
You can make use of [Jest done callback](https://jestjs.io/docs/en/asynchronous) to test components that render asynchronously. This callback is passed as param to test method passed to `initStoryshots(...)` when the `asyncJest` option is given as true.
#### Example
The following example shows how we can use the **done callback** to take StoryShots of a [Relay](http://facebook.github.io/relay/) component. Each kind of story is written into its own snapshot file with the use of `getSnapshotFileName`.
Add _stories of UserForm_ in the file: UserForm.story.jsx
```jsx
/* global module */
import React from 'react';
import { QueryRenderer } from 'react-relay';
import { storiesOf } from '@storybook/react';
// Use the same queries used in YOUR app routes
import { newUserFormQuery, editUserFormQuery } from 'app/routes';
import UserFormContainer from 'app/users/UserForm';
// YOUR function to generate a Relay Environment mock.
// See https://github.com/1stdibs/relay-mock-network-layer for more info
import getEnvironment from 'test/support/relay-environment-mock';
// User test data YOU generated for your tests
import { user } from 'test/support/data/index';
// Use this function to return a new Environment for each story
const Environment = () =>
getEnvironment({
mocks: {
Node: () => ({ __typename: 'User' }),
User: () => user,
},
});
/**
NOTICE that the QueryRenderer render its children via its render props.
If we don't take the StoryShot async then we will only see the QueryRenderer in the StoryShot.
The following QueryRenderer returns null in the first render (it can be a loading indicator instead in real file) and then when it gets the data to respond to query, it renders again with props containing the data for the Component
*/
const renderStory = (query, environment, variables = {}) => (
<QueryRenderer
environment={environment}
query={query}
variables={variables}
render={({ props, error }) => {
if (error) {
console.error(error);
} else if (props) {
return <UserFormContainer {...props} />;
}
return null;
}}
/>
);
storiesOf('users/UserForm', module)
.add('New User', () => {
const environment = new Environment();
return renderStory(newUserFormQuery, environment);
})
.add('Editing User', () => {
const environment = new Environment();
return renderStory(editUserFormQuery, environment, { id: user.id });
});
```
Then, init Storyshots for async component in the file: StoryShots.test.js
```jsx
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
// Runner
initStoryshots({
asyncJest: true, // this is the option that activates the async behaviour
test: ({
story,
context,
done, // --> callback passed to test method when asyncJest option is true
}) => {
const converter = new Stories2SnapsConverter();
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render();
// mount the story
const tree = mount(storyElement);
// wait until the mount is updated, in our app mostly by Relay
// but maybe something else updating the state of the component
// somewhere
const waitTime = 1;
setTimeout(() => {
if (snapshotFilename) {
expect(toJson(tree.update())).toMatchSpecificSnapshot(snapshotFilename);
}
done();
}, waitTime);
},
// other options here
});
```
NOTICE that When using the `asyncJest: true` option, you also must specify a `test` method that calls the `done()` callback.
This is a really powerful technique to write stories of Relay components because it integrates data fetching with component rendering. So instead of passing data props manually, we can let Relay do the job for us as it does in our application.
Whenever you change your data requirements by adding (and rendering) or (accidentally) deleting fields in your graphql query fragments, you'll get a different snapshot and thus an error in the StoryShot test.
## Using a custom directory
Depending on your project's needs, you can configure the `@storybook/addon-storyshots` to use a custom directory for the snapshots. You can read more about it in the [official docs](https://storybook.js.org/docs/react/writing-tests/snapshot-testing).
## Options
### `config`
The `config` parameter must be a function that helps to configure storybook like the `preview.js` does.
If it's not specified, storyshots will try to use [configPath](#configPath) parameter.
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
config: ({ configure }) =>
configure(() => {
require('../stories/Button.story.js');
}, module),
});
```
### `configPath`
By default, Storyshots assumes the config directory path for your project as below:
- Storybook for React: `.storybook`
- Storybook for React Native: `storybook`
If you are using a different config directory path, you could change it like this:
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
configPath: '.my-storybook-config-dir',
});
```
Or, as a more complex example, if we have a package in our `lerna` project called `app` with the path `./packages/app/src/__tests__/storyshots.js` and the storybook config directory `./packages/app/.storybook`:
```js
import path from 'path';
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({ configPath: path.resolve(__dirname, '../../.storybook') });
```
`configPath` can also specify path to the `preview.js` itself. In this case, config directory will be
a base directory of the `configPath`. It may be useful when the `preview.js` for test should differ from the
original one. It also may be useful for separating tests to different test configs:
```js
initStoryshots({
configPath: '.my-storybook-config-dir/testConfig1.js',
});
initStoryshots({
configPath: '.my-storybook-config-dir/testConfig2.js',
});
```
### `suite`
By default, Storyshots groups stories inside a Jest test suite called "Storyshots". You could change it like this:
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
suite: 'MyStoryshots',
});
```
### `storyKindRegex`
If you'd like to only run a subset of the stories for your snapshot tests based on the story's kind:
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
storyKindRegex: /^MyComponent$/,
});
```
This can be useful if you want to separate the snapshots in directories next to each component. See an example [here](https://github.com/storybookjs/storybook/issues/892).
If you want to run all stories except stories of a specific kind, you can write an inverse regex which is true for all kinds except those with a specific word such as `DontTest`
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
storyKindRegex: /^((?!.*?DontTest).)*$/,
});
```
This can be useful while testing react components which make use of the findDomNode API since they always fail with snapshot testing
while using react-test-renderer see [here](https://github.com/facebook/react/issues/8324)
### `storyNameRegex`
If you'd like to only run a subset of the stories for your snapshot tests based on the story's name:
```js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
storyNameRegex: /buttons/,
});
```
### `framework`
If you are running tests from outside of your app's directory, storyshots' detection of which framework you are using may fail. Pass `"react"` or `"react-native"` to short-circuit this.
For example:
```js
// storybook.test.js
import path from 'path';
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots({
framework: 'react', // Manually specify the project's framework
configPath: path.join(__dirname, '.storybook'),
integrityOptions: { cwd: path.join(__dirname, 'src', 'stories') },
// Other configurations
});
```
Use this table as a reference for manually specifying the framework.
| angular | html | preact |
| ------- | ------------ | -------------- |
| react | react-native | vue3 |
| svelte | vue | web-components |
### `test`
Run a custom test function for each story, rather than the default (a vanilla snapshot test).
Setting `test` will take precedence over the `renderer` option.
You can still overwrite what renderer is used for the test function:
```js
import initStoryshots, { renderWithOptions } from '@storybook/addon-storyshots';
import { mount } from 'enzyme';
initStoryshots({
test: renderWithOptions({
renderer: mount,
}),
});
```
### `renderer`
Pass a custom renderer (such as enzymes `mount`) to record snapshots.
This may be necessary if you want to use React features that are not supported by the default test renderer,
such as **ref** or **Portals**.
Note that setting `test` overrides `renderer`.
### `snapshotSerializers`
Pass an array of snapshotSerializers to the jest runtime that serializes your story (such as enzyme-to-json).
```js
import initStoryshots from '@storybook/addon-storyshots';
import { createSerializer } from 'enzyme-to-json';
initStoryshots({
renderer: mount,
snapshotSerializers: [createSerializer()],
});
```
This option needs to be set if either:
- the multiSnapshot function is used to create multiple snapshot files (i.e. one per story), since it ignores any serializers specified in your jest config.
- serializers not specified in your jest config should be used when snapshotting stories.
### `serializer` (deprecated)
Pass a custom serializer (such as enzyme-to-json) to serialize components to snapshot-comparable data. The functionality of this option is completely covered by [snapshotSerializers](#snapshotserializers) which should be used instead.
```js
import initStoryshots from '@storybook/addon-storyshots';
import toJSON from 'enzyme-to-json';
initStoryshots({
renderer: mount,
serializer: toJSON,
});
```
This option only needs to be set if the default `snapshotSerializers` is not set in your jest config.
### `stories2snapsConverter`
This parameter should be an instance of the [`Stories2SnapsConverter`](src/Stories2SnapsConverter.js) (or a derived from it) Class that is used to convert story-file name to snapshot-file name and vice versa.
By default, the instance of this class is created with these default options:
```js
{
snapshotsDirName: '__snapshots__',
snapshotExtension: '.storyshot',
storiesExtensions: ['.js', '.jsx', '.ts', '.tsx'],
}
```
This class might be overridden to extend the existing conversion functionality or instantiated to provide different options:
```js
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
initStoryshots({
stories2snapsConverter: new Stories2SnapsConverter({
snapshotExtension: '.storypuke',
storiesExtensions: ['.foo'],
}),
});
```
## Exports
Apart from the default export (`initStoryshots`), Storyshots also exports some named test functions (see the `test` option above):
### `snapshot`
The default, render the story as normal and take a Jest snapshot.
### `renderOnly`
Just render the story, don't check the output at all. This is useful as a low-effort way of smoke testing your
components to ensure they do not error.
### `snapshotWithOptions(options)`
Like the default, but allows you to specify a set of options for the test renderer. [See for example here](https://github.com/storybookjs/storybook/blob/b915b5439786e0edb17d7f5ab404bba9f7919381/examples/test-cra/src/storyshots.test.js#L14-L16).
### `renderWithOptions(options)`
Like the default, but allows you to specify a set of options for the renderer, just like `snapshotWithOptions`.
### `multiSnapshotWithOptions(options)`
Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes. If you'd like the benefit of separate snapshot files, but don't have custom options to pass, you can pass an empty object.
If you use [Component Story Format](https://storybook.js.org/docs/react/api/csf), you may also need to add an additional Jest transform to automate detecting story file names:
```js
// jest.config.js
export default {
transform: {
'^.+\\.stories\\.jsx?$': '@storybook/addon-storyshots/injectFileName',
'^.+\\.jsx?$': 'babel-jest',
},
};
```
#### integrityOptions
This option is useful when running test with `multiSnapshotWithOptions(options)` in order to track snapshots are matching the stories. (disabled by default).
The value is a [settings](https://github.com/isaacs/node-glob#options) to a `glob` object, that searches for the snapshot files.
```js
initStoryshots({
integrityOptions: { cwd: __dirname }, // it will start searching from the current directory
test: multiSnapshotWithOptions(),
});
```
### `shallowSnapshot`
Take a snapshot of a shallow-rendered version of the component. Note that this option will be overridden if you pass a `renderer` option.
### `Stories2SnapsConverter`
This is a class that generates snapshot's name based on the story (kind, story & filename) and vice versa.
###### Example:
Let's say we wanted to create a test function for shallow && multi-file snapshots:
```js
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
const converter = new Stories2SnapsConverter();
initStoryshots({
test: ({ story, context }) => {
const snapshotFileName = converter.getSnapshotFileName(context);
const storyElement = story.render();
const shallowTree = shallow(storyElement);
if (snapshotFileName) {
expect(toJson(shallowTree)).toMatchSpecificSnapshot(snapshotFileName);
}
},
});
```
### `asyncJest`
Enables Jest `done()` callback in the StoryShots tests for async testing. See [StoryShots for async rendered components](#storyshots-for-async-rendered-components) for more info.
## Story Parameters
### `disable`
Some stories are difficult or impossible to snapshot, such as those covering components that use external DOM-modifying libraries, and those that deliberately throw errors. It is possible to skip stories like these by giving them a parameter of `storyshots: {disable: true}`. There is also a shorthand for this, `storyshots: false`.
```js
export const Exception = () => {
throw new Error('storyFn threw an error! WHOOPS');
};
Exception.storyName = 'story throws exception';
Exception.parameters = {
storyshots: { disable: true },
};
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

View File

@ -1,23 +0,0 @@
const { ScriptTransformer } = require('@jest/transform');
const getNextTransformer = (fileName, config) => {
const self = config.transform.find(([pattern]) => new RegExp(pattern).test(fileName));
return new ScriptTransformer({
...config,
transform: config.transform.filter((entry) => entry !== self),
});
};
module.exports = {
process(src, fileName, config, { instrument }) {
const transformer = getNextTransformer(fileName, config);
const { code } = transformer.transformSource(fileName, src, instrument);
return `${code};
if(exports.default != null) {
exports.default.parameters = exports.default.parameters || {};
exports.default.parameters.fileName = '${fileName.replace(/\\/g, '\\\\')}';
}
`;
},
};

View File

@ -1,12 +0,0 @@
const path = require('path');
const baseConfig = require('../../jest.config.browser');
module.exports = {
...baseConfig,
snapshotSerializers: [...baseConfig.snapshotSerializers, 'enzyme-to-json/serializer'],
transform: {
...baseConfig.transform,
'^.+\\.stories\\.[jt]sx?$': '@storybook/addon-storyshots/injectFileName',
},
displayName: __dirname.split(path.sep).slice(-2).join(path.posix.sep),
};

View File

@ -1,153 +0,0 @@
{
"name": "@storybook/addon-storyshots",
"version": "7.6.0-alpha.0",
"description": "Take a code snapshot of every story automatically with Jest",
"keywords": [
"addon",
"storybook",
"test"
],
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/addons/storyshots-core",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "code/addons/storyshots-core"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.mjs",
"*.d.ts",
"!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
"prep": "../../../scripts/prepare/tsc.ts"
},
"dependencies": {
"@jest/transform": "^29.3.1",
"@storybook/babel-plugin-require-context-hook": "1.0.1",
"@storybook/client-api": "workspace:*",
"@storybook/core-common": "workspace:*",
"@storybook/core-webpack": "workspace:*",
"@storybook/global": "^5.0.0",
"@storybook/preview-api": "workspace:*",
"@storybook/types": "workspace:*",
"@types/jest-specific-snapshot": "^0.5.6",
"glob": "^10.0.0",
"jest-specific-snapshot": "^8.0.0",
"preact-render-to-string": "^5.1.19",
"pretty-format": "^29.0.0",
"react-test-renderer": "^16.8.0 || ^17.0.0 || ^18.0.0",
"read-pkg-up": "^7.0.1",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"@angular/core": "^16.0.0-rc.4",
"@angular/platform-browser-dynamic": "^16.0.0-rc.4",
"@emotion/jest": "^11.8.0",
"@storybook/addon-docs": "workspace:*",
"@storybook/angular": "workspace:*",
"@storybook/react": "workspace:*",
"@storybook/vue": "workspace:*",
"@storybook/vue3": "workspace:*",
"babel-loader": "^9.1.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"enzyme-to-json": "^3.6.1",
"jest-preset-angular": "^13.0.1",
"jest-vue-preprocessor": "^1.7.1",
"react-test-renderer": "^16",
"rxjs": "^6.6.3",
"vue-jest": "^5.0.0-alpha.8"
},
"peerDependencies": {
"@angular/core": ">=13.0.0",
"@angular/platform-browser-dynamic": ">=13.0.0",
"@storybook/angular": "*",
"@storybook/react": "*",
"@storybook/vue": "*",
"@storybook/vue3": "*",
"jest": "*",
"jest-preset-angular": " >= 12.2.3",
"jest-vue-preprocessor": "*",
"preact": "^10.5.13",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"rxjs": "*",
"svelte": "*",
"vue": "*",
"vue-jest": "*"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
},
"@angular/platform-browser-dynamic": {
"optional": true
},
"@storybook/angular": {
"optional": true
},
"@storybook/react": {
"optional": true
},
"@storybook/vue": {
"optional": true
},
"@storybook/vue3": {
"optional": true
},
"jest-preset-angular": {
"optional": true
},
"jest-vue-preprocessor": {
"optional": true
},
"preact": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"rxjs": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
},
"vue-jest": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"bundler": {},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17",
"storybook": {
"displayName": "Storyshots",
"icon": "https://user-images.githubusercontent.com/263385/101991676-48cdf300-3c7c-11eb-8aa1-944dab6ab29b.png",
"unsupportedFrameworks": [
"ember"
]
}
}

View File

@ -1,4 +0,0 @@
// storyshots is not a typical addon because it's just a command-line tool
// nevertheless if you add it to .storybook/main.js it shouldn't complain
// https://github.com/storybookjs/storybook/issues/7959
module.exports = {};

View File

@ -1,6 +0,0 @@
{
"name": "@storybook/addon-storyshots",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"implicitDependencies": [],
"type": "library"
}

View File

@ -1,52 +0,0 @@
import { Stories2SnapsConverter } from './Stories2SnapsConverter';
const target = new Stories2SnapsConverter();
describe('getSnapshotFileName', () => {
it('fileName is provided - snapshot is stored in __snapshots__ dir', () => {
const context = { fileName: 'foo.js', kind: 'kind' };
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result?.replace(/\\|\//g, '/');
// This is an absolute path, so we need to use `toContain()`
expect(platformAgnosticResult).toContain('__snapshots__/foo.storyshot');
});
it('fileName with multiple extensions is provided - only the last extension is replaced', () => {
const context = { fileName: 'foo.web.stories.js', kind: 'kind' };
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result?.replace(/\\|\//g, '/');
// This is an absolute path, so we need to use `toContain()`
expect(platformAgnosticResult).toContain('__snapshots__/foo.web.stories.storyshot');
});
it('fileName with dir is provided - __snapshots__ dir is created inside another dir', () => {
const context = { fileName: 'test/foo.js', kind: 'kind' };
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result?.replace(/\\|\//g, '/');
// This is an absolute path, so we need to use `toContain()`
expect(platformAgnosticResult).toContain('test/__snapshots__/foo.storyshot');
});
});
describe('getPossibleStoriesFiles', () => {
it('storyshots is provided and all the posible stories file names are returned', () => {
const storyshots = 'test/__snapshots__/foo.web.stories.storyshot';
const result = target.getPossibleStoriesFiles(storyshots);
const platformAgnosticResult = result.map((path) => path.replace(/\\|\//g, '/'));
expect(platformAgnosticResult).toEqual([
'test/foo.web.stories.js',
'test/foo.web.stories.jsx',
'test/foo.web.stories.ts',
'test/foo.web.stories.tsx',
'test/foo.web.stories.mdx',
]);
});
});

View File

@ -1,76 +0,0 @@
import path from 'path';
import { dedent } from 'ts-dedent';
const defaultOptions: Stories2SnapsConverterOptions = {
snapshotsDirName: '__snapshots__',
snapshotExtension: '.storyshot',
storiesExtensions: ['.js', '.jsx', '.ts', '.tsx', '.mdx'],
};
export interface Stories2SnapsConverterOptions {
storiesExtensions: string[];
snapshotExtension: string;
snapshotsDirName: string;
}
export class Stories2SnapsConverter {
options: Stories2SnapsConverterOptions;
constructor(options: Partial<Stories2SnapsConverterOptions> = {}) {
this.options = {
...defaultOptions,
...options,
};
}
getSnapshotExtension = () => this.options.snapshotExtension;
getStoryshotFile(fileName: string) {
const { dir, name } = path.parse(fileName);
const { snapshotsDirName, snapshotExtension } = this.options;
// Convert to absolute path, in case jest is not running in CWD,
// else it will create snapshots with the wrong path
const absDir = path.isAbsolute(dir) ? dir : path.resolve(dir);
return path.format({
dir: path.join(absDir, snapshotsDirName),
name,
ext: snapshotExtension,
});
}
getSnapshotFileName(context: { fileName?: string; kind: any }) {
const { fileName, kind } = context;
if (!fileName) {
// eslint-disable-next-line no-console
console.warn(
dedent`
Storybook was unable to detect filename for stories of kind "${kind}".
To fix it, add following to your jest.config.js:
transform: {
// should be above any other js transform like babel-jest
'^.+\\.stories\\.js$': '@storybook/addon-storyshots/injectFileName',
}
`
);
return undefined;
}
return this.getStoryshotFile(fileName);
}
getPossibleStoriesFiles(storyshotFile: string) {
const { dir, name } = path.parse(storyshotFile);
const { storiesExtensions } = this.options;
return storiesExtensions.map((ext) =>
path.format({
dir: path.dirname(dir),
name,
ext,
})
);
}
}

View File

@ -1,42 +0,0 @@
import type { GlobOptionsWithFileTypesFalse } from 'glob';
import type { Stories2SnapsConverter } from '../Stories2SnapsConverter';
import type { SupportedFramework } from '../frameworks';
import type { RenderTree } from '../frameworks/Loader';
export interface TestMethodOptions {
story: any;
context: any;
renderTree: RenderTree;
renderShallowTree: RenderTree;
stories2snapsConverter: Stories2SnapsConverter;
snapshotFileName?: string;
options: any;
done?: () => void;
}
export interface StoryshotsTestMethod {
(args: TestMethodOptions): any;
beforeAll?: () => void | Promise<void>;
beforeEach?: () => void | Promise<void>;
afterAll?: () => void | Promise<void>;
afterEach?: () => void | Promise<void>;
}
export interface StoryshotsOptions {
asyncJest?: boolean;
config?: (options: any) => void;
integrityOptions?: GlobOptionsWithFileTypesFalse | false;
configPath?: string;
suite?: string;
storyKindRegex?: RegExp | string;
storyNameRegex?: RegExp | string;
framework?: SupportedFramework;
test?: StoryshotsTestMethod;
renderer?: Function;
snapshotSerializers?: jest.SnapshotSerializerPlugin[];
/**
* @Deprecated The functionality of this option is completely covered by snapshotSerializers which should be used instead.
*/
serializer?: any;
stories2snapsConverter?: Stories2SnapsConverter;
}

View File

@ -1,55 +0,0 @@
import { snapshotWithOptions } from '../test-bodies';
import { Stories2SnapsConverter } from '../Stories2SnapsConverter';
import type { StoryshotsOptions } from './StoryshotsOptions';
const ignore = ['**/node_modules/**'];
const defaultStories2SnapsConverter = new Stories2SnapsConverter();
function getIntegrityOptions({ integrityOptions }: StoryshotsOptions) {
if (integrityOptions === false) {
return false;
}
if (typeof integrityOptions !== 'object') {
return false;
}
const ignoreOption: string[] = Array.isArray(integrityOptions.ignore)
? integrityOptions.ignore
: [];
return {
...integrityOptions,
ignore: [...ignore, ...ignoreOption],
absolute: true,
};
}
function ensureOptionsDefaults(options: StoryshotsOptions) {
const {
suite = 'Storyshots',
asyncJest,
storyNameRegex,
storyKindRegex,
renderer,
serializer,
snapshotSerializers,
stories2snapsConverter = defaultStories2SnapsConverter,
test: testMethod = snapshotWithOptions({ renderer, serializer }),
} = options;
const integrityOptions = getIntegrityOptions(options);
return {
asyncJest,
suite,
storyNameRegex,
storyKindRegex,
stories2snapsConverter,
testMethod,
snapshotSerializers,
integrityOptions,
} as any;
}
export default ensureOptionsDefaults;

View File

@ -1,109 +0,0 @@
import { global } from '@storybook/global';
import { addons, mockChannel } from '@storybook/preview-api';
import ensureOptionsDefaults from './ensureOptionsDefaults';
import snapshotsTests from './snapshotsTestsTemplate';
import integrityTest from './integrityTestTemplate';
import loadFramework from '../frameworks/frameworkLoader';
import type { StoryshotsOptions } from './StoryshotsOptions';
const { describe, window: globalWindow } = global;
type TestMethod = 'beforeAll' | 'beforeEach' | 'afterEach' | 'afterAll';
const methods: TestMethod[] = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];
function callTestMethodGlobals(
testMethod: { [key in TestMethod]?: Function & { timeout?: number } } & { [key in string]: any }
) {
methods.forEach((method) => {
if (typeof testMethod[method] === 'function') {
// @ts-expect-error (ignore)
global[method](testMethod[method], testMethod[method].timeout);
}
});
}
const isDisabled = (parameter: any) =>
parameter === false || (parameter && parameter.disable === true);
function testStorySnapshots(options: StoryshotsOptions = {}) {
if (typeof describe !== 'function') {
throw new Error('testStorySnapshots is intended only to be used inside jest');
}
addons.setChannel(mockChannel());
const { storybook, framework, renderTree, renderShallowTree } = loadFramework(options);
const {
asyncJest,
suite,
storyNameRegex,
storyKindRegex,
stories2snapsConverter,
testMethod,
integrityOptions,
snapshotSerializers,
} = ensureOptionsDefaults(options);
const testMethodParams = {
renderTree,
renderShallowTree,
stories2snapsConverter,
};
// NOTE: as the store + preview's initialization process entirely uses
// `SychronousPromise`s in the v6 store case, the callback to the `then()` here
// will run *immediately* (in the same tick), and thus the `snapshotsTests`, and
// subsequent calls to `it()` etc will all happen within this tick, which is required
// by Jest (cannot add tests asynchronously)
globalWindow.__STORYBOOK_STORY_STORE__.initializationPromise.then(() => {
const data = storybook.raw()?.reduce(
(acc, item) => {
if (storyNameRegex && !item.name.match(storyNameRegex)) {
return acc;
}
if (storyKindRegex && !item.kind.match(storyKindRegex)) {
return acc;
}
const { kind, storyFn: render, parameters } = item;
const existing = acc.find((i: any) => i.kind === kind);
const { fileName } = item.parameters;
if (!isDisabled(parameters.storyshots)) {
if (existing) {
existing.children.push({ ...item, render, fileName });
} else {
acc.push({
kind,
children: [{ ...item, render, fileName }],
});
}
}
return acc;
},
[] as {
kind: string;
children: any[];
}[]
);
if (data && data.length) {
callTestMethodGlobals(testMethod);
snapshotsTests({
data,
asyncJest,
suite,
framework,
testMethod,
testMethodParams,
snapshotSerializers,
});
integrityTest(integrityOptions, stories2snapsConverter);
} else {
throw new Error('storyshots found 0 stories');
}
});
}
export default testStorySnapshots;

View File

@ -1,61 +0,0 @@
/* eslint-disable jest/no-export */
import fs from 'fs';
import { globSync } from 'glob';
import { global } from '@storybook/global';
import { dedent } from 'ts-dedent';
const { describe, it } = global;
expect.extend({
notToBeAbandoned(storyshots, stories2snapsConverter) {
const abandonedStoryshots = storyshots.filter((fileName: string) => {
const possibleStoriesFiles = stories2snapsConverter.getPossibleStoriesFiles(fileName);
return !possibleStoriesFiles.some(fs.existsSync);
});
if (abandonedStoryshots.length === 0) {
return { pass: true, message: () => '' };
}
const formattedList = abandonedStoryshots.join('\n ');
// See https://github.com/facebook/jest/issues/8732#issuecomment-516445064
// eslint-disable-next-line no-underscore-dangle
const isUpdate = expect.getState().snapshotState._updateSnapshot === 'all';
if (isUpdate) {
abandonedStoryshots.forEach((file: string) => fs.unlinkSync(file));
// eslint-disable-next-line no-console
console.log(dedent`
Removed abandoned storyshots:
${formattedList}
`);
return { pass: true, message: () => '' };
}
return {
pass: false,
message: () => dedent`
Found abandoned storyshots. Run jest with -u to remove them:
${formattedList}
`,
};
},
});
function integrityTest(integrityOptions: any, stories2snapsConverter: any) {
if (integrityOptions === false) {
return;
}
describe('Storyshots Integrity', () => {
it('Abandoned Storyshots', () => {
const snapshotExtension = stories2snapsConverter.getSnapshotExtension();
const storyshots = globSync(`**/*${snapshotExtension}`, integrityOptions);
// @ts-expect-error (ts doesn't 'get' the extension happening on line 9)
expect(storyshots).notToBeAbandoned(stories2snapsConverter);
});
});
}
export default integrityTest;

View File

@ -1,64 +0,0 @@
/* eslint-disable jest/no-export */
/* eslint-disable jest/expect-expect */
import { global } from '@storybook/global';
import { addSerializer } from 'jest-specific-snapshot';
const { describe, it } = global;
function snapshotTest({ item, asyncJest, framework, testMethod, testMethodParams }: any) {
const { name } = item;
const context = { ...item, framework };
if (asyncJest === true) {
it(
`${name}`,
() =>
new Promise<void>((resolve, reject) =>
testMethod({
done: (error: any) => (error ? reject(error) : resolve()),
story: item,
context,
...testMethodParams,
})
),
testMethod.timeout
);
} else {
it(
`${name}`,
() =>
testMethod({
story: item,
context,
...testMethodParams,
}),
testMethod.timeout
);
}
}
function snapshotTestSuite({ item, suite, ...restParams }: any) {
const { kind, children } = item;
describe(`${suite}`, () => {
describe(`${kind}`, () => {
children.forEach((c: any) => {
snapshotTest({ item: c, ...restParams });
});
});
});
}
function snapshotsTests({ data, snapshotSerializers, ...restParams }: any) {
if (snapshotSerializers) {
snapshotSerializers.forEach((serializer: any) => {
addSerializer(serializer);
expect.addSnapshotSerializer(serializer);
});
}
data.forEach((item: any) => {
snapshotTestSuite({ item, ...restParams });
});
}
export default snapshotsTests;

View File

@ -1,25 +0,0 @@
import type { Renderer, Addon_Loadable } from '@storybook/types';
import type { ClientApi as ClientApiClass } from '@storybook/preview-api';
import type { StoryshotsOptions } from '../api/StoryshotsOptions';
import type { SupportedFramework } from './SupportedFramework';
export type RenderTree = (story: any, context?: any, options?: any) => any;
export interface ClientApi<TRenderer extends Renderer> extends ClientApiClass<Renderer> {
configure(
loader: Addon_Loadable,
module: NodeModule | false,
showDeprecationWarning?: boolean
): void;
forceReRender(): void;
}
export interface Loader {
load: (options: StoryshotsOptions) => {
framework: SupportedFramework;
renderTree: RenderTree;
renderShallowTree: any;
storybook: ClientApi<Renderer>;
};
test: (options: StoryshotsOptions) => boolean;
}

View File

@ -1,10 +0,0 @@
export type SupportedFramework =
| 'angular'
| 'html'
| 'preact'
| 'react'
| 'react-native'
| 'svelte'
| 'vue'
| 'vue3'
| 'web-components';

View File

@ -1,70 +0,0 @@
import hasDependency from '../hasDependency';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function setupAngularJestPreset() {
// Angular + Jest + Storyshots = Crazy Shit:
// We need to require 'jest-preset-angular/build/setupJest' before any storybook code
// is running inside jest - one of the things that `jest-preset-angular/build/setupJest` does is
// extending the `window.Reflect` with all the needed metadata functions, that are required
// for emission of the TS decorations like 'design:paramtypes'
jest.requireActual('jest-preset-angular/setup-jest');
}
function test(options: StoryshotsOptions): boolean {
return (
options.framework === 'angular' || (!options.framework && hasDependency('@storybook/angular'))
);
}
function load(options: StoryshotsOptions) {
setupAngularJestPreset();
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/angular', () => {
const renderAPI = jest.requireActual('@storybook/angular');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/angular');
configure({
...options,
storybook,
});
return {
framework: 'angular' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for angular');
},
storybook,
};
}
const angularLoader: Loader = {
load,
test,
};
export default angularLoader;

View File

@ -1,38 +0,0 @@
import AngularSnapshotSerializer from 'jest-preset-angular/build/serializers/ng-snapshot';
import HTMLCommentSerializer from 'jest-preset-angular/build/serializers/html-comment';
import { TestBed } from '@angular/core/testing';
import { addSerializer } from 'jest-specific-snapshot';
import { getApplication, storyPropsProvider, PropertyExtractor } from '@storybook/angular/renderer';
import { BehaviorSubject } from 'rxjs';
addSerializer(HTMLCommentSerializer);
addSerializer(AngularSnapshotSerializer);
function getRenderedTree(story: any) {
const currentStory = story.render();
const analyzedMetadata = new PropertyExtractor(currentStory.moduleMetadata, story.component);
const application = getApplication({
storyFnAngular: currentStory,
component: story.component,
// TODO : To change with the story Id in v7. Currently keep with static id to avoid changes in snapshots
targetSelector: 'storybook-wrapper',
analyzedMetadata,
});
TestBed.configureTestingModule({
imports: [application],
providers: [storyPropsProvider(new BehaviorSubject(currentStory.props))],
});
return TestBed.compileComponents().then(() => {
const tree = TestBed.createComponent(application);
tree.detectChanges();
// Empty componentInstance remove attributes of the internal main component (<storybook-wrapper>) in snapshot
return { ...tree, componentInstance: {} };
});
}
export default getRenderedTree;

View File

@ -1,17 +0,0 @@
export interface NgModuleMetadata {
declarations?: any[];
entryComponents?: any[];
imports?: any[];
schemas?: any[];
providers?: any[];
}
export interface ICollection {
[p: string]: any;
}
export interface NgStory {
props: ICollection;
moduleMetadata?: NgModuleMetadata;
template?: string;
}

View File

@ -1,53 +0,0 @@
import path from 'path';
import { getPreviewFile, getMainFile } from './configure';
// eslint-disable-next-line global-require, jest/no-mocks-import
jest.mock('fs', () => require('../../../../__mocks__/fs'));
const setupFiles = (files: Record<string, string>) => {
// eslint-disable-next-line no-underscore-dangle, global-require
require('fs').__setMockFiles(files);
};
describe('preview files', () => {
it.each`
filepath
${'preview.ts'}
${'preview.tsx'}
${'preview.js'}
${'preview.jsx'}
${'config.ts'}
${'config.tsx'}
${'config.js'}
${'config.jsx'}
`('resolves a valid preview file from $filepath', ({ filepath }) => {
setupFiles({ [path.join('test', filepath)]: 'true' });
expect(getPreviewFile('test/')).toEqual(`test${path.sep}${filepath}`);
});
it('returns false when none of the paths exist', () => {
setupFiles(Object.create(null));
expect(getPreviewFile('test/')).toEqual(false);
});
});
describe('main files', () => {
it.each`
filepath
${'main.ts'}
${'main.tsx'}
${'main.js'}
${'main.jsx'}
`('resolves a valid main file path from $filepath', ({ filepath }) => {
setupFiles({ [path.join('test', filepath)]: 'true' });
expect(getMainFile('test/')).toEqual(`test${path.sep}${filepath}`);
});
it('returns false when none of the paths exist', () => {
setupFiles(Object.create(null));
expect(getPreviewFile('test/')).toEqual(false);
});
});

View File

@ -1,157 +0,0 @@
import fs from 'fs';
import path from 'path';
import type {
Renderer,
ArgsEnhancer,
ArgTypesEnhancer,
NormalizedStoriesSpecifier,
StoriesEntry,
DecoratorFunction,
} from '@storybook/types';
import { toRequireContext } from '@storybook/core-webpack';
import { normalizeStoriesEntry } from '@storybook/core-common';
import registerRequireContextHook from '@storybook/babel-plugin-require-context-hook/register';
import { global } from '@storybook/global';
import type { ClientApi } from './Loader';
import type { StoryshotsOptions } from '../api/StoryshotsOptions';
registerRequireContextHook();
const isFile = (file: string): boolean => {
try {
return fs.lstatSync(file).isFile();
} catch (e) {
return false;
}
};
interface Output {
features?: Record<string, boolean>;
preview?: string;
stories?: NormalizedStoriesSpecifier[];
requireContexts?: string[];
}
const supportedExtensions = ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs'];
const resolveFile = (configDir: string, supportedFilenames: string[]) =>
supportedFilenames
.flatMap((filename) =>
supportedExtensions.map((ext) => path.join(configDir, `${filename}.${ext}`))
)
.find(isFile) || false;
export const getPreviewFile = (configDir: string): string | false =>
resolveFile(configDir, ['preview', 'config']);
export const getMainFile = (configDir: string): string | false => resolveFile(configDir, ['main']);
function getConfigPathParts(input: string): Output {
const configDir = path.resolve(input);
if (fs.lstatSync(configDir).isDirectory()) {
const output: Output = {};
const preview = getPreviewFile(configDir);
const main = getMainFile(configDir);
if (preview) {
output.preview = preview;
}
if (main) {
const { default: defaultExport, ...rest } = jest.requireActual(main);
const { stories = [], features = {} } = defaultExport || rest;
output.features = features;
const workingDir = process.cwd();
output.stories = stories.map((entry: StoriesEntry) => {
const specifier = normalizeStoriesEntry(entry, {
configDir,
workingDir,
});
return specifier;
});
output.requireContexts = output.stories?.map((specifier) => {
const { path: basePath, recursive, match } = toRequireContext(specifier);
// eslint-disable-next-line no-underscore-dangle
return global.__requireContext(workingDir, basePath, recursive, match);
});
}
return output;
}
return { preview: configDir };
}
function configure<TRenderer extends Renderer>(
options: {
storybook: ClientApi<TRenderer>;
} & StoryshotsOptions
): void {
const { configPath = '.storybook', config, storybook } = options;
if (config && typeof config === 'function') {
config(storybook);
return;
}
const {
preview,
features = {},
stories = [],
requireContexts = [],
} = getConfigPathParts(configPath);
global.FEATURES = features;
global.CONFIG_TYPE = 'DEVELOPMENT';
global.STORIES = stories.map((specifier) => ({
...specifier,
importPathMatcher: specifier.importPathMatcher.source,
}));
if (preview) {
// This is essentially the same code as builders/builder-webpack5/templates/virtualModuleEntry.template
const {
parameters,
decorators,
globals,
globalTypes,
argsEnhancers,
argTypesEnhancers,
runStep,
} = jest.requireActual(preview);
if (decorators) {
decorators.forEach((decorator: DecoratorFunction<TRenderer>) =>
storybook.addDecorator(decorator)
);
}
if (parameters || globals || globalTypes) {
storybook.addParameters({ ...parameters, globals, globalTypes });
}
if (runStep) {
storybook.addStepRunner(runStep);
}
if (argsEnhancers) {
argsEnhancers.forEach((enhancer: ArgsEnhancer<TRenderer>) =>
storybook.addArgsEnhancer(enhancer as any)
);
}
if (argTypesEnhancers) {
argTypesEnhancers.forEach((enhancer: ArgTypesEnhancer<TRenderer>) =>
storybook.addArgTypesEnhancer(enhancer as any)
);
}
}
if (requireContexts && requireContexts.length) {
storybook.configure(requireContexts, false, false);
}
}
export default configure;

View File

@ -1,48 +0,0 @@
/* eslint-disable global-require,import/no-dynamic-require */
import fs from 'fs';
import path from 'path';
import type { Loader } from './Loader';
import type { StoryshotsOptions } from '../api/StoryshotsOptions';
const loaderScriptName = 'loader.js';
const isDirectory = (source: string) => fs.lstatSync(source).isDirectory();
function getLoaders(): Loader[] {
return fs
.readdirSync(__dirname)
.map((name) => path.join(__dirname, name))
.filter(isDirectory)
.map((framework) => {
const pa = path.join(framework, loaderScriptName);
const pb = path.join(framework, 'loader.ts');
if (fs.existsSync(pa)) {
return pa;
}
if (fs.existsSync(pb)) {
return pb;
}
return null;
})
.filter(Boolean)
.map((loader) => loader && require(loader).default);
}
function loadFramework(options: StoryshotsOptions) {
const loaders = getLoaders();
const loader = loaders.find((frameworkLoader) => frameworkLoader.test(options));
if (!loader) {
throw new Error(
"Couldn't find an appropriate framework loader -- do you need to set the `framework` option?"
);
}
return loader.load(options);
}
export default loadFramework;

View File

@ -1,18 +0,0 @@
import fs from 'fs';
import path from 'path';
import readPkgUp from 'read-pkg-up';
const {
packageJson: { dependencies, devDependencies } = {
dependencies: undefined,
devDependencies: undefined,
},
} = readPkgUp.sync() || {};
export default function hasDependency(name: string): boolean {
return Boolean(
(devDependencies && devDependencies[name]) ||
(dependencies && dependencies[name]) ||
fs.existsSync(path.join('node_modules', name, 'package.json'))
);
}

View File

@ -1,59 +0,0 @@
import { global } from '@storybook/global';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return options.framework === 'html';
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'html';
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/html', () => {
const renderAPI = jest.requireActual('@storybook/html');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/html');
configure({
...options,
storybook,
});
return {
framework: 'html' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for HTML');
},
storybook,
};
}
const htmLoader: Loader = {
load,
test,
};
export default htmLoader;

View File

@ -1,22 +0,0 @@
import { global } from '@storybook/global';
const { document, Node } = global;
function getRenderedTree(story: { render: () => any }) {
const component = story.render();
if (component instanceof Node) {
return component;
}
const section: HTMLElement = document.createElement('section');
section.innerHTML = component;
if (section.childElementCount > 1) {
return section;
}
return section.firstChild;
}
export default getRenderedTree;

View File

@ -1 +0,0 @@
export * from './SupportedFramework';

View File

@ -1,65 +0,0 @@
/** @jsxRuntime classic */
/** @jsx h */
import { global } from '@storybook/global';
import configure from '../configure';
import hasDependency from '../hasDependency';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return (
options.framework === 'preact' || (!options.framework && hasDependency('@storybook/preact'))
);
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'preact';
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/preact', () => {
const renderAPI = jest.requireActual('@storybook/preact');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/preact');
configure({
...options,
storybook,
});
return {
framework: 'preact' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for preact');
},
storybook,
};
}
const preactLoader: Loader = {
load,
test,
};
export default preactLoader;

View File

@ -1,15 +0,0 @@
/** @jsx h */
import { h } from 'preact';
import preactRenderer from 'preact-render-to-string/jsx';
const boundRenderer = (_storyElement: any, _rendererOptions: any) =>
preactRenderer(_storyElement, null, { pretty: ' ' });
function getRenderedTree(story: any, context: any, { renderer, ...rendererOptions }: any) {
const currentRenderer = renderer || boundRenderer;
const tree = currentRenderer(h(story.render, null), rendererOptions);
return tree;
}
export default getRenderedTree;

View File

@ -1,44 +0,0 @@
/* eslint-disable global-require */
import path from 'path';
import hasDependency from '../hasDependency';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return (
options.framework === 'react-native' ||
(!options.framework && hasDependency('@storybook/react-native'))
);
}
function configure(options: StoryshotsOptions, storybook: any) {
const { configPath = 'storybook', config } = options;
if (config && typeof config === 'function') {
config(storybook);
return;
}
const resolvedConfigPath = path.resolve(configPath);
jest.requireActual(resolvedConfigPath);
}
function load(options: StoryshotsOptions) {
const storybook = jest.requireActual('@storybook/react-native');
configure(options, storybook);
return {
renderTree: require('../react/renderTree').default,
renderShallowTree: require('../react/renderShallowTree').default,
framework: 'react-native' as const,
storybook,
};
}
const reactNativeLoader: Loader = {
load,
test,
};
export default reactNativeLoader;

View File

@ -1,55 +0,0 @@
import configure from '../configure';
import hasDependency from '../hasDependency';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return options.framework === 'react' || (!options.framework && hasDependency('@storybook/react'));
}
function load(options: StoryshotsOptions) {
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/react', () => {
const renderAPI = jest.requireActual('@storybook/react');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/react');
configure({
...options,
storybook,
});
return {
framework: 'react' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: jest.requireActual('./renderShallowTree').default,
storybook,
};
}
const reactLoader: Loader = {
load,
test,
};
export default reactLoader;

View File

@ -1,10 +0,0 @@
import shallow from 'react-test-renderer/shallow';
function getRenderedTree(story: any, context: any, { renderer, serializer }: any) {
const storyElement = story.render();
const shallowRenderer = renderer || shallow.createRenderer();
const tree = shallowRenderer.render(storyElement);
return serializer ? serializer(tree) : tree;
}
export default getRenderedTree;

View File

@ -1,13 +0,0 @@
import React from 'react';
import reactTestRenderer from 'react-test-renderer';
function getRenderedTree(story: any, context: any, { renderer, ...rendererOptions }: any) {
const StoryFn = story.render;
const storyElement = React.createElement(StoryFn);
const currentRenderer = renderer || reactTestRenderer.create;
const tree = currentRenderer(storyElement, rendererOptions);
return tree;
}
export default getRenderedTree;

View File

@ -1,61 +0,0 @@
import { global } from '@storybook/global';
import hasDependency from '../hasDependency';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return (
options.framework === 'svelte' || (!options.framework && hasDependency('@storybook/svelte'))
);
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'svelte';
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/svelte', () => {
const renderAPI = jest.requireActual('@storybook/svelte');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/svelte');
configure({
...options,
storybook,
});
return {
framework: 'svelte' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for svelte');
},
storybook,
};
}
const svelteLoader: Loader = {
load,
test,
};
export default svelteLoader;

View File

@ -1,41 +0,0 @@
import { global } from '@storybook/global';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - types are removed in Svelte 4 but it still works. ts-ignore is safer than ts-expect-error because it's not an error in Svelte 3
// eslint-disable-next-line import/no-unresolved
import { set_current_component } from 'svelte/internal';
const { document } = global;
/**
* Provides functionality to convert your raw story to the resulting markup.
*
* Storybook snapshots need the rendered markup that svelte outputs,
* but since we only have the story config data ({ Component, data }) in
* the Svelte stories, we need to mount the component, and then return the
* resulting HTML.
*
* If we don't render to HTML, we will get a snapshot of the raw story
* i.e. ({ Component, data }).
*/
function getRenderedTree(story: any) {
// allow setContext to work
set_current_component({ $$: { context: new Map() } });
const { Component, props } = story.render();
const DefaultCompatComponent = Component.default || Component;
// We need to create a target to mount onto.
const target = document.createElement('section');
// eslint-disable-next-line no-new
new DefaultCompatComponent({ target, props });
// Classify the target so that it is clear where the markup
// originates from, and that it is specific for snapshot tests.
target.className = 'storybook-snapshot-container';
return target;
}
export default getRenderedTree;

View File

@ -1,64 +0,0 @@
import { global } from '@storybook/global';
import hasDependency from '../hasDependency';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function mockVueToIncludeCompiler() {
jest.mock('vue', () => jest.requireActual('vue/dist/vue.common.js'));
}
function test(options: StoryshotsOptions): boolean {
return options.framework === 'vue' || (!options.framework && hasDependency('@storybook/vue'));
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'vue';
mockVueToIncludeCompiler();
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/vue', () => {
const renderAPI = jest.requireActual('@storybook/vue');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/vue');
configure({
...options,
storybook,
});
return {
framework: 'vue' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for vue');
},
storybook,
};
}
const vueLoader: Loader = {
load,
test,
};
export default vueLoader;

View File

@ -1,25 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import Vue from 'vue';
// this is defined in @storybook/vue but not exported,
// and we need it to inject args into the story component's props
const VALUES = 'STORYBOOK_VALUES';
function getRenderedTree(story: any) {
const component = story.render();
// @ts-ignore FIXME storyshots type error
const vm = new Vue({
// @ts-ignore FIXME storyshots type error
render(h) {
return h(component);
},
});
// @ts-ignore FIXME storyshots type error
vm[VALUES] = story.initialArgs;
return vm.$mount().$el;
}
export default getRenderedTree;

View File

@ -1,60 +0,0 @@
import { global } from '@storybook/global';
import hasDependency from '../hasDependency';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return options.framework === 'vue3' || (!options.framework && hasDependency('@storybook/vue3'));
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'vue3';
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/vue3', () => {
const renderAPI = jest.requireActual('@storybook/vue3');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/vue3');
configure({
...options,
storybook,
});
return {
framework: 'vue3' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for Vue 3');
},
storybook,
};
}
const vueLoader: Loader = {
load,
test,
};
export default vueLoader;

View File

@ -1,23 +0,0 @@
import * as Vue from 'vue';
import { global } from '@storybook/global';
const { document } = global;
// This is cast as `any` to workaround type errors caused by Vue 2 types
const { h, createApp } = Vue as any;
function getRenderedTree(story: any) {
const component = story.render();
const app = createApp({
render() {
return h(component, story.args);
},
});
const vm = app.mount(document.createElement('div'));
vm.$forceUpdate();
return vm.$el;
}
export default getRenderedTree;

View File

@ -1,59 +0,0 @@
import { global } from '@storybook/global';
import configure from '../configure';
import type { Loader } from '../Loader';
import type { StoryshotsOptions } from '../../api/StoryshotsOptions';
function test(options: StoryshotsOptions): boolean {
return options.framework === 'web-components';
}
function load(options: StoryshotsOptions) {
global.STORYBOOK_ENV = 'web-components';
let mockStartedAPI: any;
jest.mock('@storybook/preview-api', () => {
const previewAPI = jest.requireActual('@storybook/preview-api');
return {
...previewAPI,
start: (...args: any[]) => {
mockStartedAPI = previewAPI.start(...args);
return mockStartedAPI;
},
};
});
jest.mock('@storybook/html', () => {
const renderAPI = jest.requireActual('@storybook/html');
renderAPI.addDecorator = mockStartedAPI.clientApi.addDecorator;
renderAPI.addParameters = mockStartedAPI.clientApi.addParameters;
return renderAPI;
});
// eslint-disable-next-line global-require
const storybook = require('@storybook/html');
configure({
...options,
storybook,
});
return {
framework: 'web-components' as const,
renderTree: jest.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for web-components');
},
storybook,
};
}
const webComponentsLoader: Loader = {
load,
test,
};
export default webComponentsLoader;

View File

@ -1,6 +0,0 @@
function getRenderedTree(story: { render: () => any }) {
const component = story.render();
return component.getHTML ? component.getHTML() : component;
}
export default getRenderedTree;

View File

@ -1,23 +0,0 @@
import api from './api';
import {
snapshotWithOptions,
multiSnapshotWithOptions,
renderOnly,
renderWithOptions,
shallowSnapshot,
snapshot,
} from './test-bodies';
export {
snapshotWithOptions,
multiSnapshotWithOptions,
renderOnly,
renderWithOptions,
shallowSnapshot,
snapshot,
};
export * from './Stories2SnapsConverter';
export * from './frameworks';
export default api;

View File

@ -1,85 +0,0 @@
import 'jest-specific-snapshot';
import type {
StoryshotsTestMethod,
TestMethodOptions,
StoryshotsOptions,
} from './api/StoryshotsOptions';
const isFunction = (obj: any) => !!(obj && obj.constructor && obj.call && obj.apply);
const optionsOrCallOptions = (opts: any, story: any) => (isFunction(opts) ? opts(story) : opts);
type SnapshotsWithOptionsArgType = Pick<StoryshotsOptions, 'renderer' | 'serializer'> | Function;
type SnapshotsWithOptionsReturnType = (
options: Pick<TestMethodOptions, 'story' | 'context' | 'renderTree' | 'snapshotFileName'>
) => any;
export function snapshotWithOptions(
options: SnapshotsWithOptionsArgType = {}
): SnapshotsWithOptionsReturnType {
return ({ story, context, renderTree, snapshotFileName }) => {
const result = renderTree(story, context, optionsOrCallOptions(options, story));
function match(tree: any) {
let target = tree;
const isReact = story.parameters.renderer === 'react';
if (isReact && typeof tree.childAt === 'function') {
target = tree.childAt(0);
}
if (isReact && Array.isArray(tree.children)) {
[target] = tree.children;
}
if (snapshotFileName) {
expect(target).toMatchSpecificSnapshot(snapshotFileName);
} else {
expect(target).toMatchSnapshot();
}
if (typeof tree.unmount === 'function') {
tree.unmount();
}
}
if (typeof result.then === 'function') {
return result.then(match);
}
return match(result);
};
}
export function multiSnapshotWithOptions(
options: SnapshotsWithOptionsArgType = {}
): StoryshotsTestMethod {
return ({ story, context, renderTree, stories2snapsConverter }) => {
const snapshotFileName = stories2snapsConverter.getSnapshotFileName(context);
return snapshotWithOptions(options)({ story, context, renderTree, snapshotFileName });
};
}
export const shallowSnapshot: StoryshotsTestMethod = ({
story,
context,
renderShallowTree,
options = {},
}) => {
const result = renderShallowTree(story, context, options);
expect(result).toMatchSnapshot();
};
export function renderWithOptions(options = {}): StoryshotsTestMethod {
return ({ story, context, renderTree }) => {
const result = renderTree(story, context, options);
if (typeof result.then === 'function') {
return result;
}
return undefined;
};
}
export const renderOnly = renderWithOptions();
export const snapshot = snapshotWithOptions();

View File

@ -1,16 +0,0 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/naming-convention */
declare module 'jest-preset-angular/*';
declare module 'preact-render-to-string/jsx';
declare module 'react-test-renderer*';
declare module '@storybook/babel-plugin-require-context-hook/register';
declare var STORYBOOK_ENV: any;
declare var STORIES: any;
declare var CONFIG_TYPE: 'DEVELOPMENT' | 'PRODUCTION';
declare var FEATURES: import('@storybook/types').StorybookConfig['features'];
declare var __STORYBOOK_STORY_STORE__: any;
declare var __requireContext: any;

View File

@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
Hello button
</button>
suffix
</div>
`;
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
contents
suffix
</div>
`;

View File

@ -1,56 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<Container>
<div>
prefix
<Component>
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
</Component>
suffix
</div>
</Container>
`;
exports[`Storyshots Another Button with text 1`] = `
<Container>
<div>
prefix
<Component>
<button
onClick={[Function]}
>
Hello button
</button>
</Component>
suffix
</div>
</Container>
`;
exports[`Storyshots Text Simple 1`] = `
<Container>
<div>
prefix
<Component>
contents
</Component>
suffix
</div>
</Container>
`;

View File

@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
Hello button
</button>
suffix
</div>
`;
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
contents
suffix
</div>
`;

View File

@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;

View File

@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
<Unknown />
suffix
</div>
`;

View File

@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
Hello button
</button>
suffix
</div>
`;
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
contents
suffix
</div>
`;

View File

@ -1,15 +0,0 @@
/* eslint-disable react/button-has-type */
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
storiesOf('Another Button', module)
.add('with text', () => <button onClick={action('clicked')}>Hello button</button>)
.add('with some emoji', () => (
<button onClick={action('clicked')}>
<span role="img" aria-label="so cool">
😀 😎 👍 💯
</span>
</button>
));

View File

@ -1,5 +0,0 @@
export default {
title: 'Text',
};
export const Simple = () => 'contents';

View File

@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
`;

View File

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with text 1`] = `
<button
onClick={[Function]}
>
Hello button
</button>
`;

View File

@ -1,5 +0,0 @@
const config = {
stories: ['./Text.stories.jsx', './Extra.stories.jsx'],
};
export default config;

View File

@ -1,15 +0,0 @@
/* eslint-disable react/prop-types */
import React from 'react';
const Container = ({ children }) => <div>{children}</div>;
export const decorators = [
(StoryFn, { parameters, globals }) => (
<Container>
{parameters.prefix} <StoryFn /> {globals.suffix}
</Container>
),
];
export const parameters = { prefix: 'prefix' };
export const globals = { suffix: 'suffix' };

View File

@ -1,34 +0,0 @@
import React from 'react';
export const EXPECTED_VALUE = 'THIS IS SO DONE';
export const TIMEOUT = 5;
class AsyncTestComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
value: EXPECTED_VALUE,
});
}, TIMEOUT);
}
render() {
const { value } = this.state;
return <h1>{value}</h1>;
}
}
export default {
title: 'Async',
includeStories: ['WithTimeout'],
};
export const WithTimeout = () => <AsyncTestComponent />;
WithTimeout.storyName = `with ${TIMEOUT}ms timeout simulating async operation`;

View File

@ -1,15 +0,0 @@
/* eslint-disable react/button-has-type */
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
storiesOf('Another Button', module)
.add('with text', () => <button onClick={action('clicked')}>Hello button</button>)
.add('with some emoji', () => (
<button onClick={action('clicked')}>
<span role="img" aria-label="so cool">
😀 😎 👍 💯
</span>
</button>
));

View File

@ -1,5 +0,0 @@
export default {
title: 'Text',
};
export const Simple = () => 'contents';

View File

@ -1,34 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
suffix
</div>
`;
exports[`Storyshots Another Button with text 1`] = `
<div>
prefix
<button
onClick={[Function]}
>
Hello button
</button>
suffix
</div>
`;

View File

@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<button
onClick={[Function]}
>
<span
aria-label="so cool"
role="img"
>
😀 😎 👍 💯
</span>
</button>
`;

View File

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with text 1`] = `
<button
onClick={[Function]}
>
Hello button
</button>
`;

View File

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Text Simple 1`] = `
<div>
prefix
contents
suffix
</div>
`;

View File

@ -1,3 +0,0 @@
module.exports = {
stories: ['./Text.stories.jsx', './Extra.stories.jsx'],
};

View File

@ -1,15 +0,0 @@
/* eslint-disable react/prop-types */
import React from 'react';
const Container = ({ children }) => <div>{children}</div>;
export const decorators = [
(StoryFn, { parameters, globals }) => (
<Container>
{parameters.prefix} <StoryFn /> {globals.suffix}
</Container>
),
];
export const parameters = { prefix: 'prefix' };
export const globals = { suffix: 'suffix' };

View File

@ -1,41 +0,0 @@
import path from 'path';
import { render, screen, waitFor } from '@testing-library/react';
import initStoryshots, { Stories2SnapsConverter } from '../src';
import { EXPECTED_VALUE } from './exported_metadata/Async.stories.jsx';
initStoryshots({
asyncJest: true,
framework: 'react',
integrityOptions: false,
configPath: path.join(__dirname, 'exported_metadata'),
// When async is true we need to provide a test method that
// calls done() when at the end of the test method
test: async ({ story, context, done }) => {
expect(done).toBeDefined();
// This is a storyOf Async (see ./required_with_context/Async.stories)
if (context.kind === 'Async') {
const converter = new Stories2SnapsConverter({ snapshotExtension: '.async.storyshot' });
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render();
// Mount the component
const { container } = render(storyElement);
// The Async component should not contain the expected value
expect(screen.queryByText(EXPECTED_VALUE)).toBeFalsy();
await waitFor(() => {
expect(screen.getByText(EXPECTED_VALUE)).toBeInTheDocument();
expect(container.firstChild).toMatchSpecificSnapshot(snapshotFilename);
});
// finally mark test as done
done();
} else {
// If not async, mark the test as done
done();
}
},
});

View File

@ -1,50 +0,0 @@
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions, Stories2SnapsConverter } from '../src';
/* deprecated and will be removed in Storybook 8.0 */
class AnotherStories2SnapsConverter extends Stories2SnapsConverter {
getSnapshotFileName(context) {
const { fileName, kind, name } = context;
const { dir, name: filename } = path.parse(fileName);
const uniqueName = `${filename}@${kind.replace(/ /g, '-_-')}@${name.replace(/ /g, '-_-')}`;
const { snapshotsDirName, snapshotExtension } = this.options;
return path.format({
dir: path.join(dir, snapshotsDirName),
name: uniqueName,
ext: snapshotExtension,
});
}
getPossibleStoriesFiles(storyshotFile) {
const { dir, name } = path.parse(storyshotFile);
const { storiesExtensions } = this.options;
const [fileName] = name.split('@');
return storiesExtensions.map((ext) =>
path.format({
dir: path.dirname(dir),
name: fileName,
ext,
})
);
}
}
initStoryshots({
framework: 'react',
integrityOptions: { cwd: __dirname },
stories2snapsConverter: new AnotherStories2SnapsConverter({ snapshotExtension: '.boo' }),
config: ({ configure }) =>
configure(
() => {
// eslint-disable-next-line global-require
require('./exported_metadata/Extra.stories.jsx');
},
module,
false
),
test: multiSnapshotWithOptions(),
});

View File

@ -1,7 +0,0 @@
import path from 'path';
import initStoryshots from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'default_export'),
});

View File

@ -1,13 +0,0 @@
import path from 'path';
import { mount, configure } from 'enzyme';
// @ts-expect-error (Converted from ts-ignore)
import Adapter from 'enzyme-adapter-react-16';
import initStoryshots from '../src';
configure({ adapter: new Adapter() });
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
renderer: mount,
});

View File

@ -1,9 +0,0 @@
import path from 'path';
import initStoryshots from '../src';
// jest.mock('@storybook/node-logger');
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
});

View File

@ -1,8 +0,0 @@
import path from 'path';
import initStoryshots, { renderOnly } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
test: renderOnly,
});

View File

@ -1,8 +0,0 @@
import path from 'path';
import initStoryshots, { renderWithOptions } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
test: renderWithOptions({}),
});

View File

@ -1,8 +0,0 @@
import path from 'path';
import initStoryshots, { shallowSnapshot } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
test: shallowSnapshot,
});

View File

@ -1,11 +0,0 @@
import path from 'path';
import initStoryshots, { shallowSnapshot } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
test: (data) =>
shallowSnapshot({
...data,
}),
});

View File

@ -1,8 +0,0 @@
import path from 'path';
import initStoryshots, { snapshotWithOptions } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, 'exported_metadata'),
test: snapshotWithOptions(() => ({})),
});

View File

@ -1,13 +0,0 @@
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions } from '../src';
jest.mock('@storybook/node-logger');
// with react-test-renderer
initStoryshots({
framework: 'react',
// Ignore integrityOptions for async.storyshot because only run when asyncJest is true
integrityOptions: { cwd: __dirname, ignore: ['**/**.async.storyshot'] },
configPath: path.join(__dirname, 'exported_metadata'),
test: multiSnapshotWithOptions(),
});

View File

@ -1,23 +0,0 @@
{
"extends": "./tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["es2020", "dom"],
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"outDir": "dist",
"types": ["node"],
"skipLibCheck": true,
"resolveJsonModule": true,
"allowJs": true,
"pretty": true,
"noErrorTruncation": true,
"listEmittedFiles": false,
"noUnusedLocals": false
},
"include": ["src/**/*", "src/**/*.json"]
}

View File

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"jsx": "preserve",
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"strict": true
},
"include": ["src/**/*.ts"]
}

View File

@ -1,5 +0,0 @@
module.exports = {
settings: {
'import/core-modules': ['puppeteer'],
},
};

View File

@ -1,346 +0,0 @@
# StoryShots + [Puppeteer](https://github.com/GoogleChrome/puppeteer)
## Getting Started
Add the following modules into your app.
```sh
npm install @storybook/addon-storyshots-puppeteer puppeteer --save-dev
```
⚠️ As of Storybook 5.3 `puppeteer` is no longer included in the addon dependencies and must be added to your project directly.
## Configure Storyshots for Puppeteer tests
⚠️ **React-native** is **not supported** by this test function.
When running Puppeteer tests for your stories, you have two options:
- Have a storybook running (ie. accessible via http(s), for instance using `npm run storybook`)
- Have a static build of the storybook (for instance, using `npm run build-storybook`)
Then you will need to reference the storybook URL (`http(s)://...`)
## _puppeteerTest_
Allows to define arbitrary Puppeteer tests as `story.parameters.puppeteerTest` function.
You can either create a new Storyshots instance or edit the one you previously used:
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({ suite: 'Puppeteer storyshots', test: puppeteerTest() });
```
Then, in your stories:
```js
export const myExample = () => {
...
};
myExample.parameters = {
async puppeteerTest(page) {
const element = await page.$('<some-selector>');
await element.click();
expect(something).toBe(something);
},
};
```
This will assume you have a storybook running on at _<http://localhost:6006>_.
Internally here are the steps:
- Launches a Chrome headless using [puppeteer](https://github.com/GoogleChrome/puppeteer)
- Browses each stories (calling _<http://localhost:6006/iframe.html?...>_ URL),
- Runs the `parameters.puppeteerTest` function if it's defined.
### Specifying the storybook URL
If you want to set specific storybook URL, you can specify via the `storybookUrl` parameter, see below:
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({ storybookUrl: 'http://my-specific-domain.com:9010' }),
});
```
The above config will use _<https://my-specific-domain.com:9010>_ for tests. You can also use query parameters in your URL (e.g. for setting a different background for your storyshots, if you use `@storybook/addon-backgrounds`).
### Specifying options to _goto()_ (Puppeteer API)
You might use `getGotoOptions` to specify options when the storybook is navigating to a story (using the `goto` method). Will be passed to [Puppeteer .goto() fn](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options)
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
const getGotoOptions = ({ context, url }) => {
return {
waitUntil: 'networkidle0',
};
};
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({ storybookUrl: 'http://localhost:6006', getGotoOptions }),
});
```
### Customizing browser launch options (Puppeteer API)
You might use the `browserLaunchOptions` to specify options for the default browser instance. Will be passed to [puppeteer.launch()](https://pptr.dev/api/puppeteer.puppeteernode.launch)
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({
storybookUrl: 'https://some-local-ssl-url:7777',
browserLaunchOptions: {
// For ignoring self-signed certificates
ignoreHTTPSErrors: true,
},
}),
});
```
### Specifying custom Chrome executable path (Puppeteer API)
You might use `chromeExecutablePath` to specify the path to a different version of Chrome, without downloading Chromium. Will be passed to [Runs a bundled version of Chromium](https://github.com/GoogleChrome/puppeteer#default-runtime-settings)
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
const chromeExecutablePath = '/usr/local/bin/chrome';
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({ storybookUrl: 'http://localhost:6006', chromeExecutablePath }),
});
```
Alternatively, you may set the `SB_CHROMIUM_PATH` environment variable. If both are set, then `chromeExecutablePath` will take precedence.
### Specifying a custom Puppeteer `browser` instance
You might use the async `getCustomBrowser` function to obtain a custom instance of a Puppeteer `browser` object. This will prevent `storyshots-puppeteer` from creating its own `browser`. It will create and close pages within the `browser`, and it is your responsibility to manage the lifecycle of the `browser` itself.
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
import puppeteer from 'puppeteer';
(async function () {
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({
storybookUrl: 'http://localhost:6006',
getCustomBrowser: () => puppeteer.connect({ browserWSEndpoint: 'ws://yourUrl' }),
}),
});
})();
```
### Customizing a `page` instance
Sometimes, there is a need to customize a page before it calls the `goto` api.
An example of device emulation:
```js
import initStoryshots from '@storybook/addon-storyshots';
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer';
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
function customizePage(page) {
return page.emulate(iPhone);
}
initStoryshots({
suite: 'Puppeteer storyshots',
test: puppeteerTest({
storybookUrl: 'http://localhost:6006',
customizePage,
}),
});
```
### Specifying setup and tests timeout
By default, `@storybook/addon-storyshots-puppeteer` uses 15 second timeouts for browser setup and test functions.
Those can be customized with `setupTimeout` and `testTimeout` parameters.
### Integrate Puppeteer storyshots with regular app
You may want to use another Jest project to run your Puppeteer storyshots as they require more resources: Chrome and Storybook built/served.
### Integrate Puppeteer storyshots with [Create React App](https://github.com/facebookincubator/create-react-app)
You have two options here, you can either:
- Add the storyshots configuration inside any of your `test.js` file. You must ensure you have either a running storybook or a static build available.
- Create a custom test file using Jest outside of the CRA scope:
A more robust approach would be to separate existing test files ran by create-react-app (anything `(test|spec).js` suffixed files) from the test files to run Puppeteer storyshots.
This use case can be achieved by using a custom name for the test file, ie something like `puppeteer-storyshots.runner.js`. This file will contain the `initStoryshots` call with Puppeteer storyshots configuration.
Then you will create a separate script entry in your package.json, for instance
```json
{
"scripts": {
"puppeteer-storyshots": "jest puppeteer-storyshots.runner.js --config path/to/custom/jest.config.json"
}
}
```
Note that you will certainly need a custom config file for Jest as you run it outside of the CRA scope and thus you do not have the built-in config.
Once that's setup, you can run `npm run puppeteer-storyshots`.
### Reminder
Puppeteer launches a web browser (Chrome) internally.
The browser opens a page (either using the static build of storybook or a running instance of Storybook)
If you run your test without either the static build or a running instance, this wont work.
To make sure your tests run against the latest changes of your Storybook, you must keep your static build or running Storybook up-to-date.
This can be achieved by adding a step before running the test ie: `npm run build-storybook && npm run image-snapshots`.
If you run the Puppeteer storyshots against a running Storybook in dev mode, you don't have to worry about the stories being up-to-date because the dev-server is watching changes and rebuilds automatically.
## _axeTest_
Runs [Axe](https://www.deque.com/axe/) accessibility checks and verifies that they pass using [jest-puppeteer-axe](https://github.com/WordPress/gutenberg/tree/master/packages/jest-puppeteer-axe).
```js
import initStoryshots from '@storybook/addon-storyshots';
import { axeTest } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({ suite: 'A11y checks', test: axeTest() });
```
For configuration, it uses the same `story.parameters.a11y` parameter as [`@storybook/addon-a11y`](https://github.com/storybookjs/storybook/tree/next/code/addons/a11y#parameters)
### Specifying options to `axeTest`
```js
import initStoryshots from '@storybook/addon-storyshots';
import { axeTest } from '@storybook/addon-storyshots-puppeteer';
const beforeAxeTest = (page, { context: { kind, story }, url }) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, 600)
);
};
initStoryshots({ suite: 'A11y checks', test: axeTest({ beforeAxeTest }) });
```
`beforeAxeTest` receives the [Puppeteer page instance](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page) and an object: `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot. `beforeAxeTest` is part of the promise chain and is called after the browser navigation is completed but before the screenshot is taken. It allows for triggering events on the page elements and delaying the axe test .
## _imageSnapshots_
Generates and compares screenshots of your stories using [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot).
```js
import initStoryshots from '@storybook/addon-storyshots';
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({ suite: 'Image storyshots', test: imageSnapshot() });
```
It saves all images under \_\_image_snapshots\_\_ folder.
### Specifying options to _jest-image-snapshots_
If you wish to customize [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot), then you can provide a `getMatchOptions` parameter that should return the options config object. Additionally, you can provide `beforeScreenshot` which is called before the screenshot is captured and a `afterScreenshot` handler which is called after the screenshot and receives the just created image.
```js
import initStoryshots from '@storybook/addon-storyshots';
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
const getMatchOptions = ({ context: { kind, story }, url }) => {
return {
failureThreshold: 0.2,
failureThresholdType: 'percent',
};
};
const beforeScreenshot = (page, { context: { kind, story }, url }) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, 600)
);
};
const afterScreenshot = ({ image, context }) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, 600)
);
};
initStoryshots({
suite: 'Image storyshots',
test: imageSnapshot({
storybookUrl: 'http://localhost:6006',
getMatchOptions,
beforeScreenshot,
afterScreenshot,
}),
});
```
`getMatchOptions` receives an object: `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot.
`beforeScreenshot` receives the [Puppeteer page instance](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page) and an object: `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot. `beforeScreenshot` is part of the promise chain and is called after the browser navigation is completed but before the screenshot is taken. It allows for triggering events on the page elements and delaying the screenshot and can be used avoid regressions due to mounting animations.
`afterScreenshot` receives the created image from puppeteer.
### Specifying options to _screenshot()_ (Puppeteer API)
You might use `getScreenshotOptions` to specify options for screenshot. Will be passed to [Puppeteer .screenshot() fn](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions)
```js
import initStoryshots from '@storybook/addon-storyshots';
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
const getScreenshotOptions = ({ context, url }) => {
return {
encoding: 'base64', // encoding: 'base64' is a property required by puppeteer
fullPage: false, // Do not take the full page screenshot. Default is 'true' in Storyshots.,
};
};
initStoryshots({
suite: 'Image storyshots',
test: imageSnapshot({ storybookUrl: 'http://localhost:6006', getScreenshotOptions }),
});
```
`getScreenshotOptions` receives an object `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot.
To create a screenshot of just a single element (with its children), rather than the page or current viewport, an ElementHandle can be returned from `beforeScreenshot`:
```js
import initStoryshots from '@storybook/addon-storyshots';
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
const beforeScreenshot = (page) => page.$('#storybook-root > *');
initStoryshots({
suite: 'Image storyshots',
test: imageSnapshot({ storybookUrl: 'http://localhost:6006', beforeScreenshot }),
});
```

View File

@ -1,12 +0,0 @@
const path = require('path');
const baseConfig = require('../../jest.config.browser');
module.exports = {
...baseConfig,
snapshotSerializers: [...baseConfig.snapshotSerializers, 'enzyme-to-json/serializer'],
transform: {
...baseConfig.transform,
'^.+\\.stories\\.[jt]sx?$': '@storybook/addon-storyshots/injectFileName',
},
displayName: __dirname.split(path.sep).slice(-2).join(path.posix.sep),
};

View File

@ -1,66 +0,0 @@
{
"name": "@storybook/addon-storyshots-puppeteer",
"version": "7.6.0-alpha.0",
"description": "Image snapshots addition to StoryShots based on puppeteer",
"keywords": [
"addon",
"storybook"
],
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/addons/storyshots-puppeteer",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "code/addons/storyshots-puppeteer"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.mjs",
"*.d.ts",
"!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
"prep": "../../../scripts/prepare/tsc.ts"
},
"dependencies": {
"@axe-core/puppeteer": "^4.2.0",
"@storybook/csf": "^0.1.0",
"@storybook/node-logger": "workspace:*",
"@storybook/types": "workspace:*",
"@types/jest-image-snapshot": "^6.0.0",
"jest-image-snapshot": "^6.0.0"
},
"devDependencies": {
"@types/puppeteer": "^5.4.0",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.1",
"puppeteer": "^2.0.0 || ^3.0.0"
},
"peerDependencies": {
"@storybook/addon-storyshots": "workspace:*",
"puppeteer": ">=2.0.0"
},
"peerDependenciesMeta": {
"puppeteer": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"bundler": {},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17"
}

View File

@ -1,4 +0,0 @@
// storyshots is not a typical addon because it's just a command-line tool
// nevertheless if you add it to .storybook/main.js it shouldn't complain
// https://github.com/storybookjs/storybook/issues/7959
module.exports = {};

View File

@ -1,6 +0,0 @@
{
"name": "@storybook/addon-storyshots-puppeteer",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"implicitDependencies": [],
"type": "library"
}

Some files were not shown because too many files have changed in this diff Show More