storybookのplay関数でよく使うAPIなど

userEvent

sample.stories.ts

import { userEvent, within } from '@storybook/test'

export const TestStory: Story = {
  tags: ['!autodocs'], // ドキュメントに含めない
  play: async ({ args, canvasElement }) => {
    const canvas = within(canvasElement)
    const inputEl: HTMLInputElement = canvas.getByTestId('sample-input') // data-testidがsample-inputの要素
    await userEvent.click(inputEl) // click
    
    // key入力
    inputEl.focus()
    await userEvent.keyboard('{ArrowUp}')  // 上矢印ボタン
    await userEvent.keyboard('{ArrowDown}') // 下矢印ボタン
    await userEvent.keyboard('{Enter}') // Enterキー
    await userEvent.keyboard('{Escape}') // Escapeキー
    await userEvent.keyboard('{ }') // Spaceキー

    // タイピング入力
    await userEvent.type(inputEl, 'おはようございます¥nこんにちは') // タイピング
  await userEvent.clear(inputEl) // 入力をクリア
  }
}

step, expect, fn

sample.stories.ts

import { userEvent, within, expect, fn } from '@storybook/test'

export const TestStory: Story = {
  argTypes: {
    'onUpdate:modelValue': {
      table: { category: 'events', type: { summary: '[v: string]' }, disable: true }, // 自動で表示されるupdateイベントと重複のためdisableとしている
    },
  },
  args: {
    modelValue: '',
    'onUpdate:modelValue': fn(),
  },
  play: async ({ args, canvasElement, step }) => {
    const canvas = within(canvasElement)
    const inputEl: HTMLInputElement = canvas.getByTestId('sample-input') // data-testidがsample-inputの要素

    await step('更新イベントが正しく発火するか', async () => {
      await userEvent.type(inputEl, '東京都')
      await expect(args['onUpdate:modelValue']).lastCalledWith('東京都')
    })
  }
}

waitFor

.storybook/preview.ts

import { configure } from '@storybook/test'
configure({
  asyncUtilTimeout: 6000 // グローバルにwaitForの最大待機時間を6000msに設定
})

sample.stories.ts

import { userEvent, within, expect, waitFor } from '@storybook/test'

export const TestStory: Story = {
  play: async ({ args, canvasElement }) => {
    const canvas = within(canvasElement)
    const inputEl: HTMLInputElement = canvas.getByTestId('sample-input') // data-testidがsample-inputの要素
    
    // waitForは反映に時間のかかる処理に使用する
  // 関数内のexpectが全て成功するか、configureのasyncUtilTimeoutが経過するまで、50msごとにexpectを繰り返す
    // asyncUtilTimeoutが経過しても成功しない場合は失敗となる。
    await waitFor(async () => {
      await userEvent.type(inputEl, 'おはよう')
      await expect(inputEl.value).toBe('こんばんは') // どんなに待ってもvalueが'こんばんは'になることはないのでエラーとなる
      await expect(inputEl).toBeVisible() // display: none;の場合はfalse
    }
  }
}

play関数を再利用する

公式ドキュメント:https://storybook.js.org/docs/writing-stories/play-function#querying-elements

  • 再利用するstoryの型にはRequired<Pick<Story, ‘play’>>を追加する必要がある
  • 再利用する時に呼び出すstoryに渡す際にはcontextを全て渡す

sample.stories.ts

// 再利用するstoryの型にはRequired<Pick<Story, 'play'>>を追加する必要がある
export const StoryA: Story & Required<Pick<Story, 'play'>> = {
  play: async ({ canvasElement }) => {
     const canvas = within(canvasElement)
     ....略
  }
}

export const StoryB: Story = {
  play: async (context) => { // 公式ドキュメントではasync ({context})となっているがエラーとなる
    const canvas = within(context.canvasElement)
    await StoryA.play(context) // StoryAが実行される
    ....略 // StoryAを実行した後の処理を記述する
  }
}

mockの値を上書きする

jest公式ドキュメント:https://jestjs.io/ja/docs/mock-function-api

beforeEach内で上書きして使用することができる。

テストするコンポーネント内でapiを呼んでいる場合、そのmockApiの返却値を変更してテストする時などに使用する

以下サンプルでは、storiesでmockを定義しているが、実際は別のファイルで定義したmockApiをimportして上書きする使い方をすることが多い

sample.stories.ts

import { fn } from '@storybook/test'

const mock = fn((): string => 'あいうえお').mockName('mockData')
const asyncMock = fn(async (): Promise<string> => 'かきくけこ').mockName('asyncMock')

export const TestStory: Story = {
  async beforeEach() {
    mock.mockReturnValue('アイウエオ')
    asyncMock.mockResolvedValue('カキクケコ')
  },
  play: async () => {
    mock() // 'あいうえお'ではなく、beforeEachで上書きした'アイウエオ'が返却される
  await asyncMock() // 'かきくけこ'ではなく、beforeEachで上書きした'カキクケコ'が返却される
  }
}