内容整理自官方开发文档

系列

  • 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本
  • 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Maps
  • Sentry For React 完整接入详解
  • Sentry For Vue 完整接入详解
  • Sentry-CLI 使用详解
  • Sentry Web 性能监控 - Web Vitals
  • Sentry Web 性能监控 - Metrics
  • Sentry Web 性能监控 - Trends
  • Sentry Web 前端监控 - 最佳实践(官方教程)
  • Sentry 后端监控 - 最佳实践(官方教程)
  • Sentry 监控 - Discover 大数据查询分析引擎
  • Sentry 监控 - Dashboards 数据可视化大屏
  • Sentry 监控 - Environments 区分不同部署环境的事件数据
  • Sentry 监控 - Security Policy 安全策略报告
  • Sentry 监控 - Search 搜索查询实战
  • Sentry 监控 - Alerts 告警
  • Sentry 监控 - Distributed Tracing 分布式跟踪
  • Sentry 监控 - 面向全栈开发人员的分布式跟踪 101 系列教程(一)
  • Sentry 监控 - Snuba 数据中台架构简介(Kafka+Clickhouse)
  • Sentry 监控 - Snuba 数据中台架构(Data Model 简介)
  • Sentry 监控 - Snuba 数据中台架构(Query Processing 简介)
  • Sentry 官方 JavaScript SDK 简介与调试指南
  • Sentry 监控 - Snuba 数据中台架构(编写和测试 Snuba 查询)
  • Sentry 监控 - Snuba 数据中台架构(SnQL 查询语言简介)
  • Sentry 监控 - Snuba 数据中台本地开发环境配置实战
  • Sentry 监控 - 私有 Docker Compose 部署与故障排除详解

目录

  • 前端手册
    • 目录结构
    • 文件夹和文件结构
      • 文件命名
      • 使用 index.(j|t)?(sx)
    • React
      • 定义 React 组件
      • 组件与视图
      • PropTypes
      • 事件处理程序
    • CSS 和 Emotion
      • stylelint 错误
    • 状态管理
    • 测试
      • 选择器
      • 测试中未定义的 theme 属性
    • Babel 语法插件
    • 新语法
      • 可选链
      • 语法
      • 空值合并
    • Lodash
    • Typescript
    • 迁移指南
  • Storybook Styleguide
    • 我们使用它吗?
    • 它部署在某个地方吗?
  • Typing DefaultProps
    • 类(Class)组件
    • 函数(Function)组件
    • 参考
  • 使用 Hooks
    • 使用库中的 hooks
    • 使用 react 的内置 hooks
    • 使用 context
    • 使用自定义 hooks
    • 注意 hooks 的规则和注意事项
    • 我们的基础视图组件仍然是基于类的
    • 不要为 hooks 重写
  • 使用 React Testing Library
    • 查询
    • 技巧
  • 迁移 - grid-emotion
    • 组件
    • 属性
      • margin 和 padding
      • flexbox

前端手册

本指南涵盖了我们如何在 Sentry 编写前端代码,
并特别关注
Sentry
和
Getsentry
代码库。
它假设您使用的是
eslint-config-sentry
概述的 eslint 规则;
因此,这里不会讨论由这些 linting 规则强制执行的代码风格。

  • https://github.com/getsentry/eslint-config-sentry

目录结构

前端代码库当前位于 sentry 中的 src/sentry/static/sentry/app 和 getentry 中的 static/getsentry 下。 (我们打算在未来与 static/sentry 保持一致。)

文件夹和文件结构

文件命名

  • 根据模块的功能或类的使用方式或使用它们的应用程序部分,有意义地命名文件。
  • 除非必要,否则不要使用前缀或后缀(即 dataScrubbingEditModal、dataScrubbingAddModal),而是使用像 dataScrubbing/editModal 这样的名称。

使用 index.(j|t)?(sx)

在文件夹中有一个 index 文件提供了一种隐式导入主文件而不指定它的方法

