文件
Storybook 文件

Vitest 中的可攜式 stories

可攜式 stories 是 Storybook stories,可在外部環境中使用,例如 Vitest

通常,Storybook 會自動組成 story 及其註釋,作為story 管道的一部分。當在 Vitest 測試中使用 stories 時,您必須自行處理 story 管道,而這正是 composeStoriescomposeStory 函式所實現的功能。

此處指定的 API 可在 Storybook 8.2.7 和更新版本中使用。如果您使用的是較舊版本的 Storybook,您可以升級至最新版本 (npx storybook@latest upgrade) 以使用此 API。如果您無法升級,則可以使用先前的 API,該 API 使用 .play() 方法而不是 .run(),但在其他方面是相同的。

使用 Next.js您可以透過安裝和設定 @storybook/experimental-nextjs-vite 來使用 Vitest 測試 Next.js stories,該套件會重新匯出 vite-plugin-storybook-nextjs 套件。

composeStories

composeStories 會處理您指定的元件 stories,將每個 story 與必要的註釋組成,並傳回包含組成 stories 的物件。

根據預設,組成的 story 將使用 story 中定義的args 來渲染元件。您也可以在測試中將任何 props 傳遞給元件,這些 props 將覆寫在 story 的 args 中傳遞的值。

Button.test.tsx
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
// 👉 Using Next.js? Import from @storybook/nextjs instead
import { composeStories } from '@storybook/react';
 
// Import all stories and the component annotations from the stories file
import * as stories from './Button.stories';
 
// Every component that is returned maps 1:1 with the stories,
// but they already contain all annotations from story, meta, and project levels
const { Primary, Secondary } = composeStories(stories);
 
