Fix error handling

This commit is contained in:
Hypnosphi 2019-09-16 22:00:09 +02:00
parent dfe9eb7efc
commit b97a36260a
9 changed files with 117 additions and 164 deletions

View File

@ -1,88 +0,0 @@
import React from 'react';
import isReactRenderable, { isValidFiberElement, isPriorToFiber } from './element_check';
describe('element_check.utils.isValidFiberElement', () => {
it('should accept to render a string', () => {
const string = 'react is awesome';
expect(isValidFiberElement(string)).toBe(true);
});
it('should accept to render a number', () => {
const number = 42;
expect(isValidFiberElement(number)).toBe(true);
});
it('should accept to render a valid React element', () => {
const element = <button type="button">Click me</button>;
expect(isValidFiberElement(element)).toBe(true);
});
it("shouldn't accept to render an arbitrary object", () => {
const object = { key: 'bee bop' };
expect(isValidFiberElement(object)).toBe(false);
});
it("shouldn't accept to render a function", () => {
const noop = () => {};
expect(isValidFiberElement(noop)).toBe(false);
});
it("shouldn't accept to render undefined", () => {
expect(isValidFiberElement(undefined)).toBe(false);
});
});
describe('element_check.utils.isPriorToFiber', () => {
it('should return true if React version is prior to Fiber (< 16)', () => {
const oldVersion = '0.14.5';
const version = '15.5.4';
expect(isPriorToFiber(oldVersion)).toBe(true);
expect(isPriorToFiber(version)).toBe(true);
});
it('should return false if React version is using Fiber features (>= 16)', () => {
const alphaVersion = '16.0.0-alpha.13';
const version = '18.3.1';
expect(isPriorToFiber(alphaVersion)).toBe(false);
expect(isPriorToFiber(version)).toBe(false);
});
});
describe('element_check.isReactRenderable', () => {
const string = 'yo';
const number = 1337;
const element = <span>what's up</span>;
const array = [string, number, element];
const object = { key: null } as any;
it('allows rendering React elements only prior to React Fiber', () => {
// mutate version for the purpose of the test
// @ts-ignore
React.version = '15.5.4';
expect(isReactRenderable(string)).toBe(false);
expect(isReactRenderable(number)).toBe(false);
expect(isReactRenderable(element)).toBe(true);
expect(isReactRenderable(array)).toBe(false);
expect(isReactRenderable(object)).toBe(false);
});
it('allows rendering string, numbers, arrays and React elements with React Fiber', () => {
// mutate version for the purpose of the test
// @ts-ignore
React.version = '16.0.0-alpha.13';
expect(isReactRenderable(string)).toBe(true);
expect(isReactRenderable(number)).toBe(true);
expect(isReactRenderable(element)).toBe(true);
expect(isReactRenderable(array)).toBe(true);
expect(isReactRenderable(object)).toBe(false);
});
});

View File

@ -1,41 +0,0 @@
import React from 'react';
import flattenDeep from 'lodash/flattenDeep';
// return true if the element is renderable with react fiber
export const isValidFiberElement = (element: React.ReactElement) =>
typeof element === 'string' || typeof element === 'number' || React.isValidElement(element);
export const isPriorToFiber = (version: string) => {
const [majorVersion] = version.split('.');
return Number(majorVersion) < 16;
};
// accepts an element and return true if renderable else return false
const isReactRenderable = (element: React.ReactElement): boolean => {
// storybook is running with a version prior to fiber,
// run a simple check on the element
if (isPriorToFiber(React.version)) {
return React.isValidElement(element);
}
// the element is not an array, check if its a fiber renderable element
if (!Array.isArray(element)) {
return isValidFiberElement(element);
}
// the element is in fact a list of elements (array),
// loop on its elements to see if its ok to render them
const elementsList = element.map(isReactRenderable);
// flatten the list of elements (possibly deep nested)
const flatList = flattenDeep(elementsList);
// keep only invalid elements
const invalidElements = flatList.filter(elementIsRenderable => elementIsRenderable === false);
// it's ok to render this list if there is no invalid elements inside
return !invalidElements.length;
};
export default isReactRenderable;

View File