index 文件的使用应遵循以下规则:

  • 如果创建文件夹来对一起使用的组件进行分组,并且有一个入口点组件,它使用分组内的组件(examples、avatar、idBadge)。入口点组件应该是 index 文件。

  • 不要使用 index.(j|t)?(sx) 文件,如果文件夹包含在应用程序的其他部分使用的组件,与入口点文件无关。(即,actionCreators,panels)

  • 不要仅仅为了重新导出而使用 index 文件。更倾向于导入单个组件。

React

定义 React 组件

新组件在需要访问 this 时使用 class 语法,以及类字段+箭头函数方法定义。

class Note extends React.Component {
  static propTypes = {
    author: PropTypes.object.isRequired,
    onEdit: PropTypes.func.isRequired,
  };

  // 请注意,方法是使用箭头函数类字段定义的(绑定“this”)
  handleChange = value => {
    let user = ConfigStore.get('user');

    if (user.isSuperuser) {
      this.props.onEdit(value);
    }
  };

  render() {
    let {content} = this.props; // 对 props 使用解构赋值

    return <div onChange={this.handleChange}>{content}</div>;
  }
}

export default Note;

一些较旧的组件使用 createReactClass 和 mixins,但这已被弃用。

组件与视图

app/components/ 和 app/views 文件夹都包含 React 组件。

  • 使用通常不会在代码库的其他部分重用的 UI 视图。
  • 使用设计为高度可重用的 UI 组件。

组件应该有一个关联的 .stories.js 文件来记录它应该如何使用。

使用 yarn storybook 在本地运行 Storybook 或在 https://storybook.getsentry.net/ 上查看托管版本

PropTypes

使用它们,要明确,尽可能使用共享的自定义属性。

更倾向 Proptypes.arrayOf 而不是 PropTypes.array 和 PropTypes.shape 而不是 PropTypes.object

如果你使用一组重要的、定义良好的 key(你的组件依赖)传递对象,那么使用 PropTypes.shape 显式定义它们:

PropTypes.shape({
  username: PropTypes.string.isRequired,
  email: PropTypes.string
})

如果您要重复使用自定义 prop-type 或传递常见的共享 shape(如 organization、project 或 user),
请确保从我们有用的自定义集合中导入 proptype!

  • https://github.com/getsentry/sentry/blob/master/static/app/sentryTypes.tsx

事件处理程序

我们使用不同的前缀来更好地区分事件处理程序和事件回调属性。

对事件处理程序使用 handle 前缀,例如:

<Button onClick={this.handleDelete}/>

对于传递给组件的事件回调属性,请使用 on 前缀,例如:

<Button onClick={this.props.onDelete}>

CSS 和 Emotion

  • 使用 Emotion,使用 theme 对象。
  • 最好的样式是您不编写的样式 - 尽可能使用现有组件。
  • 新代码应该使用 css-in-js 库 e m o t i o n - 它允许您将样式绑定到元素而无需全局选择器的间接性。 你甚至不需要打开另一个文件!
  • 从 props.theme 获取常量(z-indexes, paddings, colors)
    • https://emotion.sh/
    • https://github.com/getsentry/sentry/blob/master/static/app/utils/theme.tsx
import styled from 'react-emotion';

const SomeComponent = styled('div')`
  border-radius: 1.45em;
  font-weight: bold;
  z-index: ${p => p.theme.zIndex.modal};
  padding: ${p => p.theme.grid}px ${p => p.theme.grid * 2}px;
  border: 1px solid ${p => p.theme.borderLight};
  color: ${p => p.theme.purple};
  box-shadow: ${p => p.theme.dropShadowHeavy};
`;

export default SomeComponent;
  • 请注意,reflexbox(例如Flex 和Box)已被弃用,请避免在新代码中使用。

stylelint 错误

"No duplicate selectors"

当您使用样式组件(styled component)作为选择器时会发生这种情况,我们需要通过使用注释来辅助 linter 来告诉 stylelint 我们正在插入的是一个选择器。 例如


const ButtonBar = styled("div")`
  ${/* sc-selector */Button) {
     border-radius: 0;
  }
`;

有关其他标签和更多信息,请参阅。

  • https://styled-components.com/docs/tooling#interpolation-tagging

状态管理

