본문 바로가기

FrontEnd

[Storybook/ Vue3] Play 함수를 이용하여 컴포넌트 상호작용 자동화

반응형

Storybook 공식 문서를 번역한 글입니다.

https://storybook.js.org/docs/vue/writing-stories/play-function

 

Play function

Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It’s open source and free.

storybook.js.org

Play 함수는 스토리가 렌더링된 후 실행되는 작은 코드 스니펫입니다.
다음과 같은 용도로 사용할 수 있습니다.
  • 사용자 대신 컴포넌트와 상호작용
  • 테스트 시나리오를 실행

Interaction addon 설치

play 함수로로 스토리 작성을 시작하기 전, Storybook의 Interaction addon을 설치하는 것이 좋습니다.

  • 실행 흐름을 제어할 수 있는 편리한 UI 컨트롤 세트를 포함하여 이를 완벽하게 보완합니다.
  • 언제든지 각 상호 작용을 일시 중지, 다시 시작, 되감기 및 단계별 실행할 수 있습니다.
  • 잠재적인 문제를 파악하기 쉽도록 해주는 디버거를 제공합니다.

다음 명령을 실행하여 애드온과 필요한 종속성을 설치합니다.

yarn add --dev @storybook/testing-library @storybook/jest @storybook/addon-interactions
Interaction 애드온을 포함하도록 Storybook 설정(@ .storybook/main.js)을 업데이트합니다.
// .storybook/main.js

