Storybook 공식 문서를 번역한 글입니다.
https://storybook.js.org/docs/vue/writing-stories/play-function
- 사용자 대신 컴포넌트와 상호작용
- 테스트 시나리오를 실행
Interaction addon 설치
play 함수로로 스토리 작성을 시작하기 전, Storybook의 Interaction addon을 설치하는 것이 좋습니다.
- 실행 흐름을 제어할 수 있는 편리한 UI 컨트롤 세트를 포함하여 이를 완벽하게 보완합니다.
- 언제든지 각 상호 작용을 일시 중지, 다시 시작, 되감기 및 단계별 실행할 수 있습니다.
- 잠재적인 문제를 파악하기 쉽도록 해주는 디버거를 제공합니다.
다음 명령을 실행하여 애드온과 필요한 종속성을 설치합니다.
yarn add --dev @storybook/testing-library @storybook/jest @storybook/addon-interactions
// .storybook/main.js
module.exports = {
stories:[],
addons:[
// Other Storybook addons
'@storybook/addon-interactions', //👈 The addon registered here
};
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>
스토리 합성하기
// 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');
};
스토리를 결합하면 기존 컴포넌트 워크플로를 다시 활용할 수 있으며,
상용구 코드 제거를 통해 잠재적인 문제를 더 빠르게 식별할 수 있습니다.
이벤트 다루기
컴포넌트 상호 작용의 일반적인 유형은 버튼 클릭입니다.
스토리에서 재현해야 하는 경우 스토리의 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']);
};
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>
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>
엘리먼트 쿼리하기
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>
Storybook이 스토리를 로드하면 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>
캔버스로 작업하기
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>
'FrontEnd' 카테고리의 다른 글
Vue3 컴포넌트 디자인 패턴 소개 (0) | 2022.12.21 |
---|---|
[Vue3] Vue3은 리렌더링을 어떻게 트리거할까? (0) | 2022.12.21 |
[번역] Vite 플러그인 만들기 (0) | 2022.12.19 |
[Typescript] 객체 함수와 타입스크립트 (0) | 2022.12.18 |
[Vue3] Composition API와 Composable이란 무엇인가? (0) | 2022.12.18 |