我们目前使用 Reflux 来管理全局状态。

Reflux 实现了 Flux 概述的单向数据流模式。
Store 注册在 app/stores 下,用于存储应用程序使用的各种数据。
Action 需要在 app/actions 下注册。
我们使用 action creator 函数(在 app/actionCreators 下)来分派 action。
Reflux store 监听 action 并相应地更新自己。

我们目前正在探索 Reflux 库的替代方案以供将来使用。

  • https://github.com/reflux/refluxjs
  • https://facebook.github.io/flux/docs/overview.html

测试

我们正在远离 Enzyme,转而使用 React Testing Library。有关 RTL 提示,请查看此页面。

注意:你的文件名必须是 .spec.jsx 否则 jest 不会运行它!

我们在 setup.js 中定义了有用的 fixtures,使用这些!
如果您以重复的方式定义模拟数据,则可能值得添加此文件。routerContext 是一种特别有用的方法,用于提供大多数视图所依赖的上下文对象。

  • https://github.com/getsentry/sentry/blob/master/tests/js/setup.ts

Client.addMockResponse 是模拟 API 请求的最佳方式。
这是我们的代码,
所以如果它让您感到困惑,只需将 console.log() 语句放入其逻辑中即可!

  • https://github.com/getsentry/sentry/blob/master/static/app/mocks/api.tsx

我们测试环境中的一个重要问题是,enzyme 修改了 react 生命周期的许多方面以同步评估(即使它们通常是异步的)。
当您触发某些逻辑并且没有立即在您的断言逻辑中反映出来时,这可能会使您陷入一种虚假的安全感。

标记您的测试方法 async 并使用 await tick(); 实用程序可以让事件循环刷新运行事件并修复此问题:

wrapper.find('ExpandButton').simulate('click');
await tick();
expect(wrapper.find('CommitRow')).toHaveLength(2);

选择器

如果您正在编写 jest 测试,您可以使用 Component(和 Styled Component)名称作为选择器。
此外,如果您需要使用 DOM 查询选择器,请使用 data-test-id 而不是类名。我们目前没有,但我们可以在构建过程中使用 babel 去除它。

测试中未定义的 theme 属性

而不是使用来自 enzyme 的 mount() ...使用这个:import {mountWithTheme} from 'sentry-test/enzyme' 以便被测组件用 <ThemeProvider>。

  • https://emotion.sh/docs/theming

Babel 语法插件

我们决定只使用处于 stage 3(或更高版本)的 ECMAScript 提案(参见 TC39 提案)。
此外,因为我们正在迁移到 typescript,我们将与他们的编译器支持的内容保持一致。唯一的例外是装饰器。

  • https://github.com/tc39/proposals

新语法

可选链

可选链 帮助我们访问 [嵌套] 对象,
而无需在每个属性/方法访问之前检查是否存在。
如果我们尝试访问 undefined 或 null 对象的属性,它将停止并返回 undefined。

  • https://github.com/tc39/proposal-optional-chaining
语法

可选链操作符拼写为 ?.。它可能出现在三个位置:

obj?.prop       // 可选的静态属性访问
obj?.[expr]     // 可选的动态属性访问
func?.(...args) // 可选的函数或方法调用

来自 https://github.com/tc39/proposal-optional-chaining

空值合并

这是一种设置“默认”值的方法。例如:以前你会做类似的事情

let x = volume || 0.5;

这是一个问题,因为 0 是 volume 的有效值,但因为它的计算结果为 false -y,我们不会使表达式短路,并且 x 的值为 0.5

如果我们使用空值合并

  • https://github.com/tc39/proposal-nullish-coalescing
let x = volume ?? 0.5

如果 volume 为 null 或 undefined,它只会默认为 0.5。

语法

基本情况。如果表达式在 ?? 的左侧运算符计算为 undefined 或 null,则返回其右侧。

const response = {
  settings: {
    nullValue: null,
    height: 400,
    animationDuration: 0,
    headerText: '',
    showSplashScreen: false
  }
};