module.exports = {
  stories:[],
  addons:[
    // Other Storybook addons
    '@storybook/addon-interactions', //👈 The addon registered here
};

Play 함수와 같이 스토리 작성하기

Storybook의 play 함수는 스토리 렌더링이 완료되면 실행되는 작은 코드 스니펫입니다.
interaction 애드온 의 도움을 받아 사용자 개입 없이는 불가능했던 컴포넌트 상호 작용 및 테스트 시나리오를 구축할 수 있습니다.
예를 들어 회원 가입 양식(registration form)을 작성 중이고 유효성을 검사하려는 경우
play 함수를 사용하여 다음 스토리를 작성할 수 있습니다.

RegistrationForm.stories.mdx

<!-- RegistrationForm.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { userEvent, within } from '@storybook/testing-library';

import RegistrationForm from './RegistrationForm.vue';

<Meta title="RegistrationForm" component={RegistrationForm} />

export const Template = (args) => ({
  components: { RegistrationForm },
  template: '<RegistrationForm />',
});

<!--  
 See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
 to learn more about using the canvasElement to query the DOM
 -->
<Story
  name="FilledForm"
  play={ async ({ canvasElement }) => {
    const canvas= within(canvasElement);

    const emailInput = canvas.getByLabelText('email', {
      selector: 'input',
    });

    await userEvent.type(emailInput, 'example-email@email.com', {
      delay: 100,
    });
    
    const passwordInput = canvas.getByLabelText('password', {
      selector: 'input',
    });

    await userEvent.type(passwordInput, 'ExamplePassword', {
      delay: 100,
    });
    
    // See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    const Submit = canvas.getByRole('button');
    await userEvent.click(Submit);
  }}>
  {Template.bind({})}
</Story>
💡 사용 가능한 API 이벤트에 대한 개요는 상호 작용 테스트 문서(Interaction testing documentation)를 참조하세요.

스토리 합성하기

ES6 모듈 기반 파일 형식인 Component Story 형식 덕분에
다른 기존 Storybook 기능(예: args)과 유사하게 play 함수를 합성할 수 있습니다.
예를 들어 컴포넌트에 대한 특정 워크플로를 확인하려는 경우 다음 스토리를 작성할 수 있습니다.
// MyComponent.stories.ts

// import { Meta, StoryFn } from '@storybook/vue3'; for Vue 3
import { Meta, StoryFn } from '@storybook/vue';

import { userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

export default {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'MyComponent',
  component: MyComponent,
} as Meta<typeof MyComponent>;


const Template: StoryFn<typeof MyComponent> = (args) => ({
  components: { MyComponent },
  template: '<MyComponent />',
});

/*
* See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FirstStory = Template.bind({});
FirstStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  await userEvent.type(canvas.getByTestId('an-element'), 'example-value');
};

export const SecondStory = Template.bind({});
SecondStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  await userEvent.type(canvas.getByTestId('other-element'), 'another value');
};

아래와 같이 다른 스토리에서 기존 스토리의 play 함수를 실행할 수 있습니다.

export const CombinedStories = Template.bind({});
CombinedStories.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  // Runs the FirstStory and Second story play function before running this story's play function
  await FirstStory.play({ canvasElement });
  await SecondStory.play({ canvasElement });
  await userEvent.type(canvas.getByTestId('another-element'), 'random value');
};

스토리를 결합하면 기존 컴포넌트 워크플로를 다시 활용할 수 있으며,
상용구 코드 제거를 통해 잠재적인 문제를 더 빠르게 식별할 수 있습니다.


이벤트 다루기

대부분의 최신 UI는 상호 작용(예: 버튼 클릭, 옵션 선택, 확인란 선택)에 초점을 맞춰 구축되어 최종 사용자에게 풍부한 경험을 제공합니다. play 함수를 사용하면 동일한 수준의 상호 작용을 스토리에 통합할 수 있습니다.

컴포넌트 상호 작용의 일반적인 유형은 버튼 클릭입니다.
스토리에서 재현해야 하는 경우 스토리의 play 함수를 다음과 같이 정의할 수 있습니다.

(    await userEvent.click(canvas.getByRole('button'));)

MyComponent.stories.mdx

<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { fireEvent, userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="ClickExamples" component={MyComponent} />

export const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent/>',
});


<Story
  name="ClickExample"
  play={ async ({ canvasElement}) => {
    const canvas = within(canvasElement);

    // See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button'));
  }}>
  {Template.bind({})}
</Story>

<Story
  name="FireEventExample"
   play={async ({ canvasElement}) => {
    const canvas = within(canvasElement);

    // See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await fireEvent.click(canvas.getByTestId('data-testid'));
  }}>
  {Template.bind({})}
</Story>

Storybook이 스토리를 로드하고 play 함수가 실행되면
사용자가 수행하는 것과 유사하게 컴포넌트와 상호 작용하고 버튼 클릭을 트리거합니다.

 

Play 함수를 사용하여 클릭 외에도 다양한 이벤트를 스크립팅할 수 있습니다.
예를 들어 컴포넌트에 다양한 옵션이 있는 select 엘리먼트가 포함된 경우 다음 스토리를 작성하고 각 시나리오를 테스트할 수 있습니다.

MyComponent.stories.ts

// MyComponent.stories.ts

// import { Meta, StoryFn } from '@storybook/vue3'; for Vue 3
import { Meta, StoryFn } from '@storybook/vue';

import { userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

export default {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'WithSelectEvent',
  component: WithSelectEvent,
} as Meta<typeof MyComponent>;

// Custom function to emulate a pause
function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}


const Template: StoryFn<typeof MyComponent> = (args) => ({
 components: { MyComponent },
  template: '<MyComponent />',
});

/*
* See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleChangeEvent = Template.bind({});
ExampleChangeEvent.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  const select = canvas.getByRole('listbox');

  await userEvent.selectOptions(select, ['One Item']);
  await sleep(2000);

  await userEvent.selectOptions(select, ['Another Item']);
  await sleep(2000);

  await userEvent.selectOptions(select, ['Yet another item']);
};
이벤트 외에도 다른 타입의 비동기식 메서드를 기반으로 play 함수와의 상호 작용을 만들 수도 있습니다.
예를 들어 유효성 검사 논리(예: 이메일 유효성 검사, 암호 강도)가 구현된 컴포넌트를 작업한다고 가정해 보겠습니다.
이 경우 사용자 상호 작용을 에뮬레이션하고 제공된 값이 유효한지 확인하기 위해 play 함수 내에 딜레이를 도입할 수 있습니다.

MyComponent.stories.mdx

<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="WithDelay" component={MyComponent} />

const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent />',
});

<!--  
 See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
 to learn more about using the canvasElement to query the DOM
 -->
<Story
  name="DelayedStory"
   play={ async ({ canvasElement, args}) => {
    const canvas = within(canvasElement);

    const exampleElement= canvas.getByLabelText('example-element');

    // The delay option set the amount of milliseconds between characters being typed
    await userEvent.type(exampleElement, 'random string', {
      delay: 100,
    });

    const AnotherExampleElement= canvas.getByLabelText('another-example-element');
    await userEvent.type(AnotherExampleElement, 'another random string', {
      delay: 100,
    });
  }}>
  {Template.bind({})}
</Story>
Storybook은 스토리를 로드할 때 컴포넌트와 상호 작용하여 입력을 채우고 정의된 유효성 검사 논리를 트리거합니다.

또한 play 함수를 사용하여 특정 상호 작용을 기반으로 엘리먼트의 존재를 확인할 수 있습니다.
사용자가 잘못된 정보를 입력하면 어떻게 되는지 확인하려는 경우입니다.
이 경우 다음과 같은 스토리를 작성할 수 있습니다.
 

MyComponent.stories.mdx

화면에 에러 엘리먼트가 나타나는지 확인
<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { userEvent, waitFor, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="WithAsync" component={MyComponent} />

export const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent />',
});

<!--  
 See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
 to learn more about using the canvasElement to query the DOM
 -->
<Story
  name="ExampleAsyncStory"
   play={ async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const Input = canvas.getByLabelText('Username', {
      selector: 'input',
    });

    await userEvent.type(Input, 'WrongInput', {
      delay: 100,
    });

    // See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    const Submit = canvas.getByRole('button');
    await userEvent.click(Submit);

    await waitFor(async () => {
      await userEvent.hover(canvas.getByTestId('error'));
    });
  }}>
  {Template.bind({})}
</Story>​

엘리먼트 쿼리하기

필요한 경우 play 함수를 조정하여 쿼리(예: role, 텍스트 콘텐츠)를 기반으로 엘리먼트를 찾을 수도 있습니다.

MyComponent.stories.mdx

<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="QueryMethods" component={MyComponent} />

export const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent />',
});

<!--  
 See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
 to learn more about using the canvasElement to query the DOM
 -->
<Story
  name="ExampleWithRole"
   play={ async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button', { name: / button label/i }));
  }}>
  {Template.bind({})}
</Story>
💡 테스팅 라이브러리 문서(Testing library documentation)에서 엘리먼트를 쿼리하는 다양한 방법을 배울 수 있습니다.

Storybook이 스토리를 로드하면 play 함수가 실행되며,
스토리가 렌더링될 때 사용자가 해당 엘리먼트를 사용할 수 있을 것으로 기대하는 DOM 트리를 쿼리합니다.
테스트에 실패하는 경우 근본 원인을 신속하게 확인할 수 있습니다.

 

만약 play 함수 내에서 정의된 이전 단계 또는 일부 비동기 동작으로 인해 컴포넌트를 즉시 사용할 수 없는 경우
엘리먼트를 쿼리하기 전에 DOM 트리가 변경될 때까지 기다릴 수 있습니다.

MyComponent.stories.mdx

<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="Async Query Methods" component={MyComponent} />

export const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent />',
});

<!--  
 See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
 to learn more about using the canvasElement to query the DOM
 -->
<Story
  name="AsyncExample"
  play={ async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Other steps

    // Waits for the component to be rendered before querying the element
    await canvas.findByRole('button', { name: / button label/i }));
  }}>
  {Template.bind({})}
</Story>​

캔버스로 작업하기

기본적으로 Play 함수 내에서 작성하는 각 인터랙션은 Canvas의 최상위 엘리먼트에서 시작하여 실행됩니다.
이는 작은 컴포넌트(예: 버튼, 확인란, 텍스트 입력)에는 괜찮지만
복잡한 컴포넌트(예: 양식, 페이지)와 여러 스토리를 가진 컴포넌트에는 비효율적일 수 있씁니다.
컴포넌트의 루트에서 실행을 시작하도록 상호 작용을 조정할 수 있습니다.

MyComponent.stories.mdx

<!-- MyComponent.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs';

import { getByRole, userEvent, within } from '@storybook/testing-library';

import MyComponent from './MyComponent.vue';

<Meta title="WithCanvasElement" component={MyComponent} />

export const Template = (args) => ({
  components: { MyComponent },
  template: '<MyComponent/>',
});

<Story
  name="ExampleStory"
  play={async ({ canvasElement }) => {
    // 컴포넌트 루트 엘리먼트에 캔버스를 할당합니다.
    const canvas = within(canvasElement);

    // 컴포넌트 루트 요소부터 쿼리를 시작합니다.
    await userEvent.type(canvas.getByTestId('example-element'), 'something');
    await userEvent.click(canvas.getByRole('another-element'));
  }}>
  {Template.bind({})}
</Story>
이러한 변경 사항을 스토리에 적용하면 상호작용 애드온의 성능이 향상되며, 오류 처리를 쉽게 할 수 있습니다.

 

반응형