# 任务 3：组件级别的快速反馈

Hey，你好，我是 JimmyLv 吕靖，这是你开始 前端 TDD 练习的第 13 天，今天我们会接着练习 CommentBox 项目，学习如何使用 Storybook 来测试组件样式。

## 练习目标

在之前的练习中，我们还没有涉及到组件样式，但其实在前端工作中大部分时间是消耗在了调整 CSS 样式上面，可能你也有过如下“内心崩溃”的经历。

![](/files/-M1jB7M30kTPdW0J4KaV)

这一次，我们对页面及组件的样式有严格的要求，具体的评论框样式可参考 YouTube（需要梯子），或 Ant Design UI 组件库，需要尽可能做到 100% 像素级还原。

```
yarn stroybook #启动组件运行时
yarn test #运行单元测试
yarn test:story #运行组件快照测试
```

## 本次任务的练习要求

* 使用独立的 Storybook 组件运行时，调整样式
* 尽可能穷尽组件的所有使用场景，一种情况一个 story
* 完成所有情况后，使用组件快照测试自动化回归组件样式
* 别忘了重新运行 Cypress E2E 测试，看是否破坏原有功能

## Storybook 组件运行时

> Storybook: UI component explorer for frontend developers

通过 CDD（Components-Driven-Development）的方式在 React Storybook 里边儿写 `components.stories.js`，即人为验证，视觉驱动出来的组件代码，在样式上的反馈，人眼反而是最快的。

![Storybook Cart Example](/files/-M1jB7M5AO-uQARZps0l)

### 验证组件的 UI 样式

```javascript
#Counter.stories.js
export default { title: 'Counter' }
export const enabled = () => (
  <Counter text="Enabled" />
);
export const disabled = () => (
  <Counter disabled text="Disabled" />
);
```

### 在测试中使用 Storybook 用例

这是一个额外的福利，当我们在测试用例中 render 一个组件后，可以将它提取出来，放到单独的 `Component.stories.js` 文件中，从而省掉了重新定义 Storybook story case 的代码。

```javascript
#Button.test.js
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { enabled, disabled } from './Button.stories';
test('should increment when enabled', () => {
    const comp = render(enabled());
    userEvent.click(comp.getByText('Enabled: 0'));
    expect(comp.getByText('Enabled: 1')).toBeTruthy();
});
 test('should do nothing when disabled', () => {
    const comp = render(disabled());
    userEvent.click(comp.getByText('Disabled: 0'));
    expect(comp.getByText('Disabled: 0')).toBeTruthy();
 });
```

## Storyshot 快照测试

写完了组件样式，让我们通过视觉测试给 UI 组件加上一个安全保护伞。只需要给 UI Components 的 Stories 照个相就可以啦，结合 [Storyshots](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-puppeteer#imagesnapshots) 自动生成 Snapshot Testing，成本几乎为 0。

```javascript
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

const beforeScreenshot = (page, { context: { kind, story }, url }) => {
  page.setViewport({ width: 1440, height: 960 })
}

initStoryshots({
  suite: 'Image storyshots',
  test: imageSnapshot({
    storybookUrl: 'http://localhost:6006',
    beforeScreenshot,
  }),
})
```

结合 Storybook 引入 Storyshot，自动在 UI 完毕之后 take Snapshot，而且是图片级别的快照。

![](/files/-M1VmJxlqN9HEml_xvlc)

## TDD 与视觉测试保护伞

然后，在新的需求变更或是 CSS 重构之后，我们可以通过 Storyshot 自动打个快照，截图对比：

综上，个人认为 React Components 应当尽可能保持纯净，就安安心心做自己的 V(iew) 不好吗？lifecycle 什么的就不要 care 啦，现在使用 React Hook 便可非常直接得抽取逻辑，保持 functional component 纯净。而 pure UI 的组件，自然可以通过 Snapshot Testing 的方式轻松验证每次更改的合法与否。

```javascript
import React, { useState } from 'react'

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
```

与其改动测试代码与实现代码，不如在 CDD 直接改动实现代码并验证过后，重新给 UI 组件照个相然后替换原有的相片即可，这样就能用新的相片（baseline）去验证未来可能**不小心**改动所影响的 break change，也就达到了 Testing 最初的目的。

![](/files/-M1jB7M71Eq7wdcdklOC)

> 左边 vs 右边，中间是 diff 的不同内容！

至此，「驱动」和「验证」两大目的均已达到，并能在一定程度上减少「成本」与提高「速度」。

而完全抽离出来的 Redux 逻辑才是真正与 data 相关的地方，absolutely 值得一测并且应当尽可能提高测试覆盖率。再者，在合理抽象与充分隔离的基础上，pure FP 测试起来也是一件非常 easy 和享受的事情，这个留到后边儿的练习项目再说。

![](/files/-M1VmJxnZ8xU1lJ7nqM6)

对于前端来说，TDD 业务逻辑和交互行为是最值得去做的，也是最容易去实施 TDD 的，你要学会有的放矢，把力气使在刀刃上，减少浪费。保持组件尽可能 pure component，然后组件样式什么的，就不要去傻傻得验证几个 px 啦，就直接通过 CDD 在 Storybook 里面看就好了。而图片快照和全局的 visual testing 会给我最直观的反馈和最细致的对比，截图比什么都来得快、来得直观。

![](/files/-M1jB7M9JFZ2O-nnwg7D)

在这儿还可以稍微脑洞一下，最极端的情况，追求像素级还原：视觉测试，可以直接拿设计稿来对比嘛！

![](/files/-M1jB7MB2CM_bJF3vUk5)

## 也请你思考一下

* 独立的 UI 组件运行时带来了什么好处？
* 前端 TDD 的不同阶段，侧重点分别是什么？
* 整理你的窗口布局，让反馈更加一触即达（IDE、命令行、浏览器、设计稿…）

> 然后……作为前端开发者，或许你需要第二块显示器屏幕。

![multiple display](/files/-M1jB7MD5Cqi7PFKidEj)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jimmylv.gitbook.io/tdd-frontend/coding/00-project-commentbox/04-quick-feedback-of-components-from-stroybook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
