diff --git a/.gitignore b/.gitignore index 63f9445af854..0d0e9ac8d8ce 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ test-results !/**/.yarn/plugins !/**/.yarn/sdks !/**/.yarn/versions +!/**/.yarn/patches /**/.pnp.* !/node_modules diff --git a/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch b/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch new file mode 100644 index 000000000000..175c8fbcc343 --- /dev/null +++ b/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch @@ -0,0 +1,37 @@ +diff --git a/dist/index.js b/dist/index.js +index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa23f21fdd2 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -6,23 +6,29 @@ import { processError } from '@vitest/utils/error'; + import { util } from 'chai'; + + const MATCHERS_OBJECT = Symbol.for("matchers-object"); +-const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object"); ++// Patched this symbol for storybook, so that @storybook/test can be used in a jest environment as well. ++// Otherwise, vitest will override global jest matchers, and crash. ++const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object-storybook"); + const GLOBAL_EXPECT = Symbol.for("expect-global"); + + if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { + const globalState = /* @__PURE__ */ new WeakMap(); +- const matchers = /* @__PURE__ */ Object.create(null); + Object.defineProperty(globalThis, MATCHERS_OBJECT, { + get: () => globalState + }); ++} ++ ++if (!Object.prototype.hasOwnProperty.call(globalThis, JEST_MATCHERS_OBJECT)) { ++ const matchers = /* @__PURE__ */ Object.create(null); + Object.defineProperty(globalThis, JEST_MATCHERS_OBJECT, { + configurable: true, + get: () => ({ +- state: globalState.get(globalThis[GLOBAL_EXPECT]), ++ state: globalThis[MATCHERS_OBJECT].get(globalThis[GLOBAL_EXPECT]), + matchers + }) + }); + } ++ + function getState(expect) { + return globalThis[MATCHERS_OBJECT].get(expect); + } diff --git a/code/addons/a11y/src/a11yRunner.ts b/code/addons/a11y/src/a11yRunner.ts index 4163017fd7f9..fb32e0f543a0 100644 --- a/code/addons/a11y/src/a11yRunner.ts +++ b/code/addons/a11y/src/a11yRunner.ts @@ -45,12 +45,18 @@ const run = async (storyId: string) => { } const result = await axe.run(htmlElement, options); + + // Axe result contains class instances, which telejson deserializes in a + // way that violates: + // Content Security Policy directive: "script-src 'self' 'unsafe-inline'". + const resultJson = JSON.parse(JSON.stringify(result)); + // It's possible that we requested a new run on a different story. // Unfortunately, axe doesn't support a cancel method to abort current run. // We check if the story we run against is still the current one, // if not, trigger a new run using the current story if (activeStoryId === storyId) { - channel.emit(EVENTS.RESULT, result); + channel.emit(EVENTS.RESULT, resultJson); } else { active = false; run(activeStoryId); diff --git a/code/addons/actions/src/addArgs.ts b/code/addons/actions/src/addArgs.ts index 5742bd8627c0..db14aee0ce3d 100644 --- a/code/addons/actions/src/addArgs.ts +++ b/code/addons/actions/src/addArgs.ts @@ -1,7 +1,12 @@ import type { ArgsEnhancer } from '@storybook/types'; -import { addActionsFromArgTypes, inferActionsFromArgTypesRegex } from './addArgsHelpers'; +import { + addActionsFromArgTypes, + attachActionsToFunctionMocks, + inferActionsFromArgTypesRegex, +} from './addArgsHelpers'; export const argsEnhancers: ArgsEnhancer[] = [ addActionsFromArgTypes, inferActionsFromArgTypesRegex, + attachActionsToFunctionMocks, ]; diff --git a/code/addons/actions/src/addArgsHelpers.ts b/code/addons/actions/src/addArgsHelpers.ts index 7f56922d3962..0dcb56c32f55 100644 --- a/code/addons/actions/src/addArgsHelpers.ts +++ b/code/addons/actions/src/addArgsHelpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle,no-param-reassign */ import type { Args, Renderer, ArgsEnhancer } from '@storybook/types'; import { action } from './runtime/action'; @@ -31,7 +32,7 @@ export const inferActionsFromArgTypesRegex: ArgsEnhancer = (context) = return argTypesMatchingRegex.reduce((acc, [name, argType]) => { if (isInInitialArgs(name, initialArgs)) { - acc[name] = action(name); + acc[name] = action(name, { implicit: true }); } return acc; }, {} as Args); @@ -61,3 +62,33 @@ export const addActionsFromArgTypes: ArgsEnhancer = (context) => { return acc; }, {} as Args); }; + +export const attachActionsToFunctionMocks: ArgsEnhancer = (context) => { + const { + initialArgs, + argTypes, + parameters: { actions }, + } = context; + if (actions?.disable || !argTypes) { + return {}; + } + + const argTypesWithAction = Object.entries(initialArgs).filter( + ([, value]) => + typeof value === 'function' && + '_isMockFunction' in value && + value._isMockFunction && + !value._actionAttached + ); + + return argTypesWithAction.reduce((acc, [key, value]) => { + const previous = value.getMockImplementation(); + value.mockImplementation((...args: unknown[]) => { + action(key)(...args); + return previous?.(...args); + }); + // this enhancer is being called multiple times + value._actionAttached = true; + return acc; + }, {} as Args); +}; diff --git a/code/addons/actions/src/models/ActionOptions.ts b/code/addons/actions/src/models/ActionOptions.ts index 6678e5138929..b503df069d5c 100644 --- a/code/addons/actions/src/models/ActionOptions.ts +++ b/code/addons/actions/src/models/ActionOptions.ts @@ -4,6 +4,7 @@ interface Options { depth: number; // backards compatibility, remove in 7.0 clearOnStoryChange: boolean; limit: number; + implicit: boolean; } export type ActionOptions = Partial & Partial; diff --git a/code/addons/actions/src/runtime/action.ts b/code/addons/actions/src/runtime/action.ts index d41e215d3fda..9cb6055a5feb 100644 --- a/code/addons/actions/src/runtime/action.ts +++ b/code/addons/actions/src/runtime/action.ts @@ -54,6 +54,23 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti }; const handler = function actionHandler(...args: any[]) { + // TODO: Enable once codemods are finished + // if (options.implicit) { + // const preview = + // '__STORYBOOK_PREVIEW__' in global + // ? (global.__STORYBOOK_PREVIEW__ as PreviewWeb) + // : undefined; + // if ( + // preview?.storyRenders.some( + // (render) => render.phase === 'playing' || render.phase === 'rendering' + // ) + // ) { + // console.warn( + // 'Can not use implicit actions during rendering or playing of a story.' + // ); + // } + // } + const channel = addons.getChannel(); // this makes sure that in js enviroments like react native you can still get an id const id = generateId(); diff --git a/code/addons/interactions/src/components/Interaction.tsx b/code/addons/interactions/src/components/Interaction.tsx index bacaea0fe358..af2bea6bd20b 100644 --- a/code/addons/interactions/src/components/Interaction.tsx +++ b/code/addons/interactions/src/components/Interaction.tsx @@ -4,7 +4,7 @@ import { type Call, CallStates, type ControlStates } from '@storybook/instrument import { styled, typography } from '@storybook/theming'; import { transparentize } from 'polished'; -import { MatcherResult } from './MatcherResult'; +import { Expected, MatcherResult, Received } from './MatcherResult'; import { MethodCall } from './MethodCall'; import { StatusIcon } from './StatusIcon'; @@ -120,6 +120,29 @@ const Exception = ({ exception }: { exception: Call['exception'] }) => { return (
{paragraphs[0]}
+ + {exception.showDiff && exception.diff ? ( + <> +
+ + + ) : ( +
+          
+ {exception.expected && ( + <> + Expected: +
+ + )} + {exception.actual && ( + <> + Received: +
+ + )} +
+ )} {more &&

See the full stack trace in the browser console.

}
); diff --git a/code/addons/interactions/src/components/MatcherResult.tsx b/code/addons/interactions/src/components/MatcherResult.tsx index a8a1e00a63f7..6f1d8aef9f1d 100644 --- a/code/addons/interactions/src/components/MatcherResult.tsx +++ b/code/addons/interactions/src/components/MatcherResult.tsx @@ -45,7 +45,13 @@ export const Expected = ({ value, parsed }: { value: any; parsed?: boolean }) => return {value}; }; -export const MatcherResult = ({ message }: { message: string }) => { +export const MatcherResult = ({ + message, + style = {}, +}: { + message: string; + style?: React.CSSProperties; +}) => { const lines = message.split('\n'); return (
 {
         margin: 0,
         padding: '8px 10px 8px 36px',
         fontSize: typography.size.s1,
+        ...style,
       }}
     >
       {lines.flatMap((line: string, index: number) => {
diff --git a/code/addons/interactions/src/preview.ts b/code/addons/interactions/src/preview.ts
index 8aa37ef431ea..54c7c18faab5 100644
--- a/code/addons/interactions/src/preview.ts
+++ b/code/addons/interactions/src/preview.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-param-reassign,no-underscore-dangle */
 /// 
 
 import { addons } from '@storybook/preview-api';
@@ -9,6 +10,7 @@ import type {
   PlayFunction,
   PlayFunctionContext,
   StepLabel,
+  Args,
 } from '@storybook/types';
 import { instrument } from '@storybook/instrumenter';
 import { ModuleMocker } from 'jest-mock';
@@ -30,14 +32,13 @@ const addSpies = (id: string, val: any, key?: string): any => {
   try {
     if (Object.prototype.toString.call(val) === '[object Object]') {
       // We have to mutate the original object for this to survive HMR.
-      // eslint-disable-next-line no-restricted-syntax, no-param-reassign
+      // eslint-disable-next-line no-restricted-syntax
       for (const [k, v] of Object.entries(val)) val[k] = addSpies(id, v, k);
       return val;
     }
     if (Array.isArray(val)) {
       return val.map((item, index) => addSpies(id, item, `${key}[${index}]`));
     }
-    // eslint-disable-next-line no-underscore-dangle
     if (typeof val === 'function' && val.isAction && !val._isMockFunction) {
       Object.defineProperty(val, 'name', { value: key, writable: false });
       Object.defineProperty(val, '__storyId__', { value: id, writable: false });
@@ -54,7 +55,25 @@ const addSpies = (id: string, val: any, key?: string): any => {
 const addActionsFromArgTypes: ArgsEnhancer = ({ id, initialArgs }) =>
   addSpies(id, initialArgs);
 
-export const argsEnhancers = [addActionsFromArgTypes];
+const instrumentSpies: ArgsEnhancer = ({ initialArgs }) => {
+  const argTypesWithAction = Object.entries(initialArgs).filter(
+    ([, value]) =>
+      typeof value === 'function' &&
+      '_isMockFunction' in value &&
+      value._isMockFunction &&
+      !value._instrumented
+  );
+
+  return argTypesWithAction.reduce((acc, [key, value]) => {
+    const instrumented = instrument({ [key]: () => value }, { retain: true })[key];
+    acc[key] = instrumented();
+    // this enhancer is being called multiple times
+    value._instrumented = true;
+    return acc;
+  }, {} as Args);
+};
+
+export const argsEnhancers = [addActionsFromArgTypes, instrumentSpies];
 
 export const { step: runStep } = instrument(
   {
diff --git a/code/builders/builder-vite/README.md b/code/builders/builder-vite/README.md
index ce675ab3f580..1e5026e36443 100644
--- a/code/builders/builder-vite/README.md
+++ b/code/builders/builder-vite/README.md
@@ -9,7 +9,6 @@ Build your stories with [vite](https://vitejs.dev/) for fast startup times and n
   - [Getting started with Vite and Storybook (on a new project)](#getting-started-with-vite-and-storybook-on-a-new-project)
   - [Migration from webpack / CRA](#migration-from-webpack--cra)
   - [Customize Vite config](#customize-vite-config)
-  - [Svelte Options](#svelte-options)
   - [TypeScript](#typescript)
   - [React Docgen](#react-docgen)
   - [Note about working directory](#note-about-working-directory)
@@ -113,10 +112,6 @@ The `configType` variable will be either `"DEVELOPMENT"` or `"PRODUCTION"`.
 
 The function should return the updated Vite configuration.
 
-### Svelte Options
-
-When using this builder with Svelte, your `svelte.config.js` file will be used automatically.
-
 ### TypeScript
 
 Configure your `.storybook/main.ts` to use TypeScript:
diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json
index 76fdfcd405e1..d5ba68b05c9f 100644
--- a/code/frameworks/nextjs/package.json
+++ b/code/frameworks/nextjs/package.json
@@ -119,13 +119,13 @@
     "@types/babel__core": "^7",
     "@types/babel__plugin-transform-runtime": "^7",
     "@types/babel__preset-env": "^7",
-    "next": "13.5.4",
+    "next": "^14.0.0",
     "typescript": "^4.9.3",
     "webpack": "^5.65.0"
   },
   "peerDependencies": {
-    "@next/font": "^13.0.0",
-    "next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0",
+    "@next/font": "^13.0.0|| ^14.0.0",
+    "next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
     "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
     "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
     "webpack": "^5.0.0"
diff --git a/code/frameworks/nextjs/src/images/webpack.ts b/code/frameworks/nextjs/src/images/webpack.ts
index e6b91f0712d3..e80e03545beb 100644
--- a/code/frameworks/nextjs/src/images/webpack.ts
+++ b/code/frameworks/nextjs/src/images/webpack.ts
@@ -20,7 +20,7 @@ const configureImageDefaults = (baseConfig: WebpackConfig): void => {
     'next/image': path.resolve(__dirname, './images/next-image'),
   };
 
-  if (semver.satisfies(version, '^13.0.0')) {
+  if (semver.satisfies(version, '>=13.0.0')) {
     resolve.alias = {
       ...resolve.alias,
       'sb-original/next/legacy/image': require.resolve('next/legacy/image'),
diff --git a/code/frameworks/nextjs/src/nextImport/webpack.ts b/code/frameworks/nextjs/src/nextImport/webpack.ts
index b017462ae256..fc5d359ef8e2 100644
--- a/code/frameworks/nextjs/src/nextImport/webpack.ts
+++ b/code/frameworks/nextjs/src/nextImport/webpack.ts
@@ -7,20 +7,11 @@ export function configureNextImport(baseConfig: WebpackConfig) {
   const nextJSVersion = getNextjsVersion();
 
   const isNext12 = semver.satisfies(nextJSVersion, '~12');
-  const isNext13 = semver.satisfies(nextJSVersion, '~13');
   const isNextVersionSmallerThan12dot2 = semver.lt(nextJSVersion, '12.2.0');
   const isNextVersionSmallerThan13 = semver.lt(nextJSVersion, '13.0.0');
 
   baseConfig.plugins = baseConfig.plugins ?? [];
 
-  if (!isNext13) {
-    baseConfig.plugins.push(
-      new IgnorePlugin({
-        resourceRegExp: /next\/legacy\/image$/,
-      })
-    );
-  }
-
   if (!isNext12 || isNextVersionSmallerThan12dot2) {
     baseConfig.plugins.push(
       new IgnorePlugin({
@@ -32,7 +23,8 @@ export function configureNextImport(baseConfig: WebpackConfig) {
   if (isNextVersionSmallerThan13) {
     baseConfig.plugins.push(
       new IgnorePlugin({
-        resourceRegExp: /next\/dist\/shared\/lib\/hooks-client-context$/,
+        // ignore next/dist/shared/lib/hooks-client-context and next/legacy/image imports
+        resourceRegExp: /(next\/dist\/shared\/lib\/hooks-client-context|next\/legacy\/image)$/,
       })
     );
   }
diff --git a/code/jest.config.base.js b/code/jest.config.base.js
index 9fdc75f588a9..89ff7acf0b8b 100644
--- a/code/jest.config.base.js
+++ b/code/jest.config.base.js
@@ -23,6 +23,7 @@ const modulesToTransform = [
   '@angular',
   '@lit',
   '@mdx-js',
+  '@vitest',
   'ccount',
   'character-entities',
   'decode-named-character-reference',
@@ -60,6 +61,8 @@ module.exports = {
       path.resolve('./__mocks__/fileMock.js'),
     '\\.(css|scss|stylesheet)$': path.resolve('./__mocks__/styleMock.js'),
     '\\.(md)$': path.resolve('./__mocks__/htmlMock.js'),
+    '@vitest/utils/(.*)': '@vitest/utils/dist/$1.js',
+    '@vitest/utils': '@vitest/utils/dist/index.js',
   },
   transform: {
     '^.+\\.(t|j)sx?$': ['@swc/jest', swcrc],
diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json
index 2ab38d5a8770..37c76c998109 100644
--- a/code/lib/instrumenter/package.json
+++ b/code/lib/instrumenter/package.json
@@ -48,7 +48,9 @@
     "@storybook/client-logger": "workspace:*",
     "@storybook/core-events": "workspace:*",
     "@storybook/global": "^5.0.0",
-    "@storybook/preview-api": "workspace:*"
+    "@storybook/preview-api": "workspace:*",
+    "@vitest/utils": "^0.34.6",
+    "util": "^0.12.4"
   },
   "devDependencies": {
     "typescript": "~4.9.3"
diff --git a/code/lib/instrumenter/src/instrumenter.test.ts b/code/lib/instrumenter/src/instrumenter.test.ts
index eadb1f7d5c84..35f0b6a87830 100644
--- a/code/lib/instrumenter/src/instrumenter.test.ts
+++ b/code/lib/instrumenter/src/instrumenter.test.ts
@@ -112,6 +112,44 @@ describe('Instrumenter', () => {
     expect(result.fn1.fn2.__originalFn__).toBe(fn1.fn2);
   });
 
+  it('patches functions correctly that reference this', () => {
+    const object = {
+      name: 'name',
+      method() {
+        return this.name;
+      },
+    };
+
+    const instrumented = instrument(object);
+    expect(object.method()).toEqual(instrumented.method());
+
+    expect(instrumented.method).toEqual(expect.any(Function));
+    expect(instrumented.method.__originalFn__).toBe(object.method);
+  });
+
+  it('patches functions correctly that use proxies', () => {
+    const object = new Proxy(
+      {
+        name: 'name',
+        method() {
+          return this.name;
+        },
+      },
+      {
+        get(target, prop, receiver) {
+          if (prop === 'name') return `${target[prop]}!`;
+          return Reflect.get(target, prop, receiver);
+        },
+      }
+    );
+
+    const instrumented = instrument(object);
+    expect(object.method()).toEqual(instrumented.method());
+
+    expect(instrumented.method).toEqual(expect.any(Function));
+    expect(instrumented.method.__originalFn__).toBe(object.method);
+  });
+
   it('patched functions call the original function when invoked', () => {
     const { fn } = instrument({ fn: jest.fn() });
     const obj = {};
@@ -510,12 +548,12 @@ describe('Instrumenter', () => {
       expect(callSpy).toHaveBeenCalledWith(
         expect.objectContaining({
           id: 'kind--story [0] fn',
-          exception: {
+          exception: expect.objectContaining({
             name: 'Error',
             message: 'Boom!',
             stack: expect.stringContaining('Error: Boom!'),
             callId: 'kind--story [0] fn',
-          },
+          }),
         })
       );
     });
diff --git a/code/lib/instrumenter/src/instrumenter.ts b/code/lib/instrumenter/src/instrumenter.ts
index 357b9df52817..0ffaa0ca0d5f 100644
--- a/code/lib/instrumenter/src/instrumenter.ts
+++ b/code/lib/instrumenter/src/instrumenter.ts
@@ -1,4 +1,4 @@
-/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-underscore-dangle,no-param-reassign */
 import type { Channel } from '@storybook/channels';
 import { addons } from '@storybook/preview-api';
 import type { StoryId } from '@storybook/types';
@@ -10,6 +10,7 @@ import {
   STORY_RENDER_PHASE_CHANGED,
 } from '@storybook/core-events';
 import { global } from '@storybook/global';
+import { processError } from '@vitest/utils/error';
 
 import type { Call, CallRef, ControlStates, LogItem, Options, State, SyncPayload } from './types';
 import { CallStates } from './types';
@@ -24,8 +25,8 @@ export const EVENTS = {
   END: 'storybook/instrumenter/end',
 };
 
-type PatchedObj = {
-  [Property in keyof TObj]: TObj[Property] & { __originalFn__: PatchedObj };
+type PatchedObj> = {
+  [Property in keyof TObj]: TObj[Property] & { __originalFn__: TObj[Property] };
 };
 
 const controlsDisabled: ControlStates = {
@@ -49,7 +50,6 @@ const isInstrumentable = (o: unknown) => {
   if (o.constructor === undefined) return true;
   const proto = o.constructor.prototype;
   if (!isObject(proto)) return false;
-  if (Object.prototype.hasOwnProperty.call(proto, 'isPrototypeOf') === false) return false;
   return true;
 };
 
@@ -290,28 +290,46 @@ export class Instrumenter {
   // Traverses the object structure to recursively patch all function properties.
   // Returns the original object, or a new object with the same constructor,
   // depending on whether it should mutate.
-  instrument(obj: TObj, options: Options): PatchedObj {
-    if (!isInstrumentable(obj)) return obj;
+  instrument>(
+    obj: TObj,
+    options: Options,
+    depth = 0
+  ): PatchedObj {
+    if (!isInstrumentable(obj)) return obj as PatchedObj;
 
     const { mutate = false, path = [] } = options;
-    return Object.keys(obj).reduce(
+
+    const keys = options.getKeys ? options.getKeys(obj, depth) : Object.keys(obj);
+    depth += 1;
+    return keys.reduce(
       (acc, key) => {
+        const descriptor = getPropertyDescriptor(obj, key);
+        if (typeof descriptor?.get === 'function') {
+          const getter = () => descriptor?.get?.bind(obj)?.();
+          Object.defineProperty(acc, key, {
+            get: () => {
+              return this.instrument(getter(), { ...options, path: path.concat(key) }, depth);
+            },
+          });
+          return acc;
+        }
+
         const value = (obj as Record)[key];
 
         // Nothing to patch, but might be instrumentable, so we recurse
         if (typeof value !== 'function') {
-          acc[key] = this.instrument(value, { ...options, path: path.concat(key) });
+          acc[key] = this.instrument(value, { ...options, path: path.concat(key) }, depth);
           return acc;
         }
 
         // Already patched, so we pass through unchanged
-        if (typeof value.__originalFn__ === 'function') {
+        if ('__originalFn__' in value && typeof value.__originalFn__ === 'function') {
           acc[key] = value;
           return acc;
         }
 
         // Patch the function and mark it "patched" by adding a reference to the original function
-        acc[key] = (...args: any[]) => this.track(key, value, args, options);
+        acc[key] = (...args: any[]) => this.track(key, value, obj, args, options);
         acc[key].__originalFn__ = value;
 
         // Reuse the original name as the patched function's name
@@ -321,7 +339,7 @@ export class Instrumenter {
         if (Object.keys(value).length > 0) {
           Object.assign(
             acc[key],
-            this.instrument({ ...value }, { ...options, path: path.concat(key) })
+            this.instrument({ ...value }, { ...options, path: path.concat(key) }, depth)
           );
         }
 
@@ -334,7 +352,13 @@ export class Instrumenter {
   // Monkey patch an object method to record calls.
   // Returns a function that invokes the original function, records the invocation ("call") and
   // returns the original result.
-  track(method: string, fn: Function, args: any[], options: Options) {
+  track(
+    method: string,
+    fn: Function,
+    object: Record,
+    args: any[],
+    options: Options
+  ) {
     const storyId: StoryId =
       args?.[0]?.__storyId__ || global.__STORYBOOK_PREVIEW__?.selectionStore?.selection?.storyId;
     const { cursor, ancestors } = this.getState(storyId);
@@ -344,11 +368,11 @@ export class Instrumenter {
     const interceptable = typeof intercept === 'function' ? intercept(method, path) : intercept;
     const call = { id, cursor, storyId, ancestors, path, method, args, interceptable, retain };
     const interceptOrInvoke = interceptable && !ancestors.length ? this.intercept : this.invoke;
-    const result = interceptOrInvoke.call(this, fn, call, options);
+    const result = interceptOrInvoke.call(this, fn, object, call, options);
     return this.instrument(result, { ...options, mutate: true, path: [{ __callId__: call.id }] });
   }
 
-  intercept(fn: Function, call: Call, options: Options) {
+  intercept(fn: Function, object: Record, call: Call, options: Options) {
     const { chainedCallIds, isDebugging, playUntil } = this.getState(call.storyId);
 
     // For a "jump to step" action, continue playing until we hit a call by that ID.
@@ -358,7 +382,7 @@ export class Instrumenter {
       if (playUntil === call.id) {
         this.setState(call.storyId, { playUntil: undefined });
       }
-      return this.invoke(fn, call, options);
+      return this.invoke(fn, object, call, options);
     }
 
     // Instead of invoking the function, defer the function call until we continue playing.
@@ -373,11 +397,11 @@ export class Instrumenter {
         const { [call.id]: _, ...resolvers } = state.resolvers;
         return { isLocked: true, resolvers };
       });
-      return this.invoke(fn, call, options);
+      return this.invoke(fn, object, call, options);
     });
   }
 
-  invoke(fn: Function, call: Call, options: Options) {
+  invoke(fn: Function, object: Record, call: Call, options: Options) {
     // TODO this doesnt work because the abortSignal we have here is the newly created one
     // const { abortSignal } = global.window.__STORYBOOK_PREVIEW__ || {};
     // if (abortSignal && abortSignal.aborted) throw IGNORED_EXCEPTION;
@@ -443,7 +467,16 @@ export class Instrumenter {
     const handleException = (e: any) => {
       if (e instanceof Error) {
         const { name, message, stack, callId = call.id } = e as Error & { callId: Call['id'] };
-        const exception = { name, message, stack, callId };
+
+        // This will calculate the diff for chai errors
+        const {
+          showDiff = undefined,
+          diff = undefined,
+          actual = undefined,
+          expected = undefined,
+        } = processError(e);
+
+        const exception = { name, message, stack, callId, showDiff, diff, actual, expected };
         this.update({ ...info, status: CallStates.ERROR, exception });
 
         // Always track errors to their originating call.
@@ -510,7 +543,7 @@ export class Instrumenter {
         };
       });
 
-      const result = fn(...finalArgs);
+      const result = fn.apply(object, finalArgs);
 
       // Track the result so we can trace later uses of it back to the originating call.
       // Primitive results (undefined, null, boolean, string, number, BigInt) are ignored.
@@ -637,3 +670,15 @@ export function instrument>(
     return obj;
   }
 }
+
+function getPropertyDescriptor(obj: T, propName: keyof T) {
+  let target = obj;
+  while (target != null) {
+    const descriptor = Object.getOwnPropertyDescriptor(target, propName);
+    if (descriptor) {
+      return descriptor;
+    }
+    target = Object.getPrototypeOf(target);
+  }
+  return undefined;
+}
diff --git a/code/lib/instrumenter/src/types.ts b/code/lib/instrumenter/src/types.ts
index 1076d4dd3a1d..f3a2ee274200 100644
--- a/code/lib/instrumenter/src/types.ts
+++ b/code/lib/instrumenter/src/types.ts
@@ -16,6 +16,10 @@ export interface Call {
     message: Error['message'];
     stack: Error['stack'];
     callId: Call['id'];
+    showDiff?: boolean;
+    diff?: string;
+    actual?: unknown;
+    expected?: unknown;
   };
 }
 
@@ -90,4 +94,5 @@ export interface Options {
   mutate?: boolean;
   path?: Array;
   getArgs?: (call: Call, state: State) => Call['args'];
+  getKeys?: (originalObject: Record, depth: number) => string[];
 }
diff --git a/code/lib/test/jest.config.js b/code/lib/test/jest.config.js
new file mode 100644
index 000000000000..4396fbc7010d
--- /dev/null
+++ b/code/lib/test/jest.config.js
@@ -0,0 +1,7 @@
+const path = require('path');
+const baseConfig = require('../../jest.config.browser');
+
+module.exports = {
+  ...baseConfig,
+  displayName: __dirname.split(path.sep).slice(-2).join(path.posix.sep),
+};
diff --git a/code/lib/test/package.json b/code/lib/test/package.json
new file mode 100644
index 000000000000..73b04a6e593f
--- /dev/null
+++ b/code/lib/test/package.json
@@ -0,0 +1,79 @@
+{
+  "name": "@storybook/test",
+  "version": "7.4.0-alpha.0",
+  "description": "",
+  "keywords": [
+    "storybook"
+  ],
+  "homepage": "https://github.com/storybookjs/storybook/tree/next/code/lib/test",
+  "bugs": {
+    "url": "https://github.com/storybookjs/storybook/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/storybookjs/storybook.git",
+    "directory": "code/lib/test"
+  },
+  "funding": {
+    "type": "opencollective",
+    "url": "https://opencollective.com/storybook"
+  },
+  "license": "MIT",
+  "sideEffects": false,
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "node": "./dist/index.js",
+      "require": "./dist/index.js",
+      "import": "./dist/index.mjs"
+    },
+    "./package.json": "./package.json"
+  },
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "types": "dist/index.d.ts",
+  "files": [
+    "dist/**/*",
+    "README.md",
+    "*.js",
+    "*.d.ts"
+  ],
+  "scripts": {
+    "check": "../../../scripts/prepare/check.ts",
+    "prep": "../../../scripts/prepare/bundle.ts"
+  },
+  "dependencies": {
+    "@storybook/client-logger": "workspace:*",
+    "@storybook/core-events": "workspace:*",
+    "@storybook/instrumenter": "workspace:*",
+    "@storybook/preview-api": "workspace:*",
+    "@testing-library/dom": "^9.3.1",
+    "@testing-library/jest-dom": "^6.1.3",
+    "@testing-library/user-event": "^14.4.3",
+    "@types/chai": "^4",
+    "@vitest/expect": "^0.34.2",
+    "@vitest/spy": "^0.34.1",
+    "chai": "^4.3.7",
+    "util": "^0.12.4"
+  },
+  "devDependencies": {
+    "ts-dedent": "^2.2.0",
+    "type-fest": "~2.19",
+    "typescript": "~4.9.3"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "bundler": {
+    "entries": [
+      "./src/index.ts"
+    ],
+    "noExternal": [
+      "@testing-library/dom",
+      "@testing-library/jest-dom",
+      "@testing-library/user-event",
+      "chai"
+    ]
+  },
+  "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17"
+}
diff --git a/code/lib/test/project.json b/code/lib/test/project.json
new file mode 100644
index 000000000000..68c18c664fd3
--- /dev/null
+++ b/code/lib/test/project.json
@@ -0,0 +1,6 @@
+{
+  "name": "@storybook/test",
+  "$schema": "../../node_modules/nx/schemas/project-schema.json",
+  "implicitDependencies": [],
+  "type": "library"
+}
diff --git a/code/lib/test/src/expect.ts b/code/lib/test/src/expect.ts
new file mode 100644
index 000000000000..d277380a3596
--- /dev/null
+++ b/code/lib/test/src/expect.ts
@@ -0,0 +1,134 @@
+import * as chai from 'chai';
+import type {
+  AsymmetricMatchersContaining,
+  ExpectStatic,
+  JestAssertion,
+  MatchersObject,
+  MatcherState,
+} from '@vitest/expect';
+import {
+  getState,
+  GLOBAL_EXPECT,
+  JestAsymmetricMatchers,
+  JestChaiExpect,
+  JestExtend,
+  setState,
+} from '@vitest/expect';
+import * as matchers from '@testing-library/jest-dom/matchers';
+import type { TestingLibraryMatchers } from '@testing-library/jest-dom/types/matchers';
+import type { PromisifyObject } from './utils';
+
+// We only expose the jest compatible API for now
+export interface Assertion
+  extends PromisifyObject>,
+    TestingLibraryMatchers, Promise> {
+  toHaveBeenCalledOnce(): Promise;
+  toSatisfy(matcher: (value: E) => boolean, message?: string): Promise;
+  resolves: Assertion;
+  rejects: Assertion;
+  not: Assertion;
+}
+
+export interface Expect extends AsymmetricMatchersContaining {
+  (actual: T, message?: string): Assertion;
+  unreachable(message?: string): Promise;
+  soft(actual: T, message?: string): Assertion;
+  extend(expects: MatchersObject): void;
+  assertions(expected: number): Promise;
+  hasAssertions(): Promise;
+  anything(): any;
+  any(constructor: unknown): any;
+  getState(): MatcherState;
+  setState(state: Partial): void;
+  not: AsymmetricMatchersContaining;
+}
+
+export function createExpect() {
+  chai.use(JestExtend);
+  chai.use(JestChaiExpect);
+  chai.use(JestAsymmetricMatchers);
+
+  const expect = ((value: unknown, message?: string) => {
+    const { assertionCalls } = getState(expect);
+    setState({ assertionCalls: assertionCalls + 1, soft: false }, expect);
+    return chai.expect(value, message);
+  }) as ExpectStatic;
+
+  Object.assign(expect, chai.expect);
+
+  // The below methods are added to make chai jest compatible
+
+  expect.getState = () => getState(expect);
+  expect.setState = (state) => setState(state as Partial, expect);
+
+  // @ts-expect-error chai.extend is not typed
+  expect.extend = (expects: MatchersObject) => chai.expect.extend(expect, expects);
+
+  expect.soft = (...args) => {
+    const assert = expect(...args);
+    expect.setState({
+      soft: true,
+    });
+    return assert;
+  };
+
+  expect.unreachable = (message?: string): never => {
+    chai.assert.fail(`expected${message ? ` "${message}" ` : ' '}not to be reached`);
+  };
+
+  function assertions(expected: number) {
+    const errorGen = () =>
+      new Error(
+        `expected number of assertions to be ${expected}, but got ${
+          expect.getState().assertionCalls
+        }`
+      );
+    if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(errorGen(), assertions);
+
+    expect.setState({
+      expectedAssertionsNumber: expected,
+      expectedAssertionsNumberErrorGen: errorGen,
+    });
+  }
+
+  function hasAssertions() {
+    const error = new Error('expected any number of assertion, but got none');
+    if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(error, hasAssertions);
+
+    expect.setState({
+      isExpectingAssertions: true,
+      isExpectingAssertionsError: error,
+    });
+  }
+
+  setState(
+    {
+      // this should also add "snapshotState" that is added conditionally
+      assertionCalls: 0,
+      isExpectingAssertions: false,
+      isExpectingAssertionsError: null,
+      expectedAssertionsNumber: null,
+      expectedAssertionsNumberErrorGen: null,
+    },
+    expect
+  );
+
+  chai.util.addMethod(expect, 'assertions', assertions);
+  chai.util.addMethod(expect, 'hasAssertions', hasAssertions);
+  expect.extend(matchers);
+
+  return expect as unknown as Expect;
+}
+
+const expect = createExpect();
+
+// @vitest/expect expects this to be set
+Object.defineProperty(globalThis, GLOBAL_EXPECT, {
+  value: expect,
+  writable: true,
+  configurable: true,
+});
+
+export { expect };
diff --git a/code/lib/test/src/index.ts b/code/lib/test/src/index.ts
new file mode 100644
index 000000000000..34d59391676e
--- /dev/null
+++ b/code/lib/test/src/index.ts
@@ -0,0 +1,34 @@
+import { instrument } from '@storybook/instrumenter';
+import * as spy from '@vitest/spy';
+import chai from 'chai';
+import { FORCE_REMOUNT, STORY_RENDER_PHASE_CHANGED } from '@storybook/core-events';
+import { addons } from '@storybook/preview-api';
+import { expect as rawExpect } from './expect';
+
+export * from '@vitest/spy';
+
+const channel = addons.getChannel();
+
+channel.on(FORCE_REMOUNT, () => spy.spies.forEach((mock) => mock.mockClear()));
+channel.on(STORY_RENDER_PHASE_CHANGED, ({ newPhase }) => {
+  if (newPhase === 'loading') spy.spies.forEach((mock) => mock.mockClear());
+});
+
+export const { expect } = instrument(
+  { expect: rawExpect },
+  {
+    getKeys: (obj: Record, depth) => {
+      const privateApi = ['assert', '__methods', '__flags', '_obj'];
+      if (obj.constructor === chai.Assertion) {
+        const keys = Object.keys(Object.getPrototypeOf(obj)).filter(
+          (it) => !privateApi.includes(it)
+        );
+        return depth > 2 ? keys : [...keys, 'not'];
+      }
+      return Object.keys(obj);
+    },
+    intercept: (method) => method !== 'expect',
+  }
+);
+
+export * from './testing-library';
diff --git a/code/lib/test/src/testing-library.ts b/code/lib/test/src/testing-library.ts
new file mode 100644
index 000000000000..ccac1f448923
--- /dev/null
+++ b/code/lib/test/src/testing-library.ts
@@ -0,0 +1,108 @@
+/* eslint-disable @typescript-eslint/ban-types */
+import { once } from '@storybook/client-logger';
+import { instrument } from '@storybook/instrumenter';
+import * as domTestingLibrary from '@testing-library/dom';
+import _userEvent from '@testing-library/user-event';
+import dedent from 'ts-dedent';
+import type { FireFunction, FireObject } from '@testing-library/dom/types/events';
+import type { Writable } from 'type-fest';
+import type { Promisify, PromisifyObject } from './utils';
+
+type TestingLibraryDom = typeof domTestingLibrary;
+
+const testingLibrary = instrument(
+  { ...domTestingLibrary },
+  {
+    intercept: (method, path) =>
+      path[0] === 'fireEvent' || method.startsWith('find') || method.startsWith('waitFor'),
+  }
+) as {} as Writable> & {
+  fireEvent: Promisify & PromisifyObject;
+};
+
+testingLibrary.screen = new Proxy(testingLibrary.screen, {
+  get(target, prop, receiver) {
+    once.warn(dedent`
+          You are using Testing Library's \`screen\` object. Use \`within(canvasElement)\` instead.
+          More info: https://storybook.js.org/docs/react/essentials/interactions
+        `);
+    return Reflect.get(target, prop, receiver);
+  },
+});
+
+export const {
+  buildQueries,
+  configure,
+  createEvent,
+  fireEvent,
+  findAllByAltText,
+  findAllByDisplayValue,
+  findAllByLabelText,
+  findAllByPlaceholderText,
+  findAllByRole,
+  findAllByTestId,
+  findAllByText,
+  findAllByTitle,
+  findByAltText,
+  findByDisplayValue,
+  findByLabelText,
+  findByPlaceholderText,
+  findByRole,
+  findByTestId,
+  findByText,
+  findByTitle,
+  getAllByAltText,
+  getAllByDisplayValue,
+  getAllByLabelText,
+  getAllByPlaceholderText,
+  getAllByRole,
+  getAllByTestId,
+  getAllByText,
+  getAllByTitle,
+  getByAltText,
+  getByDisplayValue,
+  getByLabelText,
+  getByPlaceholderText,
+  getByRole,
+  getByTestId,
+  getByText,
+  getByTitle,
+  getConfig,
+  getDefaultNormalizer,
+  getElementError,
+  getNodeText,
+  getQueriesForElement,
+  getRoles,
+  getSuggestedQuery,
+  isInaccessible,
+  logDOM,
+  logRoles,
+  prettyDOM,
+  queries,
+  queryAllByAltText,
+  queryAllByAttribute,
+  queryAllByDisplayValue,
+  queryAllByLabelText,
+  queryAllByPlaceholderText,
+  queryAllByRole,
+  queryAllByTestId,
+  queryAllByText,
+  queryAllByTitle,
+  queryByAltText,
+  queryByAttribute,
+  queryByDisplayValue,
+  queryByLabelText,
+  queryByPlaceholderText,
+  queryByRole,
+  queryByTestId,
+  queryByText,
+  queryByTitle,
+  queryHelpers,
+  screen,
+  waitFor,
+  waitForElementToBeRemoved,
+  within,
+  prettyFormat,
+} = testingLibrary;
+
+export const { userEvent } = instrument({ userEvent: _userEvent }, { intercept: true });
diff --git a/code/lib/test/src/utils.ts b/code/lib/test/src/utils.ts
new file mode 100644
index 000000000000..6f093cd0b9f4
--- /dev/null
+++ b/code/lib/test/src/utils.ts
@@ -0,0 +1,5 @@
+export type Promisify = Fn extends (...args: infer A) => infer R
+  ? (...args: A) => R extends Promise ? R : Promise
+  : Fn;
+
+export type PromisifyObject = { [K in keyof O]: Promisify };
diff --git a/code/lib/test/tsconfig.json b/code/lib/test/tsconfig.json
new file mode 100644
index 000000000000..52d43eaaa9b9
--- /dev/null
+++ b/code/lib/test/tsconfig.json
@@ -0,0 +1,4 @@
+{
+  "extends": "../../tsconfig.json",
+  "include": ["src/**/*"]
+}
diff --git a/code/package.json b/code/package.json
index c9ea0be5b379..97c99ba5ed1b 100644
--- a/code/package.json
+++ b/code/package.json
@@ -80,10 +80,10 @@
   ],
   "resolutions": {
     "@playwright/test": "1.36.0",
-    "@testing-library/jest-dom": "^5.11.9",
     "@typescript-eslint/eslint-plugin": "^5.45.0",
     "@typescript-eslint/experimental-utils": "^5.45.0",
     "@typescript-eslint/parser": "^5.45.0",
+    "@vitest/expect@^0.34.2": "patch:@vitest/expect@npm%3A0.34.5#./.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch",
     "esbuild": "^0.18.0",
     "eslint": "^8.28.0",
     "playwright": "1.36.0",
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index 4dbbad62aa47..cfb4b0f0b4ce 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -77,6 +77,7 @@
   },
   "devDependencies": {
     "@babel/core": "^7.22.9",
+    "@storybook/test": "workspace:*",
     "@types/util-deprecate": "^1.0.0",
     "expect-type": "^0.15.0",
     "jest-specific-snapshot": "^8.0.0",
diff --git a/code/renderers/react/src/public-types.test.tsx b/code/renderers/react/src/public-types.test.tsx
index 1fc3fd15d10c..ea4beb7810c0 100644
--- a/code/renderers/react/src/public-types.test.tsx
+++ b/code/renderers/react/src/public-types.test.tsx
@@ -7,6 +7,8 @@ import type { KeyboardEventHandler, ReactNode } from 'react';
 import React from 'react';
 
 import type { SetOptional } from 'type-fest';
+import type { Mock } from '@storybook/test';
+import { fn } from '@storybook/test';
 
 import type { Decorator, Meta, StoryObj } from './public-types';
 import type { ReactRenderer } from './types';
@@ -300,3 +302,29 @@ test('Meta is broken when using discriminating types, issue #23629', () => {
     },
   }).toMatchTypeOf>();
 });
+
+test('Infer mock function given to args in meta.', () => {
+  type Props = { label: string; onClick: () => void; onRender: () => JSX.Element };
+  const TestButton = (props: Props) => <>;
+
+  const meta = {
+    component: TestButton,
+    args: { label: 'label', onClick: fn(), onRender: () => <>some jsx },
+  } satisfies Meta;
+
+  type Story = StoryObj;
+
+  const Basic: Story = {
+    play: async ({ args }) => {
+      expectTypeOf(args.onClick).toEqualTypeOf>();
+      expectTypeOf(args.onRender).toEqualTypeOf<() => JSX.Element>();
+    },
+  };
+  type Expected = StoryAnnotations<
+    ReactRenderer,
+    Props & { onClick: Mock<[], void> },
+    Partial
+  >;
+
+  expectTypeOf(Basic).toEqualTypeOf();
+});
diff --git a/code/renderers/react/src/public-types.ts b/code/renderers/react/src/public-types.ts
index 592b82b03ff9..95ad7111a3ea 100644
--- a/code/renderers/react/src/public-types.ts
+++ b/code/renderers/react/src/public-types.ts
@@ -56,7 +56,7 @@ export type StoryObj = [TMetaOrCmpOrArgs] extends [
     > extends infer TArgs
     ? StoryAnnotations<
         ReactRenderer,
-        TArgs,
+        AddMocks,
         SetOptional)>
       >
     : never
@@ -64,6 +64,16 @@ export type StoryObj = [TMetaOrCmpOrArgs] extends [
   ? StoryAnnotations>
   : StoryAnnotations;
 
+// This performs a downcast to function types that are mocks, when a mock fn is given to meta args.
+type AddMocks = Simplify<{
+  [T in keyof TArgs]: T extends keyof DefaultArgs
+    ? // eslint-disable-next-line @typescript-eslint/ban-types
+      DefaultArgs[T] extends (...args: any) => any & { mock: {} } // allow any function with a mock object
+      ? DefaultArgs[T]
+      : TArgs[T]
+    : TArgs[T];
+}>;
+
 type ActionArgs = {
   // This can be read as: filter TArgs on functions where we can assign a void function to that function.
   // The docs addon argsEnhancers can only safely provide a default value for void functions.
diff --git a/code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx b/code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx
index fb0f444720c3..b45015c94b50 100644
--- a/code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx
+++ b/code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx
@@ -1,5 +1,4 @@
 /// ;
-/// ;
 import React from 'react';
 import type { Meta, StoryObj } from '@storybook/react';
 import { userEvent, within } from '@storybook/testing-library';
diff --git a/code/ui/manager/package.json b/code/ui/manager/package.json
index f8f56fef6500..02b19b41ce34 100644
--- a/code/ui/manager/package.json
+++ b/code/ui/manager/package.json
@@ -61,6 +61,7 @@
     "@storybook/global": "^5.0.0",
     "@storybook/manager-api": "workspace:*",
     "@storybook/router": "workspace:*",
+    "@storybook/test": "workspace:*",
     "@storybook/theming": "workspace:*",
     "@storybook/types": "workspace:*",
     "@testing-library/react": "^11.2.2",
diff --git a/code/yarn.lock b/code/yarn.lock
index ac5d3a3cadb9..e985d4e8c31e 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -24,7 +24,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@adobe/css-tools@npm:^4.0.1":
+"@adobe/css-tools@npm:^4.0.1, @adobe/css-tools@npm:^4.3.0":
   version: 4.3.1
   resolution: "@adobe/css-tools@npm:4.3.1"
   checksum: 05672719b544cc0c21ae3ed0eb6349bf458e9d09457578eeeb07cf0f696469ac6417e9c9be1b129e5d6a18098a061c1db55b2275591760ef30a79822436fcbfa
@@ -3973,72 +3973,72 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@next/env@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/env@npm:13.5.4"
-  checksum: 69c013047371bde6c4dc6d03ec77140059bd4e3db38c1991a8aa8a9c8ce4d1370b98a141145a6f60e23f32ce97a3040b448bfd0455b0d9e5ba6efda8df33c89f
+"@next/env@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/env@npm:14.0.0"
+  checksum: c43e81dbd162a29a4b380342e416209d69d731e8ced7688d09668ec8196f543e358ed65adad81a26e943c63a293d7a018552f8389b6b1ac95cd0f63f4ef257c0
   languageName: node
   linkType: hard
 
-"@next/swc-darwin-arm64@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-darwin-arm64@npm:13.5.4"
+"@next/swc-darwin-arm64@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-darwin-arm64@npm:14.0.0"
   conditions: os=darwin & cpu=arm64
   languageName: node
   linkType: hard
 
-"@next/swc-darwin-x64@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-darwin-x64@npm:13.5.4"
+"@next/swc-darwin-x64@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-darwin-x64@npm:14.0.0"
   conditions: os=darwin & cpu=x64
   languageName: node
   linkType: hard
 
-"@next/swc-linux-arm64-gnu@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-linux-arm64-gnu@npm:13.5.4"
+"@next/swc-linux-arm64-gnu@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-linux-arm64-gnu@npm:14.0.0"
   conditions: os=linux & cpu=arm64 & libc=glibc
   languageName: node
   linkType: hard
 
-"@next/swc-linux-arm64-musl@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-linux-arm64-musl@npm:13.5.4"
+"@next/swc-linux-arm64-musl@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-linux-arm64-musl@npm:14.0.0"
   conditions: os=linux & cpu=arm64 & libc=musl
   languageName: node
   linkType: hard
 
-"@next/swc-linux-x64-gnu@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-linux-x64-gnu@npm:13.5.4"
+"@next/swc-linux-x64-gnu@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-linux-x64-gnu@npm:14.0.0"
   conditions: os=linux & cpu=x64 & libc=glibc
   languageName: node
   linkType: hard
 
-"@next/swc-linux-x64-musl@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-linux-x64-musl@npm:13.5.4"
+"@next/swc-linux-x64-musl@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-linux-x64-musl@npm:14.0.0"
   conditions: os=linux & cpu=x64 & libc=musl
   languageName: node
   linkType: hard
 
-"@next/swc-win32-arm64-msvc@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-win32-arm64-msvc@npm:13.5.4"
+"@next/swc-win32-arm64-msvc@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-win32-arm64-msvc@npm:14.0.0"
   conditions: os=win32 & cpu=arm64
   languageName: node
   linkType: hard
 
-"@next/swc-win32-ia32-msvc@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-win32-ia32-msvc@npm:13.5.4"
+"@next/swc-win32-ia32-msvc@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-win32-ia32-msvc@npm:14.0.0"
   conditions: os=win32 & cpu=ia32
   languageName: node
   linkType: hard
 
-"@next/swc-win32-x64-msvc@npm:13.5.4":
-  version: 13.5.4
-  resolution: "@next/swc-win32-x64-msvc@npm:13.5.4"
+"@next/swc-win32-x64-msvc@npm:14.0.0":
+  version: 14.0.0
+  resolution: "@next/swc-win32-x64-msvc@npm:14.0.0"
   conditions: os=win32 & cpu=x64
   languageName: node
   linkType: hard
@@ -6733,12 +6733,12 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@storybook/client-logger@npm:7.5.0":
-  version: 7.5.0
-  resolution: "@storybook/client-logger@npm:7.5.0"
+"@storybook/client-logger@npm:7.4.6":
+  version: 7.4.6
+  resolution: "@storybook/client-logger@npm:7.4.6"
   dependencies:
     "@storybook/global": "npm:^5.0.0"
-  checksum: 90326c49a224bf21680c04ffee94725bf75658086093ccb839a8aae39476929c4719eafb18e498a148cf0dd956d4e9a5d3b2a34d09ca4fd25e2af553458558ac
+  checksum: 170ad58c17e2608639533fe24aaa96ddd4d77d23b4b28f265b2cb67510fef966fc20b029e070fdc7216ba1cdb724d1210b2f8edc8aa538de32fd6e549f9010cf
   languageName: node
   linkType: hard
 
@@ -7166,7 +7166,9 @@ __metadata:
     "@storybook/core-events": "workspace:*"
     "@storybook/global": "npm:^5.0.0"
     "@storybook/preview-api": "workspace:*"
+    "@vitest/utils": "npm:^0.34.6"
     typescript: "npm:~4.9.3"
+    util: "npm:^0.12.4"
   languageName: unknown
   linkType: soft
 
@@ -7254,6 +7256,7 @@ __metadata:
     "@storybook/global": "npm:^5.0.0"
     "@storybook/manager-api": "workspace:*"
     "@storybook/router": "workspace:*"
+    "@storybook/test": "workspace:*"
     "@storybook/theming": "workspace:*"
     "@storybook/types": "workspace:*"
     "@testing-library/react": "npm:^11.2.2"
@@ -7323,7 +7326,7 @@ __metadata:
     fs-extra: "npm:^11.1.0"
     image-size: "npm:^1.0.0"
     loader-utils: "npm:^3.2.0"
-    next: "npm:13.5.4"
+    next: "npm:^14.0.0"
     node-polyfill-webpack-plugin: "npm:^2.0.1"
     pnp-webpack-plugin: "npm:^1.7.0"
     postcss: "npm:^8.4.21"
@@ -7339,8 +7342,8 @@ __metadata:
     typescript: "npm:^4.9.3"
     webpack: "npm:^5.65.0"
   peerDependencies:
-    "@next/font": ^13.0.0
-    next: ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0
+    "@next/font": ^13.0.0|| ^14.0.0
+    next: ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0
     react: ^16.8.0 || ^17.0.0 || ^18.0.0
     react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     webpack: ^5.0.0
@@ -7750,6 +7753,7 @@ __metadata:
     "@storybook/global": "npm:^5.0.0"
     "@storybook/preview-api": "workspace:*"
     "@storybook/react-dom-shim": "workspace:*"
+    "@storybook/test": "workspace:*"
     "@storybook/types": "workspace:*"
     "@types/escodegen": "npm:^0.0.6"
     "@types/estree": "npm:^0.0.51"
@@ -8148,6 +8152,28 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@storybook/test@workspace:*, @storybook/test@workspace:lib/test":
+  version: 0.0.0-use.local
+  resolution: "@storybook/test@workspace:lib/test"
+  dependencies:
+    "@storybook/client-logger": "workspace:*"
+    "@storybook/core-events": "workspace:*"
+    "@storybook/instrumenter": "workspace:*"
+    "@storybook/preview-api": "workspace:*"
+    "@testing-library/dom": "npm:^9.3.1"
+    "@testing-library/jest-dom": "npm:^6.1.3"
+    "@testing-library/user-event": "npm:^14.4.3"
+    "@types/chai": "npm:^4"
+    "@vitest/expect": "npm:^0.34.2"
+    "@vitest/spy": "npm:^0.34.1"
+    chai: "npm:^4.3.7"
+    ts-dedent: "npm:^2.2.0"
+    type-fest: "npm:~2.19"
+    typescript: "npm:~4.9.3"
+    util: "npm:^0.12.4"
+  languageName: unknown
+  linkType: soft
+
 "@storybook/testing-library@npm:next":
   version: 0.2.2-next.0
   resolution: "@storybook/testing-library@npm:0.2.2-next.0"
@@ -8160,17 +8186,17 @@ __metadata:
   linkType: hard
 
 "@storybook/theming@npm:^7.0.2":
-  version: 7.5.0
-  resolution: "@storybook/theming@npm:7.5.0"
+  version: 7.4.6
+  resolution: "@storybook/theming@npm:7.4.6"
   dependencies:
     "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.0.0"
-    "@storybook/client-logger": "npm:7.5.0"
+    "@storybook/client-logger": "npm:7.4.6"
     "@storybook/global": "npm:^5.0.0"
     memoizerific: "npm:^1.11.3"
   peerDependencies:
     react: ^16.8.0 || ^17.0.0 || ^18.0.0
     react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
-  checksum: 57da8e27c748cbec4dc1661cdd2d449949d97476d8e97933696b31d07c7361cbbcca8d7225cc00ca078daa160023b8965ddec7c23519ce0a4ef2658246b062e7
+  checksum: 6250a413c346971792623bf5a907811fc009ff4a36b8f292d0f45c677269b2a50c29d84ab1e869ada7df3eb23d49614e1342bd2c88e71d4467702b92ebc42f2d
   languageName: node
   linkType: hard
 
@@ -8743,7 +8769,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@testing-library/dom@npm:^9.0.0":
+"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.1":
   version: 9.3.3
   resolution: "@testing-library/dom@npm:9.3.3"
   dependencies:
@@ -8776,6 +8802,36 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@testing-library/jest-dom@npm:^6.1.2, @testing-library/jest-dom@npm:^6.1.3":
+  version: 6.1.3
+  resolution: "@testing-library/jest-dom@npm:6.1.3"
+  dependencies:
+    "@adobe/css-tools": "npm:^4.3.0"
+    "@babel/runtime": "npm:^7.9.2"
+    aria-query: "npm:^5.0.0"
+    chalk: "npm:^3.0.0"
+    css.escape: "npm:^1.5.1"
+    dom-accessibility-api: "npm:^0.5.6"
+    lodash: "npm:^4.17.15"
+    redent: "npm:^3.0.0"
+  peerDependencies:
+    "@jest/globals": ">= 28"
+    "@types/jest": ">= 28"
+    jest: ">= 28"
+    vitest: ">= 0.32"
+  peerDependenciesMeta:
+    "@jest/globals":
+      optional: true
+    "@types/jest":
+      optional: true
+    jest:
+      optional: true
+    vitest:
+      optional: true
+  checksum: 544e01939d3c14a3d44ae2e2bb9fe2a0cb5a9e4992ca2728f41188fb9fb2d56e25f1a2e1c12000be2a94d8da36cb220b24020e1b5c5c4c4bede9058a0d80583d
+  languageName: node
+  linkType: hard
+
 "@testing-library/react@npm:^11.2.2":
   version: 11.2.7
   resolution: "@testing-library/react@npm:11.2.7"
@@ -8800,7 +8856,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@testing-library/user-event@npm:^14.4.0":
+"@testing-library/user-event@npm:^14.4.0, @testing-library/user-event@npm:^14.4.3":
   version: 14.5.1
   resolution: "@testing-library/user-event@npm:14.5.1"
   peerDependencies:
@@ -8977,6 +9033,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/chai@npm:^4":
+  version: 4.3.6
+  resolution: "@types/chai@npm:4.3.6"
+  checksum: 388af382b11453a69808800479dcaff0323a0d1e15df1619175ebd55b294d716d560058f560ed55434e8846af46f017d7d78544822571f6322d3fac6d5f8a29d
+  languageName: node
+  linkType: hard
+
 "@types/cheerio@npm:^0.22.22":
   version: 0.22.32
   resolution: "@types/cheerio@npm:0.22.32"
@@ -10189,6 +10252,68 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/expect@npm:0.34.5":
+  version: 0.34.5
+  resolution: "@vitest/expect@npm:0.34.5"
+  dependencies:
+    "@vitest/spy": "npm:0.34.5"
+    "@vitest/utils": "npm:0.34.5"
+    chai: "npm:^4.3.7"
+  checksum: dc30a5e1f2732a1906df57f65381df1129dbf994496734c27e4a3f832852862501eaba1ec2987215ec12ee23a8f2ef1d8ff63c7cd5490046a7a26800da1adcb2
+  languageName: node
+  linkType: hard
+
+"@vitest/expect@patch:@vitest/expect@npm%3A0.34.5#./.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch::locator=%40storybook%2Froot%40workspace%3A.":
+  version: 0.34.5
+  resolution: "@vitest/expect@patch:@vitest/expect@npm%3A0.34.5#./.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch::version=0.34.5&hash=f89b80&locator=%40storybook%2Froot%40workspace%3A."
+  dependencies:
+    "@vitest/spy": "npm:0.34.5"
+    "@vitest/utils": "npm:0.34.5"
+    chai: "npm:^4.3.7"
+  checksum: b08f0b1df6a37305f3f68feec15cfac048ca9e3924998698625394296faac4e539e23d7422eec59c0850a83b7342b574a2d2d174aaa33a7eb0004e4e366c515c
+  languageName: node
+  linkType: hard
+
+"@vitest/spy@npm:0.34.5":
+  version: 0.34.5
+  resolution: "@vitest/spy@npm:0.34.5"
+  dependencies:
+    tinyspy: "npm:^2.1.1"
+  checksum: bbee495ca6300f50dde6418d14db0d3281daf38df15abae95202ddef253d6dd8bedf9f4a79da5a2246d3758ab24aa737caccf752fabcd8ba902a4f14801c2a0c
+  languageName: node
+  linkType: hard
+
+"@vitest/spy@npm:^0.34.1":
+  version: 0.34.7
+  resolution: "@vitest/spy@npm:0.34.7"
+  dependencies:
+    tinyspy: "npm:^2.1.1"
+  checksum: 1150b270eb72a5e8e7da997bcba90ebe5ed2ac50de1ea1f81738e16a19ab4bc77ca4d17639988df65695d4b325fe3647a1e4204d01024bcf5ecac8ba7764a2cc
+  languageName: node
+  linkType: hard
+
+"@vitest/utils@npm:0.34.5":
+  version: 0.34.5
+  resolution: "@vitest/utils@npm:0.34.5"
+  dependencies:
+    diff-sequences: "npm:^29.4.3"
+    loupe: "npm:^2.3.6"
+    pretty-format: "npm:^29.5.0"
+  checksum: 99cc5974ada1dab2b02220005c0fc97147baba175601a0faa1b2b6687c7f579d21a401077377d6f759b3aa8a07dcc8851cdc3e07f9a550ec289286107487ac36
+  languageName: node
+  linkType: hard
+
+"@vitest/utils@npm:^0.34.6":
+  version: 0.34.7
+  resolution: "@vitest/utils@npm:0.34.7"
+  dependencies:
+    diff-sequences: "npm:^29.4.3"
+    loupe: "npm:^2.3.6"
+    pretty-format: "npm:^29.5.0"
+  checksum: 5f26ec5b4a53709a50efdb57aa753e8090b3411e888774f67a0d192eb7f046ed5fcc6884eb3d6275d2674926e724b731e8d28cd3cea96a7f3d27462a0d44af9e
+  languageName: node
+  linkType: hard
+
 "@volar/language-core@npm:1.10.1, @volar/language-core@npm:~1.10.0":
   version: 1.10.1
   resolution: "@volar/language-core@npm:1.10.1"
@@ -11514,6 +11639,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"assertion-error@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "assertion-error@npm:1.1.0"
+  checksum: 25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b
+  languageName: node
+  linkType: hard
+
 "assign-symbols@npm:^1.0.0":
   version: 1.0.0
   resolution: "assign-symbols@npm:1.0.0"
@@ -12942,6 +13074,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"chai@npm:^4.3.7":
+  version: 4.3.10
+  resolution: "chai@npm:4.3.10"
+  dependencies:
+    assertion-error: "npm:^1.1.0"
+    check-error: "npm:^1.0.3"
+    deep-eql: "npm:^4.1.3"
+    get-func-name: "npm:^2.0.2"
+    loupe: "npm:^2.3.6"
+    pathval: "npm:^1.1.1"
+    type-detect: "npm:^4.0.8"
+  checksum: c887d24f67be6fb554c7ebbde3bb0568697a8833d475e4768296916891ba143f25fc079f6eb34146f3dd5a3279d34c1f387c32c9a6ab288e579f948d9ccf53fe
+  languageName: node
+  linkType: hard
+
 "chalk@npm:4.1.0":
   version: 4.1.0
   resolution: "chalk@npm:4.1.0"
@@ -13069,6 +13216,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"check-error@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "check-error@npm:1.0.3"
+  dependencies:
+    get-func-name: "npm:^2.0.2"
+  checksum: 94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841
+  languageName: node
+  linkType: hard
+
 "checkup@npm:^1.3.0":
   version: 1.3.0
   resolution: "checkup@npm:1.3.0"
@@ -14566,6 +14722,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"deep-eql@npm:^4.1.3":
+  version: 4.1.3
+  resolution: "deep-eql@npm:4.1.3"
+  dependencies:
+    type-detect: "npm:^4.0.0"
+  checksum: ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd
+  languageName: node
+  linkType: hard
+
 "deep-equal@npm:^1.1.1":
   version: 1.1.1
   resolution: "deep-equal@npm:1.1.1"
@@ -14879,7 +15044,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"diff-sequences@npm:^29.6.3":
+"diff-sequences@npm:^29.4.3, diff-sequences@npm:^29.6.3":
   version: 29.6.3
   resolution: "diff-sequences@npm:29.6.3"
   checksum: 32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2
@@ -17837,6 +18002,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"get-func-name@npm:^2.0.0, get-func-name@npm:^2.0.2":
+  version: 2.0.2
+  resolution: "get-func-name@npm:2.0.2"
+  checksum: 89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df
+  languageName: node
+  linkType: hard
+
 "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1":
   version: 1.2.1
   resolution: "get-intrinsic@npm:1.2.1"
@@ -22191,6 +22363,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"loupe@npm:^2.3.6":
+  version: 2.3.6
+  resolution: "loupe@npm:2.3.6"
+  dependencies:
+    get-func-name: "npm:^2.0.0"
+  checksum: a974841ce94ef2a35aac7144e7f9e789e3887f82286cd9ffe7ff00f2ac9d117481989948657465e2b0b102f23136d89ae0a18fd4a32d9015012cd64464453289
+  languageName: node
+  linkType: hard
+
 "lower-case@npm:^2.0.2":
   version: 2.0.2
   resolution: "lower-case@npm:2.0.2"
@@ -23937,20 +24118,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"next@npm:13.5.4":
-  version: 13.5.4
-  resolution: "next@npm:13.5.4"
-  dependencies:
-    "@next/env": "npm:13.5.4"
-    "@next/swc-darwin-arm64": "npm:13.5.4"
-    "@next/swc-darwin-x64": "npm:13.5.4"
-    "@next/swc-linux-arm64-gnu": "npm:13.5.4"
-    "@next/swc-linux-arm64-musl": "npm:13.5.4"
-    "@next/swc-linux-x64-gnu": "npm:13.5.4"
-    "@next/swc-linux-x64-musl": "npm:13.5.4"
-    "@next/swc-win32-arm64-msvc": "npm:13.5.4"
-    "@next/swc-win32-ia32-msvc": "npm:13.5.4"
-    "@next/swc-win32-x64-msvc": "npm:13.5.4"
+"next@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "next@npm:14.0.0"
+  dependencies:
+    "@next/env": "npm:14.0.0"
+    "@next/swc-darwin-arm64": "npm:14.0.0"
+    "@next/swc-darwin-x64": "npm:14.0.0"
+    "@next/swc-linux-arm64-gnu": "npm:14.0.0"
+    "@next/swc-linux-arm64-musl": "npm:14.0.0"
+    "@next/swc-linux-x64-gnu": "npm:14.0.0"
+    "@next/swc-linux-x64-musl": "npm:14.0.0"
+    "@next/swc-win32-arm64-msvc": "npm:14.0.0"
+    "@next/swc-win32-ia32-msvc": "npm:14.0.0"
+    "@next/swc-win32-x64-msvc": "npm:14.0.0"
     "@swc/helpers": "npm:0.5.2"
     busboy: "npm:1.6.0"
     caniuse-lite: "npm:^1.0.30001406"
@@ -23988,7 +24169,7 @@ __metadata:
       optional: true
   bin:
     next: dist/bin/next
-  checksum: 0b0bc7fa42844859a0444a79122a48b5e65116c30ce077a3edaaecd7cee1d7925214a659391ae6ecf8dc612869a7a646ab3a1a8aa12d074ff17e3f18c53a2621
+  checksum: cfb18a72d6e1d875efb1bb3806f9a06551f482c5cb87231e77e179a71d26f3d43700290988ad27e739302bfa7ff8ac8081aafd5456c39a2819fdd315617e5acf
   languageName: node
   linkType: hard
 
@@ -25653,6 +25834,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"pathval@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "pathval@npm:1.1.1"
+  checksum: f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc
+  languageName: node
+  linkType: hard
+
 "pbkdf2@npm:^3.0.3":
   version: 3.1.2
   resolution: "pbkdf2@npm:3.1.2"
@@ -26243,7 +26431,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0":
+"pretty-format@npm:^29.0.0, pretty-format@npm:^29.5.0, pretty-format@npm:^29.7.0":
   version: 29.7.0
   resolution: "pretty-format@npm:29.7.0"
   dependencies:
@@ -30450,6 +30638,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tinyspy@npm:^2.1.1":
+  version: 2.2.0
+  resolution: "tinyspy@npm:2.2.0"
+  checksum: 8c7b70748dd8590e85d52741db79243746c15bc03c92d75c23160a762142db577e7f53e360ba7300e321b12bca5c42dd2522a8dbeec6ba3830302573dd8516bc
+  languageName: node
+  linkType: hard
+
 "tmp@npm:0.0.28":
   version: 0.0.28
   resolution: "tmp@npm:0.0.28"
@@ -30901,7 +31096,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"type-detect@npm:4.0.8":
+"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.8":
   version: 4.0.8
   resolution: "type-detect@npm:4.0.8"
   checksum: 8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd
diff --git a/docs/api/main-config-vite-final.md b/docs/api/main-config-vite-final.md
index c21ba01da4b0..02073acee85f 100644
--- a/docs/api/main-config-vite-final.md
+++ b/docs/api/main-config-vite-final.md
@@ -6,7 +6,7 @@ Parent: [main.js|ts configuration](./main-config.md)
 
 Type: `(config: Vite.InlineConfig, options: Options) => Vite.InlineConfig | Promise`
 
-Customize Storybook's Vite setup when using the [vite builder](../builders/vite.md).
+Customize Storybook's Vite setup when using the [Vite builder](../builders/vite.md).
 
 
 
diff --git a/docs/builders/vite.md b/docs/builders/vite.md
index d964931f66ab..819c4278ed48 100644
--- a/docs/builders/vite.md
+++ b/docs/builders/vite.md
@@ -39,9 +39,9 @@ Update your Storybook configuration (in `.storybook/main.js|ts`) to include the
 
 ## Configuration
 
-Out of the box, Storybook's Vite builder includes a set of configuration defaults for the supported frameworks, which are merged alongside your existing configuration file. For an optimal experience when using the Vite builder, we recommend applying any configuration directly inside Vite's configuration file (i.e., [`vite.config.js`](https://vitejs.dev/config/)).
+Out of the box, Storybook's Vite builder includes a set of configuration defaults for the supported frameworks, which are merged alongside your existing configuration file. For an optimal experience when using the Vite builder, we recommend applying any configuration directly inside Vite's configuration file (i.e., [`vite.config.js|ts`](https://vitejs.dev/config/)).
 
-When Storybook loads, it automatically merges the configuration into its own. However, not all projects have the same requirements, and you may need to provide a custom configuration created specifically for Storybook. In that case, you can adjust your configuration file (.storybook/main.js|ts) and add the `viteFinal` configuration function as follows:
+When Storybook loads, it automatically merges the configuration into its own. However, since different projects may have specific requirements, you may need to provide a custom configuration for Storybook. In such cases, you can modify your configuration file (`.storybook/main.js|ts`) and add the `viteFinal` configuration function as follows:
 
 
 
@@ -53,20 +53,42 @@ When Storybook loads, it automatically merges the configuration into its own. Ho
 
 
 
-The asynchronous function `viteFinal` receives a `config` object with the default builder configuration and returns the updated configuration.
+The asynchronous function [`viteFinal`](../api/main-config-vite-final.md) receives a `config` object with the default builder configuration and returns the updated configuration.
 
-You can also override the builder's configuration based on the environment. For instance, if you need to provide a custom configuration for development purposes and another for production, you can extend the default configuration as follows:
+### Environment-based configuration
+
+If you need to customize the builder's configuration and apply specific options based on your environment, extend the `viteFinal` function as follows:
 
 
 
 
 
 
 
+### Override the default configuration
+
+By default, the Vite builder in Storybook searches for the Vite configuration file in the root directory of your Storybook project. However, you can customize it to look for the configuration file in a different location. For example:
+
+
+
+
+
+
+
+
+ +💡 If you do not want Storybook to load the Vite configuration file automatically, you can use the `viteConfigPath` option to point to a non-existent file. + +
+ ### TypeScript If you need, you can also configure Storybook's Vite builder using TypeScript. Rename your `.storybook/main.js` to `.storybook/main.ts` and adjust it as follows: @@ -104,6 +126,20 @@ Currently, [automatic argType inference](../api/argtypes.md#automatic-argtype-in +### Interaction tests not working as expected + +If you are migrating from a Webpack-based project, such as [CRA](https://create-react-app.dev/), to Vite, and you have enabled Interaction testing with the [`@storybook/addon-interactions`](https://storybook.js.org/addons/@storybook/addon-interactions) addon, you may run into a situation where your tests fail to execute notifying you that the `window` object is not defined. To resolve this issue, you can create a `preview-head.html` file in your Storybook configuration directory and include the following: + + + + + + + #### Learn more about builders - Vite builder for bundling with Vite diff --git a/docs/snippets/common/main-config-builder-custom-config.js.mdx b/docs/snippets/common/main-config-builder-custom-config.js.mdx new file mode 100644 index 000000000000..032d8ea6334b --- /dev/null +++ b/docs/snippets/common/main-config-builder-custom-config.js.mdx @@ -0,0 +1,15 @@ +```js +// .storybook/main.js|ts + +export default { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + core: { + builder: { + name: '@storybook/builder-vite', + options: { + viteConfigPath: '../customVite.config.js', + }, + }, + }, +}; +``` diff --git a/docs/snippets/common/main-config-core-builder.ts-4-9.mdx b/docs/snippets/common/main-config-core-builder.ts-4-9.mdx new file mode 100644 index 000000000000..e28b069e3f12 --- /dev/null +++ b/docs/snippets/common/main-config-core-builder.ts-4-9.mdx @@ -0,0 +1,21 @@ +```ts +// .storybook/main.ts + +// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite) +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + framework: '@storybook/your-framework', + core: { + builder: { + name: '@storybook/builder-vite', + options: { + viteConfigPath: '../../../vite.config.js', + }, + }, + }, +}; + +export default config; +``` diff --git a/docs/snippets/common/main-config-vite-final-env.js.mdx b/docs/snippets/common/main-config-vite-final-env.js.mdx new file mode 100644 index 000000000000..73926f408471 --- /dev/null +++ b/docs/snippets/common/main-config-vite-final-env.js.mdx @@ -0,0 +1,23 @@ +```js +// .storybook/main.js|ts + +import { mergeConfig } from 'vite'; + +export default { + stories: ['../src/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + core: { + builder: '@storybook/builder-vite', + }, + async viteFinal(config, { configType }) { + if (configType === 'DEVELOPMENT') { + // Your development configuration goes here + } + if (configType === 'PRODUCTION') { + // Your production configuration goes here. + } + return mergeConfig(config, { + // Your environment configuration here + }); + }, +}; +``` diff --git a/docs/snippets/common/main-config-vite-final.ts-4-9.mdx b/docs/snippets/common/main-config-vite-final.ts-4-9.mdx new file mode 100644 index 000000000000..42d7f8cf8b9d --- /dev/null +++ b/docs/snippets/common/main-config-vite-final.ts-4-9.mdx @@ -0,0 +1,27 @@ +```ts +// .storybook/main.ts + +// Replace your-framework with the framework you are using (e.g., react-vite, vue3-vite) +import type { StorybookConfig } from '@storybook/your-framework'; + +import { mergeConfig } from 'vite'; + +const config: StorybookConfig = { + // Replace your-framework with the framework you are using (e.g., react-vite, vue3-vite) + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + async viteFinal(config, { configType }) { + if (configType === 'DEVELOPMENT') { + // Your development configuration goes here + } + if (configType === 'PRODUCTION') { + // Your production configuration goes here. + } + return mergeConfig(config, { + // Your environment configuration here + }); + }, +}; + +export default config; +``` diff --git a/docs/snippets/common/storybook-vite-builder-jest-mock.html.mdx b/docs/snippets/common/storybook-vite-builder-jest-mock.html.mdx new file mode 100644 index 000000000000..f7dd5c715015 --- /dev/null +++ b/docs/snippets/common/storybook-vite-builder-jest-mock.html.mdx @@ -0,0 +1,7 @@ +```html + + + +``` diff --git a/docs/snippets/common/storybook-vite-builder-react-docgen.js.mdx b/docs/snippets/common/storybook-vite-builder-react-docgen.js.mdx index 63dcbfaea126..99ef2c6a0372 100644 --- a/docs/snippets/common/storybook-vite-builder-react-docgen.js.mdx +++ b/docs/snippets/common/storybook-vite-builder-react-docgen.js.mdx @@ -1,5 +1,5 @@ ```js -// .storybook/main.js +// .storybook/main.js|ts export default { stories: ['../src/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], diff --git a/docs/snippets/common/storybook-vite-builder-ts-configure.ts-4-9.mdx b/docs/snippets/common/storybook-vite-builder-ts-configure.ts-4-9.mdx new file mode 100644 index 000000000000..397e526c0b0a --- /dev/null +++ b/docs/snippets/common/storybook-vite-builder-ts-configure.ts-4-9.mdx @@ -0,0 +1,17 @@ +```ts +// .storybook/main.ts + +// Replace your-framework with the framework you are using (e.g., react-vite, vue3-vite) +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + async viteFinal(config, options) { + // Add your configuration here + return config; + }, +}; + +export default config; +``` diff --git a/docs/writing-tests/stories-in-unit-tests.md b/docs/writing-tests/stories-in-unit-tests.md index 156294d5e480..f96a72c9fef2 100644 --- a/docs/writing-tests/stories-in-unit-tests.md +++ b/docs/writing-tests/stories-in-unit-tests.md @@ -172,26 +172,35 @@ If you intend to test multiple stories in a single test, use the `composeStories Storybook provides community-led addons for other frameworks like [Vue 2](https://storybook.js.org/addons/@storybook/testing-vue) and [Angular](https://storybook.js.org/addons/@storybook/testing-angular). However, these addons still lack support for the latest stable Storybook release. If you're interested in helping out, we recommend reaching out to the maintainers using the default communication channels (GitHub and [Discord server](https://discord.com/channels/486522875931656193/839297503446695956)). -### The args are not being passed to the test - +### The args are not being passed to the test + The components returned by `composeStories` or `composeStory` not only can be rendered as React components but also come with the combined properties from the story, meta, and global configuration. This means that if you want to access args or parameters, for instance, you can do so: + + + + + + -When using the `composeStories` or `composeStory` functions, the components being rendered will have a combination of properties from the story, meta, and global configuration. Therefore, if you need to access the args or parameters, you can do so as follows: +### The args are not being passed to the test - +When using the `composeStories` or `composeStory` functions, the components being rendered will have a combination of properties from the story, meta, and global configuration. Therefore, if you need to access the args or parameters, you can do so as follows: + + #### Learn about other UI tests - [Test runner](./test-runner.md) to automate test execution diff --git a/scripts/package.json b/scripts/package.json index 85c1d15a3693..784302828efd 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -92,6 +92,7 @@ "@types/express": "^4.17.11", "@types/fs-extra": "^11.0.1", "@types/http-server": "^0.12.1", + "@types/jest": "^29.5.5", "@types/lodash": "^4", "@types/node": "^18.0.0", "@types/node-fetch": "^2.5.7", diff --git a/scripts/prepare/bundle.ts b/scripts/prepare/bundle.ts index a15f71cd6a34..1228e445a034 100755 --- a/scripts/prepare/bundle.ts +++ b/scripts/prepare/bundle.ts @@ -16,6 +16,7 @@ type Formats = 'esm' | 'cjs'; type BundlerConfig = { entries: string[]; externals: string[]; + noExternal: string[]; platform: Options['platform']; pre: string; post: string; @@ -36,6 +37,7 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { bundler: { entries = [], externals: extraExternals = [], + noExternal: extraNoExternal = [], platform, pre, post, @@ -79,9 +81,12 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { */ const nonPresetEntries = allEntries.filter((f) => !path.parse(f).name.includes('preset')); + const noExternal = [/^@vitest\/.+$/, ...extraNoExternal]; + if (formats.includes('esm')) { tasks.push( build({ + noExternal, silent: true, treeshake: true, entry: nonPresetEntries, @@ -116,6 +121,7 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { if (formats.includes('cjs')) { tasks.push( build({ + noExternal, silent: true, entry: allEntries, watch, diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index e6b1bc188fd4..393169b244f8 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -354,7 +354,7 @@ async function linkPackageStories( ); } -async function addExtraDependencies({ +export async function addExtraDependencies({ cwd, dryRun, debug, @@ -378,7 +378,7 @@ async function addExtraDependencies({ export const addStories: Task['run'] = async ( { sandboxDir, template, key }, - { addon: extraAddons, dryRun, debug, disableDocs } + { addon: extraAddons, disableDocs } ) => { logger.log('💃 adding stories'); const cwd = sandboxDir; @@ -516,9 +516,6 @@ export const addStories: Task['run'] = async ( } } - // Some addon stories require extra dependencies - await addExtraDependencies({ cwd, dryRun, debug }); - await writeConfig(mainConfig); }; diff --git a/scripts/tasks/sandbox.ts b/scripts/tasks/sandbox.ts index 1f63527ca056..58e7d92fe421 100644 --- a/scripts/tasks/sandbox.ts +++ b/scripts/tasks/sandbox.ts @@ -55,7 +55,9 @@ export const sandbox: Task = { await remove(details.sandboxDir); } - const { create, install, addStories, extendMain, init } = await import('./sandbox-parts'); + const { create, install, addStories, extendMain, init, addExtraDependencies } = await import( + './sandbox-parts' + ); let startTime = now(); await create(details, options); @@ -90,6 +92,12 @@ export const sandbox: Task = { await addStories(details, options); } + await addExtraDependencies({ + cwd: details.sandboxDir, + debug: options.debug, + dryRun: options.dryRun, + }); + await extendMain(details, options); logger.info(`✅ Storybook sandbox created at ${details.sandboxDir}`); diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 4dc6ec7e7c0f..9ab659ee9f9d 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -2969,6 +2969,7 @@ __metadata: "@types/express": "npm:^4.17.11" "@types/fs-extra": "npm:^11.0.1" "@types/http-server": "npm:^0.12.1" + "@types/jest": "npm:^29.5.5" "@types/lodash": "npm:^4" "@types/node": "npm:^18.0.0" "@types/node-fetch": "npm:^2.5.7" @@ -3614,6 +3615,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.5": + version: 29.5.5 + resolution: "@types/jest@npm:29.5.5" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 0a3481f119099e6a0a381fec0d410cd33241267a0981576a7a832687fc3f888f79285289dc7c054c3589fd443f7ed1598d25fa7bc9708491b58da17e423b4aff + languageName: node + linkType: hard + "@types/jsdom@npm:^20.0.0": version: 20.0.1 resolution: "@types/jsdom@npm:20.0.1"