8000 Merge pull request #29044 from storybookjs/valentin/propagate-error-i… · storybookjs/storybook@603841c · GitHub
[go: up one dir, main page]

Skip to content

Commit 603841c

Browse files
authored
Merge pull request #29044 from storybookjs/valentin/propagate-error-in-testing
Portable Stories: Improve Handling of React Updates and Errors
2 parents 87bf34c + 46aa6e0 commit 603841c

File tree

22 files changed

+466
-208
lines changed

22 files changed

+466
-208
lines changed

code/addons/interactions/src/preview.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { PlayFunction, StepLabel, StoryContext } from 'storybook/internal/types';
22

33
import { instrument } from '@storybook/instrumenter';
4+
// This makes sure that storybook test loaders are always loaded when addon-interactions is used
5+
// For 9.0 we want to merge storybook/test and addon-interactions into one addon.
6+
import '@storybook/test';
47

58
export const { step: runStep } = instrument(
69
{

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,17 @@ export function setProjectAnnotations<TRenderer extends Renderer = Renderer>(
7676
const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations];
7777
globalThis.globalProjectAnnotations = composeConfigs(annotations.map(extractAnnotation));
7878

79-
return globalThis.globalProjectAnnotations;
79+
/*
80+
We must return the composition of default and global annotations here
81+
To ensure that the user has the full project annotations, eg. when running
82+
83+
const projectAnnotations = setProjectAnnotations(...);
84+
beforeAll(projectAnnotations.beforeAll)
85+
*/
86+
return composeConfigs([
87+
globalThis.defaultProjectAnnotations ?? {},
88+
globalThis.globalProjectAnnotations ?? {},
89+
]);
8090
}
8191

8292
const cleanups: CleanupCallback[] = [];

code/core/template/stories/preview.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ export const parameters = {
3030

3131
export const loaders = [async () => ({ projectValue: 2 })];
3232

33-
export const decorators = [
34-
(storyFn: PartialStoryFn, context: StoryContext) => {
35-
if (context.parameters.useProjectDecorator) {
36-
return storyFn({ args: { ...context.args, text: `project ${context.args.text}` } });
37-
}
38-
return storyFn();
39-
},
40-
];
33+
const testProjectDecorator = (storyFn: PartialStoryFn, context: StoryContext) => {
34+
if (context.parameters.useProjectDecorator) {
35+
return storyFn({ args: { ...context.args, text: `project ${context.args.text}` } });
36+
}
37+
return storyFn();
38+
};
39+
40+
export const decorators = [testProjectDecorator];
4141

4242
export const initialGlobals = {
4343
foo: 'fooValue',

code/frameworks/experimental-nextjs-vite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"@storybook/react": "workspace:*",
100100
"@storybook/test": "workspace:*",
101101
"styled-jsx": "5.1.6",
102-
"vite-plugin-storybook-nextjs": "^1.0.10"
102+
"vite-plugin-storybook-nextjs": "^1.0.11"
103103
},
104104
"devDependencies": {
105105
"@types/node": "^18.0.0",

code/frameworks/nextjs/src/config/webpack.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
22
import type { Configuration as WebpackConfig } from 'webpack';
33
import { DefinePlugin } from 'webpack';
44

5-
import { addScopedAlias, resolveNextConfig } from '../utils';
5+
import { addScopedAlias, resolveNextConfig, setAlias } from '../utils';
66

77
const tryResolve = (path: string) => {
88
try {
@@ -22,12 +22,32 @@ export const configureConfig = async ({
2222
const nextConfig = await resolveNextConfig({ nextConfigPath });
2323

2424
addScopedAlias(baseConfig, 'next/config');
25+
26+
// @ts-expect-error We know that alias is an object
27+
if (baseConfig.resolve?.alias?.['react-dom']) {
28+
// Removing the alias to react-dom to avoid conflicts with the alias we are setting
29+
// because the react-dom alias is an exact match and we need to alias separate parts of react-dom
30+
// in different places
31+
// @ts-expect-error We know that alias is an object
32+
delete baseConfig.resolve.alias?.['react-dom'];
33+
}
34+
2535
if (tryResolve('next/dist/compiled/react')) {
2636
addScopedAlias(baseConfig, 'react', 'next/dist/compiled/react');
2737
}
38+
if (tryResolve('next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js')) {
39+
setAlias(
40+
baseConfig,
41+
'react-dom/test-utils',
42+
'next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js'
43+
);
44+
}
2845
if (tryResolve('next/dist/compiled/react-dom')) {
29-
addScopedAlias(baseConfig, 'react-dom', 'next/dist/compiled/react-dom');
46+
setAlias(baseConfig, 'react-dom$', 'next/dist/compiled/react-dom');
47+
setAlias(baseConfig, 'react-dom/client', 'next/dist/compiled/react-dom/client');
48+
setAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server');
3049
}
50+
3151
setupRuntimeConfig(baseConfig, nextConfig);
3252

3353
return nextConfig;

code/frameworks/nextjs/src/utils.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,27 @@ export const resolveNextConfig = async ({
2727
return loadConfig(PHASE_DEVELOPMENT_SERVER, dir, undefined);
2828
};
2929

30-
// This is to help the addon in development
31-
// Without it, webpack resolves packages in its node_modules instead of the example's node_modules
32-
export const addScopedAlias = (baseConfig: WebpackConfig, name: string, alias?: string): void => {
30+
export function setAlias(baseConfig: WebpackConfig, name: string, alias: string) {
3331
baseConfig.resolve ??= {};
3432
baseConfig.resolve.alias ??= {};
3533
const aliasConfig = baseConfig.resolve.alias;
3634

37-
const scopedAlias = scopedResolve(`${alias ?? name}`);
38-
3935
if (Array.isArray(aliasConfig)) {
4036
aliasConfig.push({
4137
name,
42-
alias: scopedAlias,
38+
alias,
4339
});
4440
} else {
45-
aliasConfig[name] = scopedAlias;
41+
aliasConfig[name] = alias;
4642
}
43+
}
44+
45+
// This is to help the addon in development
46+
// Without it, webpack resolves packages in its node_modules instead of the example's node_modules
47+
export const addScopedAlias = (baseConfig: WebpackConfig, name: string, alias?: string): void => {
48+
const scopedAlias = scopedResolve(`${alias ?? name}`);
49+
50+
setAlias(baseConfig, name, scopedAlias);
4751
};
4852

4953
/**
@@ -64,7 +68,7 @@ export const scopedResolve = (id: string): string => {
6468
let scopedModulePath;
6569

6670
try {
67-
// TODO: Remove in next major release (SB 8.0) and use the statement in the catch block per default instead
71+
// TODO: Remove in next major release (SB 9.0) and use the statement in the catch block per default instead
6872
scopedModulePath = require.resolve(id, { paths: [resolve()] });
6973
} catch (e) {
7074
scopedModulePath = require.resolve(id);

code/frameworks/sveltekit/src/preview.ts

Lines changed: 111 additions & 111 deletions
Original 10000 file line numberDiff line numberDiff line change
@@ -15,125 +15,125 @@ const normalizeHrefConfig = (hrefConfig: HrefConfig): NormalizedHrefConfig => {
1515
return hrefConfig;
1616
};
1717

18-
export const decorators: Decorator[] = [
19-
(Story, ctx) => {
20-
const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {};
21-
setPage(svelteKitParameters?.stores?.page);
22-
setNavigating(svelteKitParameters?.stores?.navigating);
23-
setUpdated(svelteKitParameters?.stores?.updated);
24-
setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate);
18+
const svelteKitMocksDecorator: Decorator = (Story, ctx) => {
19+
const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {};
20+
setPage(svelteKitParameters?.stores?.page);
21+
setNavigating(svelteKitParameters?.stores?.navigating);
22+
setUpdated(svelteKitParameters?.stores?.updated);
23+
setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate);
2524

26-
onMount(() => {
27-
const globalClickListener = (e: MouseEvent) => {
28-
// we add a global click event listener and we check if there's a link in the composedPath
29-
const path = e.composedPath();
30-
const element = path.findLast((el) => el instanceof HTMLElement && el.tagName === 'A');
31-
if (element && element instanceof HTMLAnchorElement) {
32-
// if the element is an a-tag we get the href of the element
33-
// and compare it to the hrefs-parameter set by the user
34-
const to = element.getAttribute('href');
35-
if (!to) {
36-
return;
37-
}
38-
e.preventDefault();
39-
const defaultActionCallback = () => action('navigate')(to, e);
40-
if (!svelteKitParameters.hrefs) {
41-
defaultActionCallback();
42-
return;
43-
}
44-
45-
let callDefaultCallback = true;
46-
// we loop over every href set by the user and check if the href matches
47-
// if it does we call the callback provided by the user and disable the default callback
48-
Object.entries(svelteKitParameters.hrefs).forEach(([href, hrefConfig]) => 10000 {
49-
const { callback, asRegex } = normalizeHrefConfig(hrefConfig);
50-
const isMatch = asRegex ? new RegExp(href).test(to) : to === href;
51-
if (isMatch) {
52-
callDefaultCallback = false;
53-
callback?.(to, e);
54-
}
55-
});
56-
if (callDefaultCallback) {
57-
defaultActionCallback();
58-
}
25+
onMount(() => {
26+
const globalClickListener = (e: MouseEvent) => {
27+
// we add a global click event listener and we check if there's a link in the composedPath
28+
const path = e.composedPath();
29+
const element = path.findLast((el) => el instanceof HTMLElement && el.tagName === 'A');
30+
if (element && element instanceof HTMLAnchorElement) {
31+
// if the element is an a-tag we get the href of the element
32+
// and compare it to the hrefs-parameter set by the user
33+
const to = element.getAttribute('href');
34+
if (!to) {
35+
return;
36+
}
37+
e.preventDefault();
38+
const defaultActionCallback = () => action('navigate')(to, e);
39+
if (!svelteKitParameters.hrefs) {
40+
defaultActionCallback();
41+
return;
5942
}
60-
};
61-
62-
/**
63-
* Function that create and add listeners for the event that are emitted by the mocked
64-
* functions. The event name is based on the function name
65-
*
66-
* Eg. storybook:goto, storybook:invalidateAll
67-
*
68-
* @param baseModule The base module where the function lives (navigation|forms)
69-
* @param functions The list of functions in that module that emit events
70-
* @param {boolean} [defaultToAction] The list of functions in that module that emit events
71-
* @returns A function to remove all the listener added
72-
*/
73-
function createListeners(
74-
baseModule: keyof SvelteKitParameters,
75-
functions: string[],
76-
defaultToAction?: boolean
77-
) {
78-
// the array of every added listener, we can use this in the return function
79-
// to clean them
80-
const toRemove: Array<{
81-
eventType: string;
82-
listener: (event: { detail: any[] }) => void;
83-
}> = [];
84-
functions.forEach((func) => {
85-
// we loop over every function and check if the user actually passed
86-
// a function in sveltekit_experimental[baseModule][func] eg. sveltekit_experimental.navigation.goto
87-
const hasFunction =
88-
(svelteKitParameters as any)[baseModule]?.[func] &&
89-
(svelteKitParameters as any)[baseModule][func] instanceof Function;
90-
// if we default to an action we still add the listener (this will be the case for goto, invalidate, invalidateAll)
91-
if (hasFunction || defaultToAction) {
92-
// we create the listener that will just get the detail array from the custom element
93-
// and call the user provided function spreading this args in...this will basically call
94-
// the function that the user provide with the same arguments the function is invoked to
9543

96-
// eg. if it calls goto("/my-route") inside the component the function sveltekit_experimental.navigation.goto
97-
// it provided to storybook will be called with "/my-route"
98-
const listener = ({ detail = [] as any[] }) => {
99-
const args = Array.isArray(detail) ? detail : [];
100-
// if it has a function in the parameters we call that function
101-
// otherwise we invoke the action
102-
const fnToCall = hasFunction
103-
? (svelteKitParameters as any)[baseModule][func]
104-
: action(func);
105-
fnToCall(...args);
106-
};
107-
const eventType = `storybook:${func}`;
108-
toRemove.push({ eventType, listener });
109-
// add the listener to window
110-
(window.addEventListener as any)(eventType, listener);
44+
let callDefaultCallback = true;
45+
// we loop over every href set by the user and check if the href matches
46+
// if it does we call the callback provided by the user and disable the default callback
47+
Object.entries(svelteKitParameters.hrefs).forEach(([href, hrefConfig]) => {
48+
const { callback, asRegex } =< D7AE /span> normalizeHrefConfig(hrefConfig);
49+
const isMatch = asRegex ? new RegExp(href).test(to) : to === href;
50+
if (isMatch) {
51+
callDefaultCallback = false;
52+
callback?.(to, e);
11153
}
11254
});
113-
return () => {
114-
// loop over every listener added and remove them
115-
toRemove.forEach(({ eventType, listener }) => {
116-
// @ts-expect-error apparently you can't remove a custom listener to the window with TS
117-
window.removeEventListener(eventType, listener);
118-
});
119-
};
55+
if (callDefaultCallback) {
56+
defaultActionCallback();
57+
}
12058
}
59+
};
12160

122-
const removeNavigationListeners = createListeners(
123-
'navigation',
124-
['goto', 'invalidate', 'invalidateAll', 'pushState', 'replaceState'],
125-
true
126-
);
127-
const removeFormsListeners = createListeners('forms', ['enhance']);
128-
window.addEventListener('click', globalClickListener);
61+
/**
62+
* Function that create and add listeners for the event that are emitted by the mocked
63+
* functions. The event name is based on the function name
64+
*
65+
* Eg. storybook:goto, storybook:invalidateAll
66+
*
67+
* @param baseModule The base module where the function lives (navigation|forms)
68+
* @param functions The list of functions in that module that emit events
69+
* @param {boolean} [defaultToAction] The list of functions in that module that emit events
70+
* @returns A function to remove all the listener added
71+
*/
72+
function createListeners(
73+
baseModule: keyof SvelteKitParameters,
74+
functions: string[],
75+
defaultToAction?: boolean
76+
) {
77+
// the array of every added listener, we can use this in the return function
78+
// to clean them
79+
const toRemove: Array<{
80+
eventType: string;
81+
listener: (event: { detail: any[] }) => void;
82+
}> = [];
83+
functions.forEach((func) => {
84+
// we loop over every function and check if the user actually passed
85+
// a function in sveltekit_experimental[baseModule][func] eg. sveltekit_experimental.navigation.goto
86+
const hasFunction =
87+
(svelteKitParameters as any)[baseModule]?.[func] &&
88+
(svelteKitParameters as any)[baseModule][func] instanceof Function;
89+
// if we default to an action we still add the listener (this will be the case for goto, invalidate, invalidateAll)
90+
if (hasFunction || defaultToAction) {
91+
// we create the listener that will just get the detail array from the custom element
92+
// and call the user provided function spreading this args in...this will basically call
93+
// the function that the user provide with the same arguments the function is invoked to
12994

95+
// eg. if it calls goto("/my-route") inside the component the function sveltekit_experimental.navigation.goto
96+
// it provided to storybook will be called with "/my-route"
97+
const listener = ({ detail = [] as any[] }) => {
98+
const args = Array.isArray(detail) ? detail : [];
99+
// if it has a function in the parameters we call that function
100+
// otherwise we invoke the action
101+
const fnToCall = hasFunction
102+
? (svelteKitParameters as any)[baseModule][func]
103+
: action(func);
104+
fnToCall(...args);
105+
};
106+
const eventType = `storybook:${func}`;
107+
toRemove.push({ eventType, listener });
108+
// add the listener to window
109+
(window.addEventListener as any)(eventType, listener);
110+
}
111+
});
130112
return () => {
131-
window.removeEventListener('click', globalClickListener);
132-
removeNavigationListeners();
133-
removeFormsListeners();
113+
// loop over every listener added and remove them
114+
toRemove.forEach(({ eventType, listener }) => {
115+
// @ts-expect-error apparently you can't remove a custom listener to the window with TS
116+
window.removeEventListener(eventType, listener);
117+
});
134118
};
135-
});
119+
}
120+
121+
const removeNavigationListeners = createListeners(
122+
'navigation',
123+
['goto', 'invalidate', 'invalidateAll', 'pushState', 'replaceState'],
124+
true
125+
);
126+
const removeFormsListeners = createListeners('forms', ['enhance']);
127+
window.addEventListener('click', globalClickListener);
128+
129+
return () => {
130+
window.removeEventListener('click', globalClickListener);
131+
removeNavigationListeners();
132+
removeFormsListeners();
133+
};
134+
});
135+
136+
return Story();
137+
};
136138

137-
return Story();
138-
},
139-
];
139+
export const decorators: Decorator[] = [svelteKitMocksDecorator];

0 commit comments

Comments
 (0)
0