const undefinedValue = response.settings.undefinedValue ?? 'some other default'; // result: 'some other default'
const nullValue = response.settings.nullValue ?? 'some other default'; // result: 'some other default'
const headerText = response.settings.headerText ?? 'Hello, world!'; // result: ''
const animationDuration = response.settings.animationDuration ?? 300; // result: 0
const showSplashScreen = response.settings.showSplashScreen ?? true; // result: false

From https://github.com/tc39/proposal-nullish-coalescing

Lodash

确保不要使用默认的 lodash 包导入 lodash 实用程序。
有一个 eslint 规则来确保这不会发生。
而是直接导入实用程序,例如 import isEqual from 'lodash/isEqual';。

以前我们使用了 lodash-webpack-plugin 和
babel-plugin-lodash 的组合,
但是在尝试使用新的 lodash 实用程序(例如这个 PR)时很容易忽略这些插件和配置。
通过 webpack tree shaking 和 eslint 强制执行,我们应该能够保持合理的包大小。

  • https://www.npmjs.com/package/lodash-webpack-plugin
  • https://github.com/lodash/babel-plugin-lodash
  • https://github.com/getsentry/sentry/pull/13834

有关更多信息,请参阅此 PR。

  • https://github.com/getsentry/sentry/pull/15521

我们更喜欢使用可选链和空值合并而不是来自 lodash/get 的 get。

Typescript

  • Typing DefaultProps

迁移指南

  • Grid-Emotion

Storybook Styleguide

引用其文档,“Storybook 是用于 UI 组件的 UI 开发环境。
有了它,您可以可视化 UI 组件的不同状态并以交互方式开发它们。”

更多细节在这里:

  • https://storybook.js.org/

我们使用它吗?

是的!我们将 Storybook 用于 getsentry/sentry 项目。
Storybook 的配置可以在 https://github.com/getsentry/sentry/tree/master/.storybook 中找到。

要在本地运行 Storybook,请在 getsentry/sentry 存储库的根目录中运行 npm run storybook。

它部署在某个地方吗?

Sentry 的 Storybook 是使用 Vercel 构建和部署的。
每个 Pull Request 都有自己的部署,每次推送到主分支都会部署到 https://storybook.sentry.dev。

  • https://storybook.sentry.dev

Typing DefaultProps

由于 Typescript 3.0 默认 props 可以更简单地输入。有几种不同的方法适合不同的场景。

类(Class)组件

import React from 'react';

type DefaultProps = {
  size: 'Small' | 'Medium' | 'Large'; // 这些不应标记为可选
};

// 没有 Partial<DefaultProps>
type Props = DefaultProps & {
  name: string;
  codename?: string;
};

class Planet extends React.Component<Props> {
  // 没有 Partial<Props> 因为它会将所有内容标记为可选
  static defaultProps: DefaultProps = {
    size: 'Medium',
  };

  render() {
    const {name, size, codename} = this.props;

    return (
      <p>
        {name} is a {size.toLowerCase()} planet.
        {codename && ` Its codename is ${codename}`}
      </p>
    );
  }
}

const planet = <Planet name="Mars" />;

或在 typeof 的帮助下:

import React from 'react';

const defaultProps = {
  size: 'Medium' as 'Small' | 'Medium' | 'Large',
};

type Props = {
  name: string;
  codename?: string;
} & typeof defaultProps;
// 没有 Partial<typeof defaultProps> 因为它会将所有内容标记为可选

class Planet extends React.Component<Props> {
  static defaultProps = defaultProps;

  render() {
    const {name, size, codename} = this.props;

    return (
      <p>
        {name} is a {size.toLowerCase()} planet. Its color is{' '}
        {codename && ` Its codename is ${codename}`}
      </p>
    );
  }
}

const planet = <Planet name="Mars" />;

函数式(Function)组件

import React from 'react';

// 函数组件上的 defaultProps 将在未来停止使用
// https://twitter.com/dan_abramov/status/1133878326358171650
// https://github.com/reactjs/rfcs/pull/107
// 我们应该使用默认参数

type Props = {
  name: string;
  size?: 'Small' | 'Medium' | 'Large'; // 具有 es6 默认参数的属性应标记为可选
  codename?: string;
};