test('renders primary button with default args', async () => {
  await Primary.run();
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', async () => {
  // You can override props by passing them in the context argument of the play function
  await Primary.run({ args: { ...Primary.args, children: 'Hello world' } });
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

類型

(
  csfExports: CSF file exports,
  projectAnnotations?: ProjectAnnotations
) => Record<string, ComposedStoryFn>

參數

csfExports

必要

類型:CSF 檔案匯出

指定您想要組成的元件 stories。從 CSF 檔案傳遞整組匯出(而不是預設匯出!)。例如:import * as stories from './Button.stories'

projectAnnotations

類型:ProjectAnnotation | ProjectAnnotation[]

指定要套用至組成的 stories 的專案註釋。

為了方便起見,提供了此參數。您應該改用 setProjectAnnotations。有關 ProjectAnnotation 類型的詳細資訊,請參閱該函式的 projectAnnotations 參數。

此參數可用於覆寫透過 setProjectAnnotations 套用的專案註釋。

傳回

類型:Record<string, ComposedStoryFn>

物件,其中鍵是 stories 的名稱,而值是組成的 stories。

此外,組合後的故事將具有以下屬性

屬性類型描述
argsRecord<string, any>故事的 args
argTypesArgType故事的 argTypes
idstring故事的 id
parametersRecord<string, any>故事的 parameters
play(context) => Promise<void> | undefined執行給定故事的 play 函式
run(context) => Promise<void> | undefined掛載並執行 給定故事的 play 函式
storyNamestring故事的名稱
tagsstring[]故事的 tags

composeStory

如果您希望為元件組合單一故事,可以使用 composeStory

Button.test.tsx
import { vi, test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { composeStory } from '@storybook/react';
 
import meta, { Primary as PrimaryStory } from './Button.stories';
 
// Returns a story which already contains all annotations from story, meta and global levels
const Primary = composeStory(PrimaryStory, meta);
 
test('renders primary button with default args', async () => {
  await Primary.run();
 
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', async () => {
  await Primary.run({ args: { ...Primary.args, label: 'Hello world' } });
 
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

類型

(
  story: Story export,
  componentAnnotations: Meta,
  projectAnnotations?: ProjectAnnotations,
  exportsName?: string
) => ComposedStoryFn

參數

story

必要

類型:Story export

指定您要組合的故事。

componentAnnotations

必要

類型:Meta

包含 story 的故事檔案中的預設匯出。

projectAnnotations

類型:ProjectAnnotation | ProjectAnnotation[]

指定要套用至組合後故事的專案註解。

為了方便起見,提供了此參數。您應該改用 setProjectAnnotations。有關 ProjectAnnotation 類型的詳細資訊,請參閱該函式的 projectAnnotations 參數。

此參數可用於覆寫透過 setProjectAnnotations 套用的專案註釋。

exportsName

類型:string

您可能不需要這個。因為 composeStory 接受單一故事,它無法存取該故事在檔案中的匯出名稱(例如 composeStories)。如果您必須確保測試中故事名稱的唯一性,而且您無法使用 composeStories,則可以在這裡傳遞故事匯出的名稱。

傳回

類型:ComposedStoryFn

單一的 組合故事

setProjectAnnotations

這個 API 應該在測試執行之前,通常在 設定檔案 中呼叫一次。這將確保在呼叫 composeStoriescomposeStory 時,也會將專案註解納入考量。

這些是在設定檔案中需要的組態

  • 預覽註解:在 .storybook/preview.ts 中定義的註解
  • 外掛註解(選用):由外掛程式匯出的註解
  • beforeAll:在所有測試之前執行的程式碼(更多資訊
setupTest.ts
import { beforeAll } from 'vitest';
// 👇 If you're using Next.js, import from @storybook/nextjs
//   If you're using Next.js with Vite, import from @storybook/experimental-nextjs-vite
import { setProjectAnnotations } from '@storybook/react';
// 👇 Import the exported annotations, if any, from the addons you're using; otherwise remove this
import * as addonAnnotations from 'my-addon/preview';
import * as previewAnnotations from './.storybook/preview';
 
const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]);
 
// Run Storybook's beforeAll hook
beforeAll(annotations.beforeAll);

有時,故事可能需要外掛程式的裝飾器載入器才能正確呈現。例如,外掛程式可以套用一個裝飾器,將您的故事包裝在必要的路由器上下文中。在這種情況下,您必須將該外掛程式的 preview 匯出包含在設定的專案註解中。請參閱上面範例中的 addonAnnotations

注意:如果外掛程式本身不會自動套用裝飾器或載入器,而是將它們匯出以供您在 .storybook/preview.js|ts 中手動套用(例如,使用來自 @storybook/addon-themeswithThemeFromJSXProvider),則您不需要執行其他任何操作。它們已包含在上述範例中的 previewAnnotations 中。

如果您需要設定 Testing Library 的 render 或使用不同的 render 函式,請在此討論中告訴我們,以便我們進一步了解您的需求。

類型

(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => ProjectAnnotation

參數

projectAnnotations

必要

類型:ProjectAnnotation | ProjectAnnotation[]

一組專案註解(在 .storybook/preview.js|ts 中定義的註解)或一組專案註解的陣列,將套用至所有組合後的故事。

註解

註解是套用至故事的中繼資料,例如 args裝飾器載入器play 函式。它們可以為特定故事、元件的所有故事或專案中的所有故事定義。

故事管線

為了在 Storybook 中預覽您的故事,Storybook 會執行一個故事管線,其中包括套用專案註解、載入資料、呈現故事和播放互動。這是管線的簡化版本

A flow diagram of the story pipeline. First, set project annotations. Collect annotations (decorators, args, etc) which are exported by addons and the preview file. Second, compose story. Create renderable elements based on the stories passed onto the API. Third, run. Mount the component and execute all the story lifecycle hooks, including the play function.

但是,當您想要在不同的環境中重複使用故事時,了解所有這些步驟如何構成故事至關重要。可攜式故事 API 為您提供在外部環境中重新建立該故事管線的機制

1. 套用專案層級註解

註解來自故事本身、該故事的元件和專案。專案層級註解是在您的 .storybook/preview.js 檔案中以及您正在使用的外掛程式中定義的註解。在可攜式故事中,這些註解不會自動套用 — 您必須自行套用它們。

👉 為此,您可以使用 setProjectAnnotations API。

2. 組合

這個故事是透過執行 composeStoriescomposeStory 來準備的。結果是一個可渲染的元件,代表故事的渲染函式。

3. 執行

最後,故事可以在渲染之前,透過定義 loadersbeforeEach 或在使用 mount 時將所有故事程式碼放在 play 函式中,來準備它們需要的資料(例如,設定一些 mock 或獲取資料)。在可攜式故事中,當您呼叫已組合故事的 run 方法時,將會執行所有這些步驟。

👉 為此,您可以使用 composeStoriescomposeStory API。已組合的故事將會回傳一個 run 方法供呼叫。

Button.test.tsx
import { test } from 'vitest';
import { composeStories } from '@storybook/react';
 
import * as stories from './Button.stories';
 
const { Primary } = composeStories(stories);
 
test('renders and executes the play function', async () => {
  // Mount story and run interactions
  await Primary.run();
});

如果您的 play 函式包含斷言(例如 expect 呼叫),當這些斷言失敗時,您的測試將會失敗。

覆寫全域設定

如果您的故事根據 全域設定 有不同的行為(例如,以英文或西班牙文渲染文字),您可以在組合故事時覆寫專案註解,以在可攜式故事中定義這些全域值

Button.test.tsx
import { test } from 'vitest';
import { render } from '@testing-library/react';
import { composeStory } from '@storybook/react';
 
import meta, { Primary as PrimaryStory } from './Button.stories';
 
test('renders in English', async () => {
  const Primary = composeStory(
    PrimaryStory,
    meta,
    { globals: { locale: 'en' } } // 👈 Project annotations to override the locale
  );
 
  await Primary.run();
});
 
test('renders in Spanish', async () => {
  const Primary = composeStory(PrimaryStory, meta, { globals: { locale: 'es' } });
 
  await Primary.run();
});