Skip to content

Commit

Permalink
Merge pull request #26688 from storybookjs/kasper/module-mocking
Browse files Browse the repository at this point in the history
Test: Support module mocking with conditional subpath imports in `package.json`
  • Loading branch information
kasperpeulen authored May 1, 2024
2 parents 4537146 + 0c2818a commit 0bf9744
Show file tree
Hide file tree
Showing 75 changed files with 1,888 additions and 441 deletions.
28 changes: 25 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<h1>Migration</h1>

- [From version 8.0 to 8.1.0](#from-version-80-to-810)
- [From version 8.0.x to 8.1.x](#from-version-80x-to-81x)
- [Subtitle block and `parameters.componentSubtitle`](#subtitle-block-and-parameterscomponentsubtitle)
- [Title block](#title-block)
- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- [Portable stories](#portable-stories)
- [@storybook/nextjs requires specific path aliases to be setup](#storybooknextjs-requires-specific-path-aliases-to-be-setup)
- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- [Portable stories](#portable-stories-1)
- [Project annotations are now merged instead of overwritten in composeStory](#project-annotations-are-now-merged-instead-of-overwritten-in-composestory)
- [Type change in `composeStories` API](#type-change-in-composestories-api)
- [Composed Vue stories are now components instead of functions](#composed-vue-stories-are-now-components-instead-of-functions)
Expand Down Expand Up @@ -406,7 +408,27 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)

## From version 8.0 to 8.1.0
## From version 8.0.x to 8.1.x

### Portable stories

#### @storybook/nextjs requires specific path aliases to be setup

In order to properly mock the `next/router`, `next/header`, `next/navigation` and `next/cache` APIs, the `@storybook/nextjs` framework includes internal Webpack aliases to those modules. If you use portable stories in your Jest tests, you should set the aliases in your Jest config files `moduleNameMapper` property using the `getPackageAliases` helper from `@storybook/nextjs/export-mocks`:

```js
const nextJest = require("next/jest.js");
const { getPackageAliases } = require("@storybook/nextjs/export-mocks");
const createJestConfig = nextJest();
const customJestConfig = {
moduleNameMapper: {
...getPackageAliases(), // Add aliases for @storybook/nextjs mocks
},
};
module.exports = createJestConfig(customJestConfig);
```

This will make sure you end using the correct implementation of the packages and avoid having issues in your tests.

### Subtitle block and `parameters.componentSubtitle`

Expand Down
75 changes: 43 additions & 32 deletions code/addons/actions/src/components/ActionLogger/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import type { FC, PropsWithChildren } from 'react';
import React, { Fragment } from 'react';
import { styled, withTheme } from '@storybook/theming';
import type { ElementRef, ReactNode } from 'react';
import React, { forwardRef, Fragment, useEffect, useRef } from 'react';
import type { Theme } from '@storybook/theming';
import { styled, withTheme } from '@storybook/theming';

import { Inspector } from 'react-inspector';
import { ActionBar, ScrollArea } from '@storybook/components';

import { Action, InspectorContainer, Counter } from './style';
import { Action, Counter, InspectorContainer } from './style';
import type { ActionDisplay } from '../../models';

const UnstyledWrapped: FC<PropsWithChildren<{ className?: string }>> = ({
children,
className,
}) => (
<ScrollArea horizontal vertical className={className}>
{children}
</ScrollArea>
const UnstyledWrapped = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, className }, ref) => (
<ScrollArea ref={ref} horizontal vertical className={className}>
{children}
</ScrollArea>
)
);
UnstyledWrapped.displayName = 'UnstyledWrapped';

export const Wrapper = styled(UnstyledWrapped)({
margin: 0,
padding: '10px 5px 20px',
Expand All @@ -39,24 +40,34 @@ interface ActionLoggerProps {
onClear: () => void;
}

export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => (
<Fragment>
<Wrapper>
{actions.map((action: ActionDisplay) => (
<Action key={action.id}>
{action.count > 1 && <Counter>{action.count}</Counter>}
<InspectorContainer>
<ThemedInspector
sortObjectKeys
showNonenumerable={false}
name={action.data.name}
data={action.data.args ?? action.data}
/>
</InspectorContainer>
</Action>
))}
</Wrapper>

<ActionBar actionItems={[{ title: 'Clear', onClick: onClear }]} />
</Fragment>
);
export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => {
const wrapperRef = useRef<ElementRef<typeof Wrapper>>(null);
const wrapper = wrapperRef.current;
const wasAtBottom = wrapper && wrapper.scrollHeight - wrapper.scrollTop === wrapper.clientHeight;

useEffect(() => {
// Scroll to bottom, when the action panel was already scrolled down
if (wasAtBottom) wrapperRef.current.scrollTop = wrapperRef.current.scrollHeight;
}, [wasAtBottom, actions.length]);

return (
<Fragment>
<Wrapper ref={wrapperRef}>
{actions.map((action: ActionDisplay) => (
<Action key={action.id}>
{action.count > 1 && <Counter>{action.count}</Counter>}
<InspectorContainer>
<ThemedInspector
sortObjectKeys
showNonenumerable={false}
name={action.data.name}
data={action.data.args ?? action.data}
/>
</InspectorContainer>
</Action>
))}
</Wrapper>
<ActionBar actionItems={[{ title: 'Clear', onClick: onClear }]} />
</Fragment>
);
};
4 changes: 2 additions & 2 deletions code/addons/actions/src/containers/ActionLogger/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ export default class ActionLogger extends Component<ActionLoggerProps, ActionLog
addAction = (action: ActionDisplay) => {
this.setState((prevState: ActionLoggerState) => {
const actions = [...prevState.actions];
const previous = actions.length && actions[0];
const previous = actions.length && actions[actions.length - 1];
if (previous && safeDeepEqual(previous.data, action.data)) {
previous.count++;
} else {
action.count = 1;
actions.unshift(action);
actions.push(action);
}
return { actions: actions.slice(0, action.options.limit) };
});
Expand Down
21 changes: 20 additions & 1 deletion code/addons/actions/src/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,26 @@ const logActionsWhenMockCalled: LoaderFunction = (context) => {
typeof global.__STORYBOOK_TEST_ON_MOCK_CALL__ === 'function'
) {
const onMockCall = global.__STORYBOOK_TEST_ON_MOCK_CALL__ as typeof onMockCallType;
onMockCall((mock, args) => action(mock.getMockName())(args));
onMockCall((mock, args) => {
const name = mock.getMockName();

// TODO: Make this a configurable API in 8.2
if (
!/^next\/.*::/.test(name) ||
[
'next/router::useRouter()',
'next/navigation::useRouter()',
'next/navigation::redirect',
'next/cache::',
'next/headers::cookies().set',
'next/headers::cookies().delete',
'next/headers::headers().set',
'next/headers::headers().delete',
].some((prefix) => name.startsWith(prefix))
) {
action(name)(args);
}
});
subscribed = true;
}
};
Expand Down
23 changes: 13 additions & 10 deletions code/addons/actions/template/stories/spies.stories.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { global as globalThis } from '@storybook/global';
import { spyOn } from '@storybook/test';

export default {
const meta = {
component: globalThis.Components.Button,
loaders() {
beforeEach() {
spyOn(console, 'log').mockName('console.log');
},
args: {
label: 'Button',
},
parameters: {
chromatic: { disable: true },
console.log('first');
},
};

export default meta;

export const ShowSpyOnInActions = {
parameters: {
chromatic: { disable: true },
},
beforeEach() {
console.log('second');
},
args: {
label: 'Button',
onClick: () => {
console.log('first');
console.log('second');
console.log('third');
},
},
};
2 changes: 1 addition & 1 deletion code/addons/links/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts"
},
"dependencies": {
"@storybook/csf": "^0.1.5",
"@storybook/csf": "^0.1.6",
"@storybook/global": "^5.0.0",
"ts-dedent": "^2.0.0"
},
Expand Down
1 change: 1 addition & 0 deletions code/builders/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export async function commonConfig(
base: './',
plugins: await pluginConfig(options),
resolve: {
conditions: ['storybook', 'stories', 'test'],
preserveSymlinks: isPreservingSymlinks(),
alias: {
assert: require.resolve('browser-assert'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ export async function createDefaultWebpackConfig(
},
resolve: {
...storybookBaseConfig.resolve,
// see https://github.com/webpack/webpack/issues/17692#issuecomment-1866272674 for the docs
conditionNames: [
...(storybookBaseConfig.resolve?.conditionNames ?? []),
'storybook',
'stories',
'test',
'...',
],
fallback: {
crypto: false,
assert: false,
Expand Down
4 changes: 2 additions & 2 deletions code/e2e-tests/framework-nextjs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test.describe('Next.js', () => {

await sbPage.viewAddonPanel('Actions');
const logItem = await page.locator('#storybook-panel-root #panel-tab-content', {
hasText: `nextNavigation.${action}`,
hasText: `useRouter().${action}`,
});
await expect(logItem).toBeVisible();
});
Expand Down Expand Up @@ -91,7 +91,7 @@ test.describe('Next.js', () => {

await sbPage.viewAddonPanel('Actions');
const logItem = await page.locator('#storybook-panel-root #panel-tab-content', {
hasText: `nextRouter.${action}`,
hasText: `useRouter().${action}`,
});
await expect(logItem).toBeVisible();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { SbPage } from './util';
const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001';
const templateName = process.env.STORYBOOK_TEMPLATE_NAME || '';

test.describe('preview-web', () => {
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

test.describe('preview-api', () => {
test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);

Expand Down Expand Up @@ -50,4 +52,40 @@ test.describe('preview-web', () => {
await sbPage.previewRoot().getByRole('button').getByText('Submit').first().press('Alt+s');
await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible();
});

// if rerenders were interleaved the button would have rendered "Error: Interleaved loaders. Changed arg"
test('should only render once at a time when rapidly changing args', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('lib/preview-api/rendering', 'slow-loader');

const root = sbPage.previewRoot();

const labelControl = await sbPage.page.locator('#control-label');

await expect(root.getByText('Loaded. Click me')).toBeVisible();
await expect(labelControl).toBeVisible();

await labelControl.fill('');
await labelControl.type('Changed arg', { delay: 50 });
await labelControl.blur();

await expect(root.getByText('Loaded. Changed arg')).toBeVisible();
});

test('should reload plage when remounting while loading', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('lib/preview-api/rendering', 'slow-loader');

const root = sbPage.previewRoot();

await expect(root.getByText('Loaded. Click me')).toBeVisible();

await sbPage.page.getByRole('button', { name: 'Remount component' }).click();
await wait(200);
await sbPage.page.getByRole('button', { name: 'Remount component' }).click();

// the loading spinner indicates the iframe is being fully reloaded
await expect(sbPage.previewIframe().locator('.sb-preparing-story > .sb-loader')).toBeVisible();
await expect(root.getByText('Loaded. Click me')).toBeVisible();
});
});
47 changes: 46 additions & 1 deletion code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,31 @@
"require": "./dist/next-image-loader-stub.js",
"import": "./dist/next-image-loader-stub.mjs"
},
"./export-mocks": {
"types": "./dist/export-mocks/index.d.ts",
"require": "./dist/export-mocks/index.js",
"import": "./dist/export-mocks/index.mjs"
},
"./cache.mock": {
"types": "./dist/export-mocks/cache/index.d.ts",
"require": "./dist/export-mocks/cache/index.js",
"import": "./dist/export-mocks/cache/index.mjs"
},
"./headers.mock": {
"types": "./dist/export-mocks/headers/index.d.ts",
"require": "./dist/export-mocks/headers/index.js",
"import": "./dist/export-mocks/headers/index.mjs"
},
"./navigation.mock": {
"types": "./dist/export-mocks/navigation/index.d.ts",
"require": "./dist/export-mocks/navigation/index.js",
"import": "./dist/export-mocks/navigation/index.mjs"
},
"./router.mock": {
"types": "./dist/export-mocks/router/index.d.ts",
"require": "./dist/export-mocks/router/index.js",
"import": "./dist/export-mocks/router/index.mjs"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
Expand All @@ -59,6 +84,21 @@
],
"dist/image-context": [
"dist/image-context.d.ts"
],
"export-mocks": [
"dist/export-mocks/index.d.ts"
],
"cache.mock": [
"dist/export-mocks/cache/index.d.ts"
],
"headers.mock": [
"dist/export-mocks/headers/index.d.ts"
],
"router.mock": [
"dist/export-mocks/router/index.d.ts"
],
"navigation.mock": [
"dist/export-mocks/navigation/index.d.ts"
]
}
},
Expand Down Expand Up @@ -89,14 +129,14 @@
"@babel/preset-typescript": "^7.23.2",
"@babel/runtime": "^7.23.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@storybook/addon-actions": "workspace:*",
"@storybook/builder-webpack5": "workspace:*",
"@storybook/core-common": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/preset-react-webpack": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/react": "workspace:*",
"@storybook/test": "workspace:*",
"@storybook/types": "workspace:*",
"@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
Expand Down Expand Up @@ -160,6 +200,11 @@
"./src/index.ts",
"./src/preset.ts",
"./src/preview.tsx",
"./src/export-mocks/index.ts",
"./src/export-mocks/cache/index.ts",
"./src/export-mocks/headers/index.ts",
"./src/export-mocks/router/index.ts",
"./src/export-mocks/navigation/index.ts",
"./src/next-image-loader-stub.ts",
"./src/images/decorator.tsx",
"./src/images/next-legacy-image.tsx",
Expand Down
Loading

0 comments on commit 0bf9744

Please sign in to comment.