// 共识是输入解构的 Props 比使用 React.FC<Props> 稍微好一点
// https://github.com/typescript-cheatsheets/react-typescript-cheatsheet#function-components
const Planet = ({name, size = 'Medium', codename}: Props) => {
  return (
    <p>
      {name} is a {size.toLowerCase()} planet.
      {codename && ` Its codename is ${codename}`}
    </p>
  );
};

const planet = <Planet name="Mars" />;

参考

  • Typescript 3.0 Release notes
    • https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#support-for-defaultprops-in-jsx
  • Stack Overflow question on typing default props
    • https://stackoverflow.com/questions/37282159/default-property-value-in-react-component-using-typescript/37282264#37282264

使用 Hooks

为了使组件更易于重用和更易于理解,React 和 React 生态系统一直趋向于函数式组件和 hooks。
Hooks 是一种向功能组件添加状态和副作用的便捷方式。它们还为库提供了一种公开行为的便捷方式。

虽然我们通常支持 hooks,但我们有一些关于 hooks 应该如何与 Sentry 前端一起使用的建议。

使用库中的 hooks

如果一个库提供了 hooks,你应该使用它们。
通常,这将是使用库的唯一方法。
例如,dnd-kit 通过钩子公开了它的所有原语(primitives),我们应该按照预期的方式使用该库。

我们不喜欢使用不用 hooks 的库。
相反,与具有更大、更复杂的 API 或更大的包大小的库相比,
更喜欢具有更清晰、更简单的 API 和更小的包大小的库。

使用 react 的内置 hooks

useState, useMemo, useCallback, useContext 和 useRef hooks 在任何函数式组件中都是受欢迎的。
在需要少量状态或访问 react 原语(如引用和上下文)的展示组件中,它们通常是一个不错的选择。
例如,具有滑出(slide-out)或可展开状态(expandable state)的组件。

useEffect hook 更复杂,您需要小心地跟踪您的依赖项并确保通过清理回调取消订阅。
应避免 useEffect 的复杂链式应用程序,此时 'controller' 组件应保持基于类(class)。

同样,useReducer 钩子与目前尚未确定的状态管理重叠。
我们希望避免 又一个 状态管理模式,因此此时避免使用useReducer。

使用 context

当我们计划远离 Reflux 的路径时,useContext hook 提供了一个更简单的实现选项来共享状态和行为。
当您需要创建新的共享状态源时,请考虑使用 context 和 useContext 而不是 Reflux。
此外,可以利用虫洞状态管理模式来公开共享状态和突变函数。

  • https://swizec.com/blog/wormhole-state-management

使用自定义 hooks

可以创建自定义 hooks 来共享应用程序中的可重用逻辑。
创建自定义 hook 时,函数名称必须遵循约定,以 “use” 开头(例如 useTheme),
并且可以在自定义 hooks 内调用其他 hooks。

注意 hooks 的规则和注意事项

React hooks 有一些规则。
请注意 hooks 创建的规则和限制。
我们使用 ESLint 规则来防止大多数 hook 规则被非法侵入。

  • https://reactjs.org/docs/hooks-rules.html

此外,我们建议您尽量少使用 useEffect。
使用多个 useEffect 回调表示您有一个高度有状态的组件,
您应该使用类(class)组件来代替。

我们的基础视图组件仍然是基于类的

我们的基础视图组件(AsyncView 和 AsyncComponent)是基于类的,并且会持续很长时间。
在构建视图时请记住这一点。
您将需要额外的 wrapper 组件来访问 hooks 或将 hook state 转换为您的 AsyncComponent 的 props。

不要为 hooks 重写

虽然 hooks 可以在新代码中符合人体工程学,但我们应该避免重写现有代码以利用 hooks。
重写需要时间,使我们面临风险,并且为最终用户提供的价值很小。

如果您需要重新设计一个组件以使用库中的 hooks,那么还可以考虑从一个类转换为一个函数组件。

使用 React Testing Library

我们正在将我们的测试从 Enzyme 转换为 React Testing Library。
在本指南中,您将找到遵循最佳实践和避免常见陷阱的技巧。

