构建屏幕
我们专注于自下而上构建 UI,从小处着手并增加复杂性。这样做使我们能够隔离地开发每个组件,找出其数据需求,并在 Storybook 中进行操作。所有这些都不需要启动服务器或构建屏幕!
在本章中,我们将继续提高复杂性,方法是将组件组合在一个屏幕中并在 Storybook 中开发该屏幕。
嵌套容器组件
由于我们的应用程序很简单,因此我们将构建的屏幕非常简单,只是将 TaskList
组件(通过 Pinia 提供自己的数据)包装在一些布局中,并从 store 中拉出一个顶级的 error
字段(假设如果我们在连接到服务器时遇到问题,我们将设置该字段)。让我们在您的 src/components/
文件夹中创建一个展示型组件 PureInboxScreen.vue
<template>
<div>
<div v-if="error" class="page lists-show">
<div class="wrapper-message">
<span class="icon-face-sad" />
<p class="title-message">Oh no!</p>
<p class="subtitle-message">Something went wrong</p>
</div>
</div>
<div v-else class="page lists-show">
<nav>
<h1 class="title-page">Taskbox</h1>
</nav>
<TaskList />
</div>
</div>
</template>
<script>
import TaskList from './TaskList.vue';
export default {
name: 'PureInboxScreen',
components: { TaskList },
props: {
error: { type: Boolean, default: false },
},
};
</script>
然后,我们可以创建一个容器,它再次在 src/components/InboxScreen.vue
中获取 PureInboxScreen
的数据
<template>
<PureInboxScreen :error="isError" />
</template>
<script>
import PureInboxScreen from './PureInboxScreen.vue';
import { computed } from 'vue';
import { useTaskStore } from '../store';
export default {
name: 'InboxScreen',
components: { PureInboxScreen },
setup() {
//👇 Creates a store instance
const store = useTaskStore();
//👇 Retrieves the error from the store's state
const isError = computed(() => store.status==='error');
return {
isError,
};
},
};
</script>
接下来,我们需要更新应用程序的入口点 (src/main.js
),以便我们可以相当快地将 store 连接到我们的组件层级结构中
import { createApp } from 'vue';
+ import { createPinia } from 'pinia';
- import './assets/main.css';
import App from './App.vue';
- createApp(App).mount('#app')
+ createApp(App).use(createPinia()).mount('#app');
我们还需要更改 App
组件以渲染 InboxScreen
(最终,我们将使用路由器来选择正确的屏幕,但这里我们先不用担心)
<script setup>
import InboxScreen from './components/InboxScreen.vue';
</script>
<template>
<div id="app">
<InboxScreen />
</div>
</template>
<style>
@import './index.css';
</style>
然而,有趣的地方在于在 Storybook 中渲染 story。
正如我们之前看到的,TaskList
组件是一个容器,它渲染了 PureTaskList
展示型组件。根据定义,容器组件不能简单地孤立地渲染;它们期望传递一些上下文或连接到服务。这意味着要在 Storybook 中渲染容器,我们必须模拟(即,提供一个虚假版本)它所需的上下文或服务。
当将 TaskList
放入 Storybook 时,我们通过简单地渲染 PureTaskList
并避免容器来避开了这个问题。我们也将做类似的事情,并在 Storybook 中渲染 PureInboxScreen
。
但是,我们在 PureInboxScreen
上遇到了问题,因为虽然 PureInboxScreen
本身是展示型的,但它的子组件 TaskList
却不是。从某种意义上说,PureInboxScreen
已被“容器性”污染。因此,当我们在 src/components/PureInboxScreen.stories.js
中设置我们的 story 时
import PureInboxScreen from './PureInboxScreen.vue';
export default {
component: PureInboxScreen,
title: 'PureInboxScreen',
tags: ['autodocs'],
};
export const Default = {};
export const Error = {
args: { error: true },
}
我们看到,虽然 error
story 工作正常,但我们在 default
story 中遇到了问题,因为 TaskList
没有 Pinia store 可以连接。
解决此问题的一种方法是永远不要在应用程序中的任何地方渲染容器组件,除非在最高级别,而是将所有数据需求向下传递到组件层级结构中。
然而,开发人员**将**不可避免地需要渲染组件层级结构中更深层的容器。如果我们想在 Storybook 中渲染大部分或全部应用程序(我们确实想!),我们需要一个解决此问题的方法。
为 story 提供上下文
好消息是,将 Storybook 连接到 Pinia store 并在 story 之间重用它很容易!我们可以更新我们的 .storybook/preview.js
配置文件,并依靠 Storybook 的 setup
函数来注册我们现有的 Pinia store
+ import { setup } from '@storybook/vue3';
+ import { createPinia } from 'pinia';
import '../src/index.css';
//👇 Registers a global Pinia instance inside Storybook to be consumed by existing stories
+ setup((app) => {
+ app.use(createPinia());
+ });
/** @type { import('@storybook/vue3').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
存在类似的方法来为其他数据库(例如 Apollo、Relay 等)提供模拟上下文。
在 Storybook 中循环浏览状态可以轻松测试我们是否正确地完成了此操作
组件测试
到目前为止,我们已经能够从头开始构建一个功能齐全的应用程序,从一个简单的组件开始,一直到一个屏幕,并使用我们的 story 持续测试每个更改。但是,每个新的 story 也需要手动检查所有其他 story,以确保 UI 不会崩溃。这需要大量额外的工作。
我们能否自动化此工作流程并自动测试我们的组件交互?
使用 play 函数编写组件测试
Storybook 的 play
和 @storybook/addon-interactions
可以帮助我们实现这一点。play 函数包含在 story 渲染后运行的小段代码。
play 函数帮助我们验证更新任务时 UI 会发生什么。它使用与框架无关的 DOM API,这意味着我们可以使用 play 函数编写 story 来与 UI 交互并模拟人类行为,无论前端框架如何。
@storybook/addon-interactions
帮助我们在 Storybook 中可视化我们的测试,提供逐步流程。它还提供了一组方便的 UI 控件,用于暂停、恢复、倒带和逐步浏览每个交互。
让我们看看它的实际效果!更新您新创建的 PureInboxScreen
story,并通过添加以下内容来设置组件交互
import PureInboxScreen from './PureInboxScreen.vue';
+ import { fireEvent, within } from '@storybook/test';
export default {
component: PureInboxScreen,
title: 'PureInboxScreen',
tags: ['autodocs'],
};
export const Default = {};
export const Error = {
args: { error: true },
};
+ export const WithInteractions = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ // Simulates pinning the first task
+ await fireEvent.click(canvas.getByLabelText('pinTask-1'));
+ // Simulates pinning the third task
+ await fireEvent.click(canvas.getByLabelText('pinTask-3'));
+ },
+ };
💡 @storybook/test
包取代了 @storybook/jest
和 @storybook/testing-library
测试包,提供了更小的捆绑包大小和基于 Vitest 包的更直接的 API。
检查您新创建的 story。单击“Interactions”面板以查看 story 的 play 函数中的交互列表。
使用测试运行器自动化测试
借助 Storybook 的 play 函数,我们能够避开我们的问题,使我们能够与 UI 交互并快速检查如果我们更新任务,UI 如何响应——在无需额外手动操作的情况下保持 UI 的一致性。
但是,如果我们仔细查看我们的 Storybook,我们可以看到它仅在查看 story 时运行交互测试。因此,如果我们进行更改,我们仍然需要浏览每个 story 以运行所有检查。我们能否自动化它?
好消息是我们能做到!Storybook 的 测试运行器 允许我们做到这一点。它是一个独立的实用程序——由 Playwright 提供支持——它运行我们所有的交互测试并捕获损坏的 story。
让我们看看它是如何工作的!运行以下命令来安装它
yarn add --dev @storybook/test-runner
接下来,更新您的 package.json
scripts
并添加一个新的测试任务
{
"scripts": {
"test-storybook": "test-storybook"
}
}
最后,在您的 Storybook 运行时,打开一个新的终端窗口并运行以下命令
yarn test-storybook --watch
💡 使用 play 函数进行组件测试是测试 UI 组件的绝佳方法。它可以做的事情远不止我们在这里看到的;我们建议阅读 官方文档 以了解更多信息。
要更深入地了解测试,请查看 测试手册。它涵盖了规模化前端团队使用的测试策略,以增强您的开发工作流程。
成功!现在我们有了一个工具,可以帮助我们验证是否所有 story 都在没有错误的情况下渲染,并且所有断言都自动通过。更重要的是,如果测试失败,它将为我们提供一个链接,该链接会在浏览器中打开失败的 story。
组件驱动开发
我们从底部的 Task
开始,然后发展到 TaskList
,现在我们来到了一个完整的屏幕 UI。我们的 InboxScreen
容纳了一个嵌套的容器组件,并包含随附的 story。
**组件驱动开发** 允许您在组件层级结构中向上移动时逐步扩展复杂性。好处包括更集中的开发过程和增加对所有可能的 UI 排列的覆盖。简而言之,CDD 帮助您构建更高质量和更复杂的用户界面。
我们还没有完成——当 UI 构建完成时,工作并没有结束。我们还需要确保它随着时间的推移保持持久。