mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
Merge branch 'release-8-0' into new-icon-library
This commit is contained in:
commit
117bfbdfe2
@ -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
|
||||
|
@ -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',
|
||||
|
@ -7,8 +7,6 @@ logFilters:
|
||||
level: discard
|
||||
- code: YN0076
|
||||
level: discard
|
||||
- level: discard
|
||||
pattern: '@workspace:addons/storyshots-*/'
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -78,6 +78,5 @@ export const Defined = {
|
||||
// parameters: {
|
||||
// docs: { disable: true },
|
||||
// chromatic: { disable: true },
|
||||
// storyshots: { disable: true },
|
||||
// },
|
||||
// };
|
||||
|
@ -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": {
|
||||
|
@ -57,7 +57,7 @@
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~4.9.3"
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -68,7 +68,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
"typescript": "~4.9.3"
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
settings: {
|
||||
'import/core-modules': [
|
||||
'@storybook/angular',
|
||||
'@storybook/html',
|
||||
'@storybook/react',
|
||||
'@storybook/preact',
|
||||
'@storybook/vue',
|
||||
'@storybook/svelte',
|
||||
],
|
||||
},
|
||||
};
|
@ -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)
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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 |
@ -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, '\\\\')}';
|
||||
}
|
||||
`;
|
||||
},
|
||||
};
|
@ -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),
|
||||
};
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
@ -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 = {};
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@storybook/addon-storyshots",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"implicitDependencies": [],
|
||||
"type": "library"
|
||||
}
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export type SupportedFramework =
|
||||
| 'angular'
|
||||
| 'html'
|
||||
| 'preact'
|
||||
| 'react'
|
||||
| 'react-native'
|
||||
| 'svelte'
|
||||
| 'vue'
|
||||
| 'vue3'
|
||||
| 'web-components';
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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;
|
@ -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'))
|
||||
);
|
||||
}
|
@ -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;
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export * from './SupportedFramework';
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -1,6 +0,0 @@
|
||||
function getRenderedTree(story: { render: () => any }) {
|
||||
const component = story.render();
|
||||
return component.getHTML ? component.getHTML() : component;
|
||||
}
|
||||
|
||||
export default getRenderedTree;
|
@ -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;
|
@ -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();
|
16
code/addons/storyshots-core/src/typings.d.ts
vendored
16
code/addons/storyshots-core/src/typings.d.ts
vendored
@ -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;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
));
|
@ -1,5 +0,0 @@
|
||||
export default {
|
||||
title: 'Text',
|
||||
};
|
||||
|
||||
export const Simple = () => 'contents';
|
@ -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>
|
||||
`;
|
@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Another Button with text 1`] = `
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hello button
|
||||
</button>
|
||||
`;
|
@ -1,5 +0,0 @@
|
||||
const config = {
|
||||
stories: ['./Text.stories.jsx', './Extra.stories.jsx'],
|
||||
};
|
||||
|
||||
export default config;
|
@ -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' };
|
@ -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`;
|
@ -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>
|
||||
));
|
@ -1,5 +0,0 @@
|
||||
export default {
|
||||
title: 'Text',
|
||||
};
|
||||
|
||||
export const Simple = () => 'contents';
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Another Button with text 1`] = `
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hello button
|
||||
</button>
|
||||
`;
|
@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Text Simple 1`] = `
|
||||
<div>
|
||||
prefix
|
||||
|
||||
contents
|
||||
|
||||
suffix
|
||||
</div>
|
||||
`;
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
stories: ['./Text.stories.jsx', './Extra.stories.jsx'],
|
||||
};
|
@ -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' };
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
@ -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(),
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
import path from 'path';
|
||||
import initStoryshots from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, 'default_export'),
|
||||
});
|
@ -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,
|
||||
});
|
@ -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'),
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
import path from 'path';
|
||||
import initStoryshots, { renderOnly } from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, 'exported_metadata'),
|
||||
test: renderOnly,
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
import path from 'path';
|
||||
import initStoryshots, { renderWithOptions } from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, 'exported_metadata'),
|
||||
test: renderWithOptions({}),
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
import path from 'path';
|
||||
import initStoryshots, { shallowSnapshot } from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, 'exported_metadata'),
|
||||
test: shallowSnapshot,
|
||||
});
|
@ -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,
|
||||
}),
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
import path from 'path';
|
||||
import initStoryshots, { snapshotWithOptions } from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, 'exported_metadata'),
|
||||
test: snapshotWithOptions(() => ({})),
|
||||
});
|
@ -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(),
|
||||
});
|
@ -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"]
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"jsx": "preserve",
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
settings: {
|
||||
'import/core-modules': ['puppeteer'],
|
||||
},
|
||||
};
|
@ -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 }),
|
||||
});
|
||||
```
|
@ -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),
|
||||
};
|
@ -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"
|
||||
}
|
@ -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 = {};
|
@ -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
Loading…
x
Reference in New Issue
Block a user