我们有两个 ESLint 规则来帮助解决这个问题:

  • eslint-plugin-jest-dom
    • https://github.com/testing-library/eslint-plugin-jest-dom
  • eslint-plugin-testing-library
    • https://github.com/testing-library/eslint-plugin-testing-library

我们努力以一种与应用程序使用方式非常相似的方式编写测试。

我们不是处理渲染组件的实例,而是以与用户相同的方式查询 DOM。
我们通过 label 文本找到表单元素(就像用户一样),我们从他们的文本中找到链接和按钮(就像用户一样)。

作为此目标的一部分,我们避免测试实现细节,因此重构(更改实现但不是功能)不会破坏测试。

我们通常赞成用例覆盖而不是代码覆盖。

查询

  • 尽可能使用 getBy...
  • 仅在检查不存在时使用 queryBy...
  • 仅当期望元素在可能不会立即发生的 DOM 更改后出现时才使用 await findBy...

为确保测试类似于用户与我们的代码交互的方式,我们建议使用以下优先级进行查询:

  1. getByRole - 这应该是几乎所有东西的首选选择器。
    作为这个选择器的一个很好的奖励,我们确保我们的应用程序是可访问的。
    它很可能与 name 选项 getByRole('button', {name: /save/i}) 一起使用。
    name 通常是表单元素的 label 或 button 的文本内容,或 aria-label 属性的值。
    如果不确定,请使用 logRoles 功能
    或查阅可用角色列表。
  • https://testing-library.com/docs/dom-testing-library/api-accessibility/#logroles
  • https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles
  1. getByLabelText/getByPlaceholderText - 用户使用 label 文本查找表单元素,因此在测试表单时首选此选项。
  2. getByText - 在表单之外,文本内容是用户查找元素的主要方式。此方法可用于查找非交互式元素(如 div、span 和 paragraph)。
  3. getByTestId - 因为这不反映用户如何与应用交互,所以只推荐用于不能使用任何其他选择器的情况

如果您仍然无法决定使用哪个查询,
请查看 testing-playground.com
以及 screen.logTestingPlaygroundURL() 及其浏览器扩展。

  • https://testing-playground.com/

不要忘记,你可以在测试中的任何地方放置 screen.debug() 来查看当前的 DOM。

在官方文档中阅读有关查询的更多信息。

  • https://testing-library.com/docs/queries/about/

技巧

避免从 render 方法中解构查询函数,而是使用 screen(examples)。
当您添加/删除您需要的查询时,您不必使 render 调用解构保持最新。
您只需要输入 screen 并让您的编辑器的自动完成功能处理其余的工作。

  • https://github.com/getsentry/sentry/pull/29312
import { mountWithTheme, screen } from "sentry-test/reactTestingLibrary";

// ❌
const { getByRole } = mountWithTheme(<Example />);
const errorMessageNode = getByRole("alert");

// ✅
mountWithTheme(<Example />);
const errorMessageNode = screen.getByRole("alert");

除了检查不存在(examples)之外,避免将 queryBy... 用于任何事情。
如果没有找到元素,getBy... 和 findBy... 变量将抛出更有用的错误消息。

  • https://github.com/getsentry/sentry/pull/29517
import { mountWithTheme, screen } from "sentry-test/reactTestingLibrary";

// ❌
mountWithTheme(<Example />);
expect(screen.queryByRole("alert")).toBeInTheDocument();

// ✅
mountWithTheme(<Example />);
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();

避免使用 waitFor 等待出现,而是使用 findBy...(examples)。
这两个基本上是等价的(findBy... 甚至在其里面使用了 waitFor),但是 findBy... 更简单,我们得到的错误信息也会更好。

  • https://github.com/getsentry/sentry/pull/29544
import {
  mountWithTheme,
  screen,
  waitFor,
} from "sentry-test/reactTestingLibrary";

// ❌
mountWithTheme(<Example />);
await waitFor(() => {
  expect(screen.getByRole("alert")).toBeInTheDocument();
});

// ✅
mountWithTheme(<Example />);
expect(await screen.findByRole("alert")).toBeInTheDocument();

