Skip to content

Commit ad08bfc

Browse files
valentinpalkovicyannbfJReinhold
committed
Portable Stories: Improve Handling of React Updates and Errors
Co-authored-by: Yann Braga <[email protected]> Co-authored-by: Jeppe Reinhold <[email protected]>
1 parent dea51a7 commit ad08bfc

15 files changed

+535
-74
lines changed

code/core/src/preview-api/modules/store/csf/portable-stories.ts

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export function setProjectAnnotations<TRenderer extends Renderer = Renderer>(
7474
| NamedOrDefaultProjectAnnotations<TRenderer>[]
7575
): NormalizedProjectAnnotations<TRenderer> {
7676
const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations];
77+
if (globalThis.defaultProjectAnnotations) {
78+
annotations.push(globalThis.defaultProjectAnnotations);
79+
}
80+
7781
globalThis.globalProjectAnnotations = composeConfigs(annotations.map(extractAnnotation));
7882

7983
return globalThis.globalProjectAnnotations;

code/lib/react-dom-shim/src/preventActChecks.tsx

-17
This file was deleted.

code/lib/react-dom-shim/src/react-16.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
import type { ReactElement } from 'react';
33
import * as ReactDOM from 'react-dom';
44

5-
import { preventActChecks } from './preventActChecks';
6-
75
export const renderElement = async (node: ReactElement, el: Element) => {
86
return new Promise<null>((resolve) => {
9-
preventActChecks(() => void ReactDOM.render(node, el, () => resolve(null)));
7+
ReactDOM.render(node, el, () => resolve(null));
108
});
119
};
1210

1311
export const unmountElement = (el: Element) => {
14-
preventActChecks(() => void ReactDOM.unmountComponentAtNode(el));
12+
ReactDOM.unmountComponentAtNode(el);
1513
};

code/lib/react-dom-shim/src/react-18.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */
2-
import type { FC, ReactElement } from 'react';
2+
import type { ReactElement } from 'react';
33
import * as React from 'react';
44
import type { Root as ReactRoot, RootOptions } from 'react-dom/client';
55
import * as ReactDOM from 'react-dom/client';
66

7-
import { preventActChecks } from './preventActChecks';
8-
97
// A map of all rendered React 18 nodes
108
const nodes = new Map<Element, ReactRoot>();
119

12-
const WithCallback: FC<{ callback: () => void; children: ReactElement }> = ({
10+
declare const globalThis: {
11+
IS_REACT_ACT_ENVIRONMENT: boolean;
12+
};
13+
14+
function getIsReactActEnvironment() {
15+
return globalThis.IS_REACT_ACT_ENVIRONMENT;
16+
}
17+
18+
const WithCallback: React.FC<{ callback: () => void; children: ReactElement }> = ({
1319
callback,
1420
children,
1521
}) => {
@@ -43,16 +49,21 @@ export const renderElement = async (node: ReactElement, el: Element, rootOptions
4349
// Create Root Element conditionally for new React 18 Root Api
4450
const root = await getReactRoot(el, rootOptions);
4551

52+
if (getIsReactActEnvironment()) {
53+
root.render(node);
54+
return;
55+
}
56+
4657
const { promise, resolve } = Promise.withResolvers<void>();
47-
preventActChecks(() => root.render(<WithCallback callback={resolve}>{node}</WithCallback>));
58+
root.render(<WithCallback callback={resolve}>{node}</WithCallback>);
4859
return promise;
4960
};
5061

5162
export const unmountElement = (el: Element, shouldUseNewRootApi?: boolean) => {
5263
const root = nodes.get(el);
5364

5465
if (root) {
55-
preventActChecks(() => root.unmount());
66+
root.unmount();
5667
nodes.delete(el);
5768
}
5869
};

code/renderers/react/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,16 @@
9494
"require-from-string": "^2.0.2"
9595
},
9696
"peerDependencies": {
97+
"@storybook/test": "workspace:*",
9798
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
9899
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
99100
"storybook": "workspace:^",
100101
"typescript": ">= 4.2.x"
101102
},
102103
"peerDependenciesMeta": {
104+
"@storybook/test": {
105+
"optional": true
106+
},
103107
"typescript": {
104108
"optional": true
105109
}

code/renderers/react/src/__test__/Button.stories.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ export const HooksStory: CSF3Story = {
103103
);
104104
},
105105
play: async ({ canvasElement, step }) => {
106-
console.log('start of play function');
107106
const canvas = within(canvasElement);
108107
await step('Step label', async () => {
109108
const inputEl = canvas.getByTestId('input');
@@ -112,8 +111,8 @@ export const HooksStory: CSF3Story = {
112111
await userEvent.type(inputEl, 'Hello world!');
113112

114113
await expect(inputEl).toHaveValue('Hello world!');
114+
await expect(buttonEl).toHaveTextContent('I am clicked');
115115
});
116-
console.log('end of play function');
117116
},
118117
};
119118

@@ -182,6 +181,12 @@ export const MountInPlayFunction: CSF3Story<{ mockFn: (val: string) => string }>
182181
},
183182
};
184183

184+
export const MountInPlayFunctionThrow: CSF3Story<{ mockFn: (val: string) => string }> = {
185+
play: async () => {
186+
throw new Error('Error thrown in play');
187+
},
188+
};
189+
185190
export const WithActionArg: CSF3Story<{ someActionArg: HandlerFunction }> = {
186191
args: {
187192
someActionArg: action('some-action-arg'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Meta, StoryObj } from '..';
2+
import { ComponentWithError } from './ComponentWithError';
3+
4+
const meta = {
5+
title: 'Example/ComponentWithError',
6+
component: ComponentWithError as any,
7+
} satisfies Meta<typeof ComponentWithError>;
8+
9+
export default meta;
10+
11+
type Story = StoryObj<typeof meta>;
12+
13+
export const ThrowsError: Story = {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function ComponentWithError() {
2+
// eslint-disable-next-line local-rules/no-uncategorized-errors
3+
throw new Error('Error in render');
4+
}

code/renderers/react/src/__test__/__snapshots__/portable-stories-legacy.test.tsx.snap

+17
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,23 @@ exports[`Legacy Portable Stories API > Renders Modal story 1`] = `
147147
</body>
148148
`;
149149

150+
exports[`Legacy Portable Stories API > Renders MountInPlayFunctionThrow story 1`] = `
151+
<body>
152+
<div>
153+
<div
154+
data-testid="loaded-data"
155+
>
156+
loaded data
157+
</div>
158+
<div
159+
data-testid="spy-data"
160+
>
161+
mockFn return value
162+
</div>
163+
</div>
164+
</body>
165+
`;
166+
150167
exports[`Legacy Portable Stories API > Renders WithActionArg story 1`] = `
151168
<body>
152169
<div>

0 commit comments

Comments
 (0)