mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 11:11:53 +08:00
Merge branch 'next' of github.com:storybookjs/storybook into next
This commit is contained in:
commit
c783b30285
@ -187,7 +187,7 @@ If you've made a change to storybook's codebase and would want this change to be
|
||||
|
||||
### Updating Tests
|
||||
|
||||
Before any contributes are submitted in a PR, make sure to add or update meaningful tests. A PR that has failing tests will be regarded as a “Work in Progress” and will not be merged until all tests pass.
|
||||
Before any contributions are submitted in a PR, make sure to add or update meaningful tests. A PR that has failing tests will be regarded as a “Work in Progress” and will not be merged until all tests pass.
|
||||
When creating new unit test files, the tests should adhere to a particular folder structure and naming convention, as defined below.
|
||||
|
||||
```sh
|
||||
@ -205,7 +205,7 @@ Before you submit a new PR, make sure you run `yarn test`. Do not submit a PR if
|
||||
|
||||
### Reviewing PRs
|
||||
|
||||
**As a PR submitter**, you should reference the issue if there is one, include a short description of what you contributed and, if it is a code change, instructions for how to manually test out the change. This is informally enforced by our [PR template](https://github.com/storybookjs/storybook/blob/master/.github/PULL_REQUEST_TEMPLATE.md). If your PR is reviewed as only needing trivial changes (e.g. small typos etc), and you have commit access, then you can merge the PR after making those changes.
|
||||
**As a PR submitter**, you should reference the issue if there is one, include a short description of what you contributed and, if it is a code change, instructions for how to manually test out the change. This is informally enforced by our [PR template](https://github.com/storybookjs/storybook/blob/master/.github/PULL_REQUEST_TEMPLATE.md). If your PR is reviewed as only needing trivial changes (e.g. small typos etc), and you have commit access then you can merge the PR after making those changes.
|
||||
|
||||
**As a PR reviewer**, you should read through the changes and comment on any potential problems. If you see something cool, a kind word never hurts either! Additionally, you should follow the testing instructions and manually test the changes. If the instructions are missing, unclear, or overly complex, feel free to request better instructions from the submitter. Unless the PR is tagged with the `do not merge` label, if you approve the review and there is no other required discussion or changes, you should also go ahead and merge the PR.
|
||||
|
||||
@ -215,7 +215,7 @@ If you are looking for a way to help the project, triaging issues is a great pla
|
||||
|
||||
### Responding to issues
|
||||
|
||||
Issues that are tagged `question / support` or `needs reproduction` are great places to help. If you can answer a question, it will help the asker as well as anyone searching. If an issue needs reproduction, you may be able to guide the reporter toward one, or even reproduce it yourself using [this technique](https://github.com/storybookjs/storybook/blob/master/CONTRIBUTING.md#reproductions).
|
||||
Issues that are tagged `question / support` or `needs reproduction` are great places to help. If you can answer a question, it will help the asker as well as anyone who has a similar question. Also in the future if anyone has that same question they can easily find it by searching. If an issue needs reproduction, you may be able to guide the reporter toward one, or even reproduce it yourself using [this technique](https://github.com/storybookjs/storybook/blob/master/CONTRIBUTING.md#reproductions).
|
||||
|
||||
### Triaging issues
|
||||
|
||||
|
@ -35,9 +35,11 @@
|
||||
"@jest/transform": "^24.9.0",
|
||||
"@storybook/addons": "5.3.0-beta.6",
|
||||
"@storybook/client-api": "5.3.0-beta.6",
|
||||
"@storybook/core": "5.3.0-beta.6",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^24.0.16",
|
||||
"@types/jest-specific-snapshot": "^0.5.3",
|
||||
"babel-plugin-require-context-hook": "^1.0.0",
|
||||
"core-js": "^3.0.1",
|
||||
"glob": "^7.1.3",
|
||||
"global": "^4.3.2",
|
||||
|
@ -12,11 +12,11 @@ type TestMethod = 'beforeAll' | 'beforeEach' | 'afterEach' | 'afterAll';
|
||||
const methods: TestMethod[] = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];
|
||||
|
||||
function callTestMethodGlobals(
|
||||
testMethod: { [key in TestMethod]?: Function } & { [key in string]: any }
|
||||
testMethod: { [key in TestMethod]?: Function & { timeout?: number } } & { [key in string]: any }
|
||||
) {
|
||||
methods.forEach(method => {
|
||||
if (typeof testMethod[method] === 'function') {
|
||||
global[method](testMethod[method]);
|
||||
global[method](testMethod[method], testMethod[method].timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -18,15 +18,19 @@ function snapshotTest({ item, asyncJest, framework, testMethod, testMethodParams
|
||||
context,
|
||||
...testMethodParams,
|
||||
})
|
||||
)
|
||||
),
|
||||
testMethod.timeout
|
||||
);
|
||||
} else {
|
||||
it(name, () =>
|
||||
testMethod({
|
||||
story: item,
|
||||
context,
|
||||
...testMethodParams,
|
||||
})
|
||||
it(
|
||||
name,
|
||||
() =>
|
||||
testMethod({
|
||||
story: item,
|
||||
context,
|
||||
...testMethodParams,
|
||||
}),
|
||||
testMethod.timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'glob';
|
||||
import { toRequireContext } from '@storybook/core/server';
|
||||
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
|
||||
import global from 'global';
|
||||
import { ClientApi } from './Loader';
|
||||
import { StoryshotsOptions } from '../api/StoryshotsOptions';
|
||||
|
||||
registerRequireContextHook();
|
||||
|
||||
const isFile = (file: string): boolean => {
|
||||
try {
|
||||
return fs.lstatSync(file).isFile();
|
||||
@ -64,9 +68,18 @@ function getConfigPathParts(input: string): Output {
|
||||
if (main) {
|
||||
const { stories = [] } = require.requireActual(main);
|
||||
|
||||
const result = stories.reduce((acc: string[], i: string) => [...acc, ...glob.sync(i)], []);
|
||||
|
||||
output.stories = result;
|
||||
output.stories = stories.map(
|
||||
(pattern: string | { path: string; recursive: boolean; match: string }) => {
|
||||
const { path: basePath, recursive, match } = toRequireContext(pattern);
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return global.__requireContext(
|
||||
configDir,
|
||||
basePath,
|
||||
recursive,
|
||||
new RegExp(match.slice(1, -1))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
@ -94,8 +107,7 @@ function configure(
|
||||
});
|
||||
|
||||
if (stories && stories.length) {
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
storybook.configure(() => stories.map(f => require(f)), false);
|
||||
storybook.configure(stories, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,3 +2,5 @@ declare module 'global';
|
||||
declare module 'jest-preset-angular/*';
|
||||
declare module 'preact-render-to-json';
|
||||
declare module 'react-test-renderer*';
|
||||
declare module '@storybook/core/server';
|
||||
declare module 'babel-plugin-require-context-hook/register';
|
||||
|
@ -67,7 +67,7 @@ initStoryshots({
|
||||
|
||||
### 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.
|
||||
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';
|
||||
@ -85,9 +85,16 @@ const beforeScreenshot = (page, { context: { kind, story }, url }) => {
|
||||
}, 600)
|
||||
);
|
||||
};
|
||||
const afterScreenshot = ({ image, context }) => {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 600)
|
||||
);
|
||||
};
|
||||
initStoryshots({
|
||||
suite: 'Image storyshots',
|
||||
test: imageSnapshot({ storybookUrl: 'http://localhost:6006', getMatchOptions, beforeScreenshot }),
|
||||
test: imageSnapshot({ storybookUrl: 'http://localhost:6006', getMatchOptions, beforeScreenshot, afterScreenshot }),
|
||||
});
|
||||
```
|
||||
|
||||
@ -95,6 +102,8 @@ initStoryshots({
|
||||
|
||||
`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 _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)
|
||||
@ -196,6 +205,11 @@ initStoryshots({
|
||||
});
|
||||
```
|
||||
|
||||
### 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 image storyshots with regular app
|
||||
|
||||
You may want to use another Jest project to run your image snapshots as they require more resources: Chrome and Storybook built/served.
|
||||
|
@ -11,8 +11,11 @@ export interface ImageSnapshotConfig {
|
||||
chromeExecutablePath: string;
|
||||
getMatchOptions: (options: { context: Context; url: string }) => MatchImageSnapshotOptions;
|
||||
getScreenshotOptions: (options: { context: Context; url: string }) => Base64ScreenShotOptions;
|
||||
afterScreenshot: (options: { image: string; context: Context }) => void;
|
||||
beforeScreenshot: (page: Page, options: { context: Context; url: string }) => void;
|
||||
getGotoOptions: (options: { context: Context; url: string }) => DirectNavigationOptions;
|
||||
customizePage: (page: Page) => Promise<void>;
|
||||
getCustomBrowser: () => Promise<Browser>;
|
||||
setupTimeout: number;
|
||||
testTimeout: number;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
import { Browser, Page } from 'puppeteer';
|
||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||
import { logger } from '@storybook/node-logger';
|
||||
import { constructUrl } from './url';
|
||||
@ -18,9 +18,12 @@ const defaultConfig: ImageSnapshotConfig = {
|
||||
getMatchOptions: noop,
|
||||
getScreenshotOptions: defaultScreenshotOptions,
|
||||
beforeScreenshot: noop,
|
||||
afterScreenshot: noop,
|
||||
getGotoOptions: noop,
|
||||
customizePage: asyncNoop,
|
||||
getCustomBrowser: undefined,
|
||||
setupTimeout: 15000,
|
||||
testTimeout: 15000,
|
||||
};
|
||||
|
||||
export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) => {
|
||||
@ -30,9 +33,12 @@ export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) =
|
||||
getMatchOptions,
|
||||
getScreenshotOptions,
|
||||
beforeScreenshot,
|
||||
afterScreenshot,
|
||||
getGotoOptions,
|
||||
customizePage,
|
||||
getCustomBrowser,
|
||||
setupTimeout,
|
||||
testTimeout,
|
||||
} = { ...defaultConfig, ...customConfig };
|
||||
|
||||
let browser: Browser; // holds ref to browser. (ie. Chrome)
|
||||
@ -66,6 +72,7 @@ export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) =
|
||||
await page.goto(url, getGotoOptions({ context, url }));
|
||||
await beforeScreenshot(page, { context, url });
|
||||
image = await page.screenshot(getScreenshotOptions({ context, url }));
|
||||
await afterScreenshot({ image, context });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Error when connecting to ${url}, did you start or build the storybook first? A storybook instance should be running or a static version should be built when using image snapshot feature.`
|
||||
@ -75,19 +82,22 @@ export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) =
|
||||
|
||||
expect(image).toMatchImageSnapshot(getMatchOptions({ context, url }));
|
||||
};
|
||||
testFn.timeout = testTimeout;
|
||||
|
||||
testFn.afterAll = () => {
|
||||
testFn.afterAll = async () => {
|
||||
if (getCustomBrowser && page) {
|
||||
return page.close();
|
||||
await page.close();
|
||||
} else if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
return browser.close();
|
||||
};
|
||||
|
||||
testFn.beforeAll = async () => {
|
||||
const beforeAll = async () => {
|
||||
if (getCustomBrowser) {
|
||||
browser = await getCustomBrowser();
|
||||
} else {
|
||||
// eslint-disable-next-line global-require
|
||||
const puppeteer = require('puppeteer');
|
||||
// add some options "no-sandbox" to make it work properly on some Linux systems as proposed here: https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322851507
|
||||
browser = await puppeteer.launch({
|
||||
args: ['--no-sandbox ', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||
@ -97,6 +107,8 @@ export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) =
|
||||
|
||||
page = await browser.newPage();
|
||||
};
|
||||
beforeAll.timeout = setupTimeout;
|
||||
testFn.beforeAll = beforeAll;
|
||||
|
||||
return testFn;
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ if (!fs.existsSync(pathToStorybookStatic)) {
|
||||
} else {
|
||||
initStoryshots({
|
||||
suite: 'Image snapshots',
|
||||
storyKindRegex: /^Addons\|Storyshots/,
|
||||
storyKindRegex: /^Addons\/Storyshots/,
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, '..'),
|
||||
test: imageSnapshot({
|
||||
|
@ -61,5 +61,8 @@
|
||||
"ts-loader": "^6.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"webpack": "^4.33.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"puppeteer": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export const version = '5.3.0-beta.3';
|
||||
export const version = '5.3.0-beta.5';
|
||||
|
@ -66,7 +66,6 @@
|
||||
"find-up": "^4.1.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"glob-base": "^0.3.0",
|
||||
"glob-regex": "^0.3.2",
|
||||
"global": "^4.3.2",
|
||||
"html-webpack-plugin": "^4.0.0-beta.2",
|
||||
"inquirer": "^7.0.0",
|
||||
@ -74,6 +73,7 @@
|
||||
"ip": "^1.1.5",
|
||||
"json5": "^2.1.1",
|
||||
"lazy-universal-dotenv": "^3.0.1",
|
||||
"micromatch": "^4.0.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"open": "^7.0.0",
|
||||
"pnp-webpack-plugin": "1.5.0",
|
||||
@ -104,6 +104,7 @@
|
||||
"mock-fs": "^4.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.1",
|
||||
"babel-loader": "^7.0.0 || ^8.0.0",
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
|
@ -2,6 +2,7 @@ const defaultWebpackConfig = require('./dist/server/preview/base-webpack.config'
|
||||
const serverUtils = require('./dist/server/utils/template');
|
||||
const buildStatic = require('./dist/server/build-static');
|
||||
const buildDev = require('./dist/server/build-dev');
|
||||
const toRequireContext = require('./dist/server/preview/to-require-context');
|
||||
|
||||
const managerPreset = require.resolve('./dist/server/manager/manager-preset');
|
||||
|
||||
@ -11,4 +12,5 @@ module.exports = {
|
||||
...buildStatic,
|
||||
...buildDev,
|
||||
...serverUtils,
|
||||
...toRequireContext,
|
||||
};
|
||||
|
@ -361,7 +361,7 @@ export default function start(render, { decorateStory } = {}) {
|
||||
});
|
||||
} else {
|
||||
const exported = loadable();
|
||||
if (Array.isArray(exported) && !exported.find(obj => !obj.default)) {
|
||||
if (Array.isArray(exported) && exported.every(obj => obj.default != null)) {
|
||||
currentExports = new Map(exported.map(fileExports => [fileExports, null]));
|
||||
} else if (exported) {
|
||||
logger.warn(
|
||||
|
@ -9,14 +9,11 @@ import CoreJSUpgradeWebpackPlugin from 'corejs-upgrade-webpack-plugin';
|
||||
import VirtualModulePlugin from 'webpack-virtual-modules';
|
||||
|
||||
import resolveFrom from 'resolve-from';
|
||||
import toRegex from 'glob-regex';
|
||||
import globBase from 'glob-base';
|
||||
|
||||
import babelLoader from '../common/babel-loader';
|
||||
import { nodeModulesPaths, loadEnv } from '../config/utils';
|
||||
import { getPreviewHeadHtml, getPreviewBodyHtml } from '../utils/template';
|
||||
|
||||
const isObject = val => val != null && typeof val === 'object' && Array.isArray(val) === false;
|
||||
import { loadEnv, nodeModulesPaths } from '../config/utils';
|
||||
import { getPreviewBodyHtml, getPreviewHeadHtml } from '../utils/template';
|
||||
import { toRequireContextString } from './to-require-context';
|
||||
|
||||
const reactPaths = {};
|
||||
try {
|
||||
@ -26,27 +23,6 @@ try {
|
||||
//
|
||||
}
|
||||
|
||||
const toRequireContext = input => {
|
||||
switch (true) {
|
||||
case typeof input === 'string': {
|
||||
const { base, glob } = globBase(input);
|
||||
const regex = toRegex(glob)
|
||||
.toString()
|
||||
.replace('^([^\\/]+)', '');
|
||||
|
||||
return `require.context('${base}', true, ${regex})`;
|
||||
}
|
||||
case isObject(input): {
|
||||
const { path: p, recursive: r, match: m } = input;
|
||||
return `require.context('${p}', ${r}, ${m})`;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('the provided input cannot be transformed into a require.context');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ({
|
||||
configDir,
|
||||
babelOptions,
|
||||
@ -77,7 +53,7 @@ export default ({
|
||||
[path.resolve(path.join(configDir, `generated-entry.js`))]: `
|
||||
import { configure, addDecorator, addParameters } from '@storybook/${framework}';
|
||||
|
||||
configure([${stories.map(toRequireContext).join(',')}
|
||||
configure([${stories.map(toRequireContextString).join(',')}
|
||||
], module);
|
||||
`,
|
||||
})
|
||||
|
30
lib/core/src/server/preview/to-require-context.js
Normal file
30
lib/core/src/server/preview/to-require-context.js
Normal file
@ -0,0 +1,30 @@
|
||||
import globBase from 'glob-base';
|
||||
import { makeRe } from 'micromatch';
|
||||
|
||||
const isObject = val => val != null && typeof val === 'object' && Array.isArray(val) === false;
|
||||
export const toRequireContext = input => {
|
||||
switch (true) {
|
||||
case typeof input === 'string': {
|
||||
const { base, glob } = globBase(input);
|
||||
const regex = makeRe(glob)
|
||||
.toString()
|
||||
// webpack prepends the relative path with './'
|
||||
.replace(/^\/\^/, '/^\\.\\/')
|
||||
.replace(/\?:\^/g, '?:');
|
||||
|
||||
return { path: base, recursive: glob.startsWith('**'), match: regex };
|
||||
}
|
||||
case isObject(input): {
|
||||
return input;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('the provided input cannot be transformed into a require.context');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toRequireContextString = input => {
|
||||
const { path: p, recursive: r, match: m } = toRequireContext(input);
|
||||
return `require.context('${p}', ${r}, ${m})`;
|
||||
};
|
@ -2,5 +2,5 @@ const path = require('path');
|
||||
const babelJest = require('babel-jest');
|
||||
|
||||
module.exports = babelJest.createTransformer({
|
||||
configFile: path.resolve('.babelrc'),
|
||||
configFile: path.resolve(__dirname, '../.babelrc'),
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user