避免使用 waitFor 等待消失,使用 waitForElementToBeRemoved 代替(examples)。

  • https://github.com/getsentry/sentry/pull/29547

后者使用 MutationObserver,这比使用 waitFor 定期轮询 DOM 更有效。

import {
  mountWithTheme,
  screen,
  waitFor,
  waitForElementToBeRemoved,
} from "sentry-test/reactTestingLibrary";

// ❌
mountWithTheme(<Example />);
await waitFor(() =>
  expect(screen.queryByRole("alert")).not.toBeInTheDocument()
);

// ✅
mountWithTheme(<Example />);
await waitForElementToBeRemoved(() => screen.getByRole("alert"));

更喜欢使用 jest-dom 断言(examples)。
使用这些推荐的断言的优点是更好的错误消息、整体语义、一致性和统一性。

  • https://github.com/getsentry/sentry/pull/29508
import { mountWithTheme, screen } from "sentry-test/reactTestingLibrary";

// ❌
mountWithTheme(<Example />);
expect(screen.getByRole("alert")).toBeTruthy();
expect(screen.getByRole("alert").textContent).toEqual("abc");
expect(screen.queryByRole("button")).toBeFalsy();
expect(screen.queryByRole("button")).toBeNull();

// ✅
mountWithTheme(<Example />);
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveTextContent("abc");
expect(screen.queryByRole("button")).not.toBeInTheDocument();

按文本搜索时,最好使用不区分大小写的正则表达式。它将使测试更能适应变化。

import { mountWithTheme, screen } from "sentry-test/reactTestingLibrary";

// ❌
mountWithTheme(<Example />);
expect(screen.getByText("Hello World")).toBeInTheDocument();

// ✅
mountWithTheme(<Example />);
expect(screen.getByText(/hello world/i)).toBeInTheDocument();

尽可能在 fireEvent 上使用 userEvent。
userEvent 来自 @testing-library/user-event 包,它构建在 fireEvent 之上,但它提供了几种更类似于用户交互的方法。

// ❌
import {
  mountWithTheme,
  screen,
  fireEvent,
} from "sentry-test/reactTestingLibrary";
mountWithTheme(<Example />);
fireEvent.change(screen.getByLabelText("Search by name"), {
  target: { value: "sentry" },
});

// ✅
import {
  mountWithTheme,
  screen,
  userEvent,
} from "sentry-test/reactTestingLibrary";
mountWithTheme(<Example />);
userEvent.type(screen.getByLabelText("Search by name"), "sentry");

迁移 - grid-emotion

grid-emotion 已经被弃用一年多了,新项目是 reflexbox。
为了升级到最新版本的 emotion,我们需要迁移出 grid-emotion。

要迁移,请使用 emotion 将导入的 <Flex> 和 <Box> 组件替换为带样式的组件。

组件

用下面的替换组件,然后删除必要的 props 并移动到 styled component。

<Flex>

const Flex = styled('div')`
  display: flex;
`;

<Box>

const Box = styled('div')`
`;

props

如果您正在修改导出的组件,请确保通过该组件的代码库进行 grep 以确保它没有被渲染为特定于 grid-emotion 的附加属性。示例是<Panel> 组件。

margin 和 padding

Margin 属性 以 m 开头,以 p 填充。下面的例子将使用 margin 作为例子

旧 (grid-emotion) 新 (css/emotion/styled) m={2} margin: ${space(2); mx={2} margin-left: ${space(2); margin-right: ${space(2)}; my={2} margin-top: ${space(2); margin-bottom: ${space(2)}; ml={2} margin-left: ${space(2); mr={2} margin-right: ${space(2); mt={2} margin-top: ${space(2); mb={2} margin-bottom: ${space(2);

flexbox

这些是 flexbox 属性

旧 (grid-emotion) 新 (css/emotion/styled) align="center" align-items: center; justify="center" justify-content: center; direction="column" flex-direction: column; wrap="wrap" flex-wrap: wrap;

现在只需忽略 grid-emotion 的导入语句,例如 // eslint-disable-line no-restricted-imports