构建一个屏幕
我们专注于自下而上地构建 UI,从小处着手,逐步增加复杂性。这样做使我们能够独立开发每个组件,弄清楚其数据需求,并在 Storybook 中进行尝试。所有这一切都不需要搭建服务器或构建屏幕!
在本章中,我们将通过在屏幕中组合组件并在 Storybook 中开发该屏幕来继续提高复杂性。
嵌套容器组件
由于我们的应用很简单,我们将构建的屏幕也非常简单,只需将 TaskList
组件(通过 Svelte Store 提供自己的数据)包装在某个布局中,并从 Store 中提取顶级 error
字段(假设如果连接到服务器时出现问题,我们将设置该字段)。
让我们首先更新我们的 Svelte Store(位于 src/store.js
中),以包含我们想要的新 error
字段
// A simple Svelte store implementation with update methods and initial data.
// A true app would be more complex and separated into different files.
import { writable } from "svelte/store";
/*
* The initial state of our store when the app loads.
* Usually, you would fetch this from a server. Let's not worry about that now
*/
const defaultTasks = [
{ id: '1', title: 'Something', state: 'TASK_INBOX' },
{ id: '2', title: 'Something more', state: 'TASK_INBOX' },
{ id: '3', title: 'Something else', state: 'TASK_INBOX' },
{ id: '4', title: 'Something again', state: 'TASK_INBOX' },
];
const TaskBox = () => {
// Creates a new writable store populated with some initial data
const { subscribe, update } = writable({
tasks: defaultTasks,
status: 'idle',
error: false,
});
return {
subscribe,
// Method to archive a task, think of a action with redux or Pinia
archiveTask: (id) =>
update((store) => {
const filteredTasks = store.tasks
.map((task) =>
task.id === id ? { ...task, state: 'TASK_ARCHIVED' } : task
)
.filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
return { ...store, tasks: filteredTasks };
}),
// Method to archive a task, think of a action with redux or Pinia
pinTask: (id) => {
update((store) => {
const task = store.tasks.find((t) => t.id === id);
if (task) {
task.state = 'TASK_PINNED';
}
return store;
});
},
+ isError: () => update((store) => ({ ...store, error: true })),
};
};
export const taskStore = TaskBox();
Store 现在已经更新了新字段。接下来在你的 components
目录中创建 InboxScreen.svelte
<script>
import TaskList from "./TaskList.svelte";
export let error = false;
</script>
<div>
{#if error}
<div 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>
{:else}
<div class="page lists-show">
<nav>
<h1 class="title-page">Taskbox</h1>
</nav>
<TaskList />
</div>
{/if}
</div>
我们还需要更改 App
组件以渲染 InboxScreen
(最终,我们会使用路由器来选择正确的屏幕,但在此处暂不考虑)。
<script>
import InboxScreen from './components/InboxScreen.svelte';
import { taskStore } from './store';
</script>
<InboxScreen error={$taskStore.error} />
最后是 src/main.js
- import './app.css';
+ import './index.css';
import App from './App.svelte';
const app = new App({
target: document.getElementById("app"),
});
export default app;
然而,有趣的是在 Storybook 中渲染 story。
如前所述,TaskList
组件是一个容器,它渲染 PureTaskList
演示组件。根据定义,容器组件不能简单地独立渲染;它们期望传入一些上下文或连接到某个服务。这意味着要在 Storybook 中渲染一个容器,我们必须模拟(即,提供一个模拟版本)它所需的上下文或服务。
将 TaskList
放入 Storybook 时,我们通过仅渲染 PureTaskList
并避免使用容器来避开了这个问题。我们将采用类似的方法,也在 Storybook 中渲染 InboxScreen
。
所以当我们在 InboxScreen.stories.js
中设置我们的 stories 时
import InboxScreen from './InboxScreen.svelte';
export default {
component: InboxScreen,
title: 'InboxScreen',
tags: ['autodocs'],
};
export const Default = {};
export const Error = {
args: { error: true },
};
我们看到 Error
和 Default
stories 都正常工作。
在 Storybook 中切换不同的状态可以很容易地测试我们是否正确地完成了此操作
组件测试
到目前为止,我们已经能够从零开始构建一个功能齐全的应用,从简单的组件到屏幕,并使用我们的 stories 持续测试每次更改。但是每个新的 story 也都需要手动检查所有其他 stories,以确保 UI 不会出错。这会增加大量额外工作。
我们不能自动化这个工作流程并自动测试我们的组件交互吗?
使用 play 函数编写组件测试
Storybook 的 play
和 @storybook/addon-interactions
有助于我们实现这一点。play 函数包含在 story 渲染后运行的小段代码片段。
play 函数帮助我们验证任务更新时 UI 会发生什么变化。它使用与框架无关的 DOM API,这意味着我们可以使用 play 函数编写 stories 来与 UI 交互并模拟人类行为,无论使用何种前端框架。
@storybook/addon-interactions
帮助我们在 Storybook 中可视化测试,提供分步流程。它还提供了一系列方便的 UI 控件,用于暂停、恢复、回退和逐步执行每个交互。
让我们看看它的实际效果!更新你新创建的 InboxScreen
story,并通过添加以下内容来设置组件交互
import InboxScreen from './InboxScreen.svelte';
+ import { fireEvent, within } from '@storybook/test';
export default {
component: InboxScreen,
title: 'InboxScreen',
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 驱动——可以运行我们所有的交互测试并捕获损坏的 stories。
让我们看看它是如何工作的!运行以下命令来安装它
yarn add --dev @storybook/test-runner
接下来,更新你的 package.json
中的 scripts
,并添加一个新的测试任务
{
"scripts": {
"test-storybook": "test-storybook"
}
}
最后,在 Storybook 运行的情况下,打开一个新的终端窗口并运行以下命令
yarn test-storybook --watch
💡 使用 play 函数进行组件测试是测试 UI 组件的绝佳方式。它的功能远不止我们在此看到的内容;我们建议阅读官方文档了解更多信息。
要更深入地了解测试,请查阅测试手册。它涵盖了大型前端团队使用的测试策略,以增强你的开发工作流程。
成功!现在我们有了一个工具,可以帮助我们验证所有 stories 是否都能无错误地渲染,并且所有断言都能自动通过。更重要的是,如果测试失败,它会提供一个链接,可在浏览器中打开失败的 story。
组件驱动开发
我们从底层的 Task
开始,然后进展到 TaskList
,现在我们有了一个完整的屏幕 UI。我们的 InboxScreen
容纳了一个嵌套容器组件,并包含相应的 stories。
组件驱动开发 允许你在组件层级结构中逐步扩展复杂性。其好处包括更集中的开发过程,以及增加对所有可能 UI 排列组合的覆盖。简而言之,CDD 帮助你构建更高质量、更复杂的用户界面。
我们还没有完成——工作并非止于 UI 构建完成。我们还需要确保它能随着时间的推移保持稳定。