@ -1,9 +1,7 @@
import { document } from 'global';
import React from 'react';
import React, { ErrorInfo } from 'react';
import ReactDOM from 'react-dom';
import { stripIndents } from 'common-tags';
import isReactRenderable from './element_check';
import { RenderMainArgs } from './types';
import { RenderMainArgs, ShowErrorArgs } from './types';
const rootEl = document ? document.getElementById('root') : null;
@ -16,37 +14,49 @@ const render = (node: React.ReactElement, el: Element) =>
);
});
class ErrorBoundary extends React.Component<{
showError: (args: ShowErrorArgs) => void;
showMain: () => void;
}> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidMount() {
const { hasError } = this.state;
const { showMain } = this.props;
if (!hasError) {
showMain();
}
}
componentDidCatch({ message, stack }: Error) {
const { showError } = this.props;
// message partially duplicates stack, strip it
showError({ title: message.split(/\n/)[0], description: stack });
}
render() {
const { hasError } = this.state;
const { children } = this.props;
return hasError ? null : children;
}
}
export default async function renderMain({
storyFn: StoryFn,
selectedKind,
selectedStory,
showMain,
showError,
forceRender,
}: RenderMainArgs) {
const element = <StoryFn />;
if (!element) {
showError({
title: `Expecting a React element from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the React element from the story?
Use "() => (<MyComp/>)" or "() => { return <MyComp/>; }" when defining the story.
`,
});
return;
}
if (!isReactRenderable(element)) {
showError({
title: `Expecting a valid React element from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Seems like you are not returning a correct React element from the story.
Could you double check that?
`,
});
return;
}
const element = (
<ErrorBoundary showMain={showMain} showError={showError}>
<StoryFn />
</ErrorBoundary>
);
// We need to unmount the existing set of components in the DOM node.
// Otherwise, React may not recreate instances for every story run.
@ -58,5 +68,4 @@ export default async function renderMain({
}
await render(element, rootEl);
showMain();
}

View File

@ -1,6 +1,7 @@
import React, { Fragment } from 'react';
const BadComponent = () => ({ renderable: 'no, react can not render objects' });
const badOutput = { renderable: 'no, react can not render objects' };
const BadComponent = () => badOutput;
export default {
title: 'Core|Errors',
@ -24,7 +25,7 @@ export const badComponent = () => (
</Fragment>
);
badComponent.story = {
name: 'story errors - variant error',
name: 'story errors - invariant error',
parameters: {
notes: 'Story does not return something react can render',
storyshots: { disable: true },
@ -32,7 +33,7 @@ badComponent.story = {
},
};
export const badStory = () => false;
export const badStory = () => badOutput;
badStory.story = {
name: 'story errors - story un-renderable type',
parameters: {

View File

@ -278,6 +278,8 @@ exports[`Storyshots Button Square 1`] = `
</button>
`;
exports[`Storyshots Core|Errors Null Error 1`] = `<!---->`;
exports[`Storyshots Core|Parameters Passed To Story 1`] = `
<div>
Parameters are {"options":{"hierarchyRootSeparator":{},"hierarchySeparator":{}},"docs":{"iframeHeight":"60px"},"globalParameter":"globalParameter","framework":"vue","chapterParameter":"chapterParameter","storyParameter":"storyParameter","displayName":"passed to story"}

View File

@ -0,0 +1,10 @@
export default {
title: 'Core|Errors',
};
export const throwsError = () => {
throw new Error('foo');
};
throwsError.story = { parameters: { storyshots: { disable: true } } };
export const nullError = () => null;

View File

@ -248,8 +248,12 @@ export default function start(render, { decorateStory } = {}) {
default: {
if (getDecorated) {
(async () => {
await render(renderContext);
addons.getChannel().emit(Events.STORY_RENDERED, id);
try {
await render(renderContext);
addons.getChannel().emit(Events.STORY_RENDERED, id);
} catch (ex) {
showException(ex);
}
})();
} else {
showNopreview();

View File

@ -24,6 +24,7 @@
font-weight: 600;
letter-spacing: 0.2px;
margin: 10px 0;
padding-right: 25px;
}
.sb-nopreview {
@ -56,6 +57,10 @@
color: #eee;
font-family: "Operator Mono", "Fira Code Retina", "Fira Code", "FiraCode-Retina", "Andale Mono", "Lucida Console", Consolas, Monaco, monospace;
}
.sb-errordisplay pre {
white-space: pre-wrap;
}
</style>
<script>

View File

@ -3466,6 +3466,57 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@storybook/cli@5.2.0-rc.9":
version "5.2.0-rc.9"
resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-5.2.0-rc.9.tgz#8f8229440a0070755b805e3b666fd92612723794"
integrity sha512-G87+xQ4KDaKnCnNQM1IupvNo2DIezlpooAsEjTwPVSVRddH9faQrIVyCnm5ygeyB4BYVBdzLSREz5BCR5hnImg==
dependencies:
"@babel/core" "^7.4.5"
"@babel/preset-env" "^7.4.5"
"@storybook/codemod" "5.2.0-rc.9"
chalk "^2.4.1"
commander "^2.19.0"
core-js "^3.0.1"
cross-spawn "^6.0.5"
didyoumean "^1.2.1"
envinfo "^7.3.1"
esm "3.2.25"
fs-extra "^8.0.1"
inquirer "^6.2.0"
jscodeshift "^0.6.3"
json5 "^2.1.0"
pkg-add-deps "^0.1.0"
semver "^6.0.0"
shelljs "^0.8.3"
update-notifier "^3.0.0"
"@storybook/codemod@5.2.0-rc.9":
version "5.2.0-rc.9"
resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-5.2.0-rc.9.tgz#421d62c4e44435881795e268f2e1ac79937f1b54"
integrity sha512-/6FCsde3gts3kAdmgf20BeGOTJwMTqHZul9cgztJ8mqI9UBEiCxLFn8BJgb60VB0gLpLBNY6gFNdi//ZK+x1jA==
dependencies:
"@mdx-js/mdx" "^1.0.0"
"@storybook/node-logger" "5.2.0-rc.9"
core-js "^3.0.1"
cross-spawn "^6.0.5"
globby "^10.0.1"
jscodeshift "^0.6.3"
lodash "^4.17.11"
prettier "^1.16.4"
recast "^0.16.1"
regenerator-runtime "^0.12.1"
"@storybook/node-logger@5.2.0-rc.9":
version "5.2.0-rc.9"
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.2.0-rc.9.tgz#fd7a07b6aebd49f81ac593b57854fcd1385f46aa"
integrity sha512-FyOexug8FPrrhI4Zk/Iq9KfsW5sBK4nBxE/pN6j5pdb3H7pFoEu+uPv/TFDIo03Im1cF/vUCDGd+qsJkUd+dvA==
dependencies:
chalk "^2.4.2"
core-js "^3.0.1"
npmlog "^4.1.2"
pretty-hrtime "^1.0.3"
regenerator-runtime "^0.12.1"
"@svgr/babel-plugin-add-jsx-attribute@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1"