Sentry 開發(fā)者貢獻(xiàn)指南 - 前端(ReactJS生態(tài))
本文轉(zhuǎn)載自微信公眾號(hào)「黑客下午茶」,作者為少 。轉(zhuǎn)載本文請(qǐng)聯(lián)系黑客下午茶公眾號(hào)。
前端手冊(cè)
本指南涵蓋了我們?nèi)绾卧?Sentry 編寫前端代碼, 并特別關(guān)注 Sentry 和 Getsentry 代碼庫。它假設(shè)您使用的是 eslint-config-sentry 概述的 eslint 規(guī)則;因此,這里不會(huì)討論由這些 linting 規(guī)則強(qiáng)制執(zhí)行的代碼風(fēng)格。
- https://github.com/getsentry/eslint-config-sentry
目錄結(jié)構(gòu)
前端代碼庫當(dāng)前位于 sentry 中的 src/sentry/static/sentry/app 和 getentry 中的 static/getsentry 下。(我們打算在未來與 static/sentry 保持一致。)
文件夾和文件結(jié)構(gòu)
文件命名
- 根據(jù)模塊的功能或類的使用方式或使用它們的應(yīng)用程序部分,有意義地命名文件。
- 除非必要,否則不要使用前綴或后綴(即 dataScrubbingEditModal、dataScrubbingAddModal),而是使用像 dataScrubbing/editModal 這樣的名稱。
使用 index.(j|t)?(sx)
在文件夾中有一個(gè) index 文件提供了一種隱式導(dǎo)入主文件而不指定它的方法
index 文件的使用應(yīng)遵循以下規(guī)則:
- 如果創(chuàng)建文件夾來對(duì)一起使用的組件進(jìn)行分組,并且有一個(gè)入口點(diǎn)組件,它使用分組內(nèi)的組件(examples、avatar、idBadge)。入口點(diǎn)組件應(yīng)該是 index 文件。
- 不要使用 index.(j|t)?(sx) 文件,如果文件夾包含在應(yīng)用程序的其他部分使用的組件,與入口點(diǎn)文件無關(guān)。(即,actionCreators,panels)
- 不要僅僅為了重新導(dǎo)出而使用 index 文件。更傾向于導(dǎo)入單個(gè)組件。
React
定義 React 組件
新組件在需要訪問 this 時(shí)使用 class 語法,以及類字段+箭頭函數(shù)方法定義。
- class Note extends React.Component {
- static propTypes = {
- author: PropTypes.object.isRequired,
- onEdit: PropTypes.func.isRequired,
- };
- // 請(qǐng)注意,方法是使用箭頭函數(shù)類字段定義的(綁定“this”)
- handleChange = value => {
- let user = ConfigStore.get('user');
- if (user.isSuperuser) {
- this.props.onEdit(value);
- }
- };
- render() {
- let {content} = this.props; // 對(duì) props 使用解構(gòu)賦值
- return <div onChange={this.handleChange}>{content}</div>;
- }
- }
- export default Note;
一些較舊的組件使用 createReactClass 和 mixins,但這已被棄用。
組件與視圖
app/components/ 和 app/views 文件夾都包含 React 組件。
使用通常不會(huì)在代碼庫的其他部分重用的 UI 視圖。
使用設(shè)計(jì)為高度可重用的 UI 組件。
組件應(yīng)該有一個(gè)關(guān)聯(lián)的 .stories.js 文件來記錄它應(yīng)該如何使用。
使用 yarn storybook 在本地運(yùn)行 Storybook 或在 https://storybook.getsentry.net/ 上查看托管版本
PropTypes
使用它們,要明確,盡可能使用共享的自定義屬性。
更傾向 Proptypes.arrayOf 而不是 PropTypes.array 和 PropTypes.shape 而不是 PropTypes.object
如果你使用一組重要的、定義良好的 key(你的組件依賴)傳遞對(duì)象,那么使用 PropTypes.shape 顯式定義它們:
- PropTypes.shape({
- username: PropTypes.string.isRequired,
- email: PropTypes.string
- })
如果您要重復(fù)使用自定義 prop-type 或傳遞常見的共享 shape(如 organization、project 或 user), 請(qǐng)確保從我們有用的自定義集合中導(dǎo)入 proptype!
- https://github.com/getsentry/sentry/blob/master/static/app/sentryTypes.tsx
事件處理程序
我們使用不同的前綴來更好地區(qū)分事件處理程序和事件回調(diào)屬性。
對(duì)事件處理程序使用 handle 前綴,例如:
- <Button onClick={this.handleDelete}/>
對(duì)于傳遞給組件的事件回調(diào)屬性,請(qǐng)使用 on 前綴,例如:
- <Button onClick={this.props.onDelete}>
CSS 和 Emotion
- 使用 Emotion,使用 theme 對(duì)象。
- 最好的樣式是您不編寫的樣式 - 盡可能使用現(xiàn)有組件。
- 新代碼應(yīng)該使用 css-in-js 庫 e m o t i o n - 它允許您將樣式綁定到元素而無需全局選擇器的間接性。你甚至不需要打開另一個(gè)文件!
- 從 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;
請(qǐng)注意,reflexbox(例如Flex 和Box)已被棄用,請(qǐng)避免在新代碼中使用。
stylelint 錯(cuò)誤
"No duplicate selectors"
當(dāng)您使用樣式組件(styled component)作為選擇器時(shí)會(huì)發(fā)生這種情況,我們需要通過使用注釋來輔助 linter 來告訴 stylelint 我們正在插入的是一個(gè)選擇器。例如
- const ButtonBar = styled("div")`
- ${/* sc-selector */Button) {
- border-radius: 0;
- }
- `;
有關(guān)其他標(biāo)簽和更多信息,請(qǐng)參閱。
- https://styled-components.com/docs/tooling#interpolation-tagging
狀態(tài)管理
我們目前使用 Reflux 來管理全局狀態(tài)。
Reflux 實(shí)現(xiàn)了 Flux 概述的單向數(shù)據(jù)流模式。 Store 注冊(cè)在 app/stores 下,用于存儲(chǔ)應(yīng)用程序使用的各種數(shù)據(jù)。 Action 需要在 app/actions 下注冊(cè)。我們使用 action creator 函數(shù)(在 app/actionCreators 下)來分派 action。 Reflux store 監(jiān)聽 action 并相應(yīng)地更新自己。
我們目前正在探索 Reflux 庫的替代方案以供將來使用。
- https://github.com/reflux/refluxjs
- https://facebook.github.io/flux/docs/overview.html
測(cè)試
我們正在遠(yuǎn)離 Enzyme,轉(zhuǎn)而使用 React Testing Library。有關(guān) RTL 提示,請(qǐng)查看此頁面。
注意:你的文件名必須是 .spec.jsx 否則 jest 不會(huì)運(yùn)行它!
我們?cè)?setup.js 中定義了有用的 fixtures,使用這些!如果您以重復(fù)的方式定義模擬數(shù)據(jù),則可能值得添加此文件。routerContext 是一種特別有用的方法,用于提供大多數(shù)視圖所依賴的上下文對(duì)象。
- https://github.com/getsentry/sentry/blob/master/tests/js/setup.ts
Client.addMockResponse 是模擬 API 請(qǐng)求的最佳方式。這是我們的代碼, 所以如果它讓您感到困惑,只需將 console.log() 語句放入其邏輯中即可!
- https://github.com/getsentry/sentry/blob/master/static/app/__mocks__/api.tsx
我們測(cè)試環(huán)境中的一個(gè)重要問題是,enzyme 修改了 react 生命周期的許多方面以同步評(píng)估(即使它們通常是異步的)。當(dāng)您觸發(fā)某些邏輯并且沒有立即在您的斷言邏輯中反映出來時(shí),這可能會(huì)使您陷入一種虛假的安全感。
標(biāo)記您的測(cè)試方法 async 并使用 await tick(); 實(shí)用程序可以讓事件循環(huán)刷新運(yùn)行事件并修復(fù)此問題:
- wrapper.find('ExpandButton').simulate('click');
- await tick();
- expect(wrapper.find('CommitRow')).toHaveLength(2);
選擇器
如果您正在編寫 jest 測(cè)試,您可以使用 Component(和 Styled Component)名稱作為選擇器。此外,如果您需要使用 DOM 查詢選擇器,請(qǐng)使用 data-test-id 而不是類名。我們目前沒有,但我們可以在構(gòu)建過程中使用 babel 去除它。
測(cè)試中未定義的 theme 屬性
而不是使用來自 enzyme 的 mount() ...使用這個(gè):import {mountWithTheme} from 'sentry-test/enzyme' 以便被測(cè)組件用
- https://emotion.sh/docs/theming
Babel 語法插件
我們決定只使用處于 stage 3(或更高版本)的 ECMAScript 提案(參見 TC39 提案)。此外,因?yàn)槲覀冋谶w移到 typescript,我們將與他們的編譯器支持的內(nèi)容保持一致。唯一的例外是裝飾器。
- https://github.com/tc39/proposals
新語法
可選鏈
可選鏈 幫助我們?cè)L問 [嵌套] 對(duì)象, 而無需在每個(gè)屬性/方法訪問之前檢查是否存在。如果我們嘗試訪問 undefined 或 null 對(duì)象的屬性,它將停止并返回 undefined。
https://github.com/tc39/proposal-optional-chaining
語法
可選鏈操作符拼寫為 ?.。它可能出現(xiàn)在三個(gè)位置:
- obj?.prop // 可選的靜態(tài)屬性訪問
- obj?.[expr] // 可選的動(dòng)態(tài)屬性訪問
- func?.(...args) // 可選的函數(shù)或方法調(diào)用
來自 https://github.com/tc39/proposal-optional-chaining
空值合并
這是一種設(shè)置“默認(rèn)”值的方法。例如:以前你會(huì)做類似的事情
- let x = volume || 0.5;
這是一個(gè)問題,因?yàn)?0 是 volume 的有效值,但因?yàn)樗挠?jì)算結(jié)果為 false -y,我們不會(huì)使表達(dá)式短路,并且 x 的值為 0.5
如果我們使用空值合并
- https://github.com/tc39/proposal-nullish-coalescing
- let x = volume ?? 0.5
如果 volume 為 null 或 undefined,它只會(huì)默認(rèn)為 0.5。
語法
基本情況。如果表達(dá)式在 ?? 的左側(cè)運(yùn)算符計(jì)算為 undefined 或 null,則返回其右側(cè)。
- 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
Lodash
確保不要使用默認(rèn)的 lodash 包導(dǎo)入 lodash 實(shí)用程序。有一個(gè) eslint 規(guī)則來確保這不會(huì)發(fā)生。而是直接導(dǎo)入實(shí)用程序,例如 import isEqual from 'lodash/isEqual';。
以前我們使用了 lodash-webpack-plugin 和 babel-plugin-lodash 的組合, 但是在嘗試使用新的 lodash 實(shí)用程序(例如這個(gè) PR)時(shí)很容易忽略這些插件和配置。通過 webpack tree shaking 和 eslint 強(qiáng)制執(zhí)行,我們應(yīng)該能夠保持合理的包大小。
- https://www.npmjs.com/package/lodash-webpack-plugin
- https://github.com/lodash/babel-plugin-lodash
- https://github.com/getsentry/sentry/pull/13834
有關(guān)更多信息,請(qǐng)參閱此 PR。
- https://github.com/getsentry/sentry/pull/15521
我們更喜歡使用可選鏈和空值合并而不是來自 lodash/get 的 get。
Typescript
- Typing DefaultProps
遷移指南
- Grid-Emotion
Storybook Styleguide
引用其文檔,“Storybook 是用于 UI 組件的 UI 開發(fā)環(huán)境。有了它,您可以可視化 UI 組件的不同狀態(tài)并以交互方式開發(fā)它們。”
更多細(xì)節(jié)在這里:
- https://storybook.js.org/
我們使用它嗎?
是的!我們將 Storybook 用于 getsentry/sentry 項(xiàng)目。 Storybook 的配置可以在 https://github.com/getsentry/sentry/tree/master/.storybook 中找到。
要在本地運(yùn)行 Storybook,請(qǐng)?jiān)?getsentry/sentry 存儲(chǔ)庫的根目錄中運(yùn)行 npm run storybook。
它部署在某個(gè)地方嗎?
Sentry 的 Storybook 是使用 Vercel 構(gòu)建和部署的。每個(gè) Pull Request 都有自己的部署,每次推送到主分支都會(huì)部署到 https://storybook.sentry.dev。
- https://storybook.sentry.dev
Typing DefaultProps
由于 Typescript 3.0 默認(rèn) props 可以更簡單地輸入。有幾種不同的方法適合不同的場(chǎng)景。
類(Class)組件
- import React from 'react';
- type DefaultProps = {
- size: 'Small' | 'Medium' | 'Large'; // 這些不應(yīng)標(biāo)記為可選
- };
- // 沒有 Partial<DefaultProps>
- type Props = DefaultProps & {
- name: string;
- codename?: string;
- };
- class Planet extends React.Component<Props> {
- // 沒有 Partial<Props> 因?yàn)樗鼤?huì)將所有內(nèi)容標(biāo)記為可選
- 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> 因?yàn)樗鼤?huì)將所有內(nèi)容標(biāo)記為可選
- 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" />;
函數(shù)式(Function)組件
- import React from 'react';
- // 函數(shù)組件上的 defaultProps 將在未來停止使用
- // https://twitter.com/dan_abramov/status/1133878326358171650
- // https://github.com/reactjs/rfcs/pull/107
- // 我們應(yīng)該使用默認(rèn)參數(shù)
- type Props = {
- name: string;
- size?: 'Small' | 'Medium' | 'Large'; // 具有 es6 默認(rèn)參數(shù)的屬性應(yīng)標(biāo)記為可選
- codename?: string;
- };
- // 共識(shí)是輸入解構(gòu)的 Props 比使用 React.FC<Props> 稍微好一點(diǎn)
- // 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 生態(tài)系統(tǒng)一直趨向于函數(shù)式組件和 hooks。 Hooks 是一種向功能組件添加狀態(tài)和副作用的便捷方式。它們還為庫提供了一種公開行為的便捷方式。
雖然我們通常支持 hooks,但我們有一些關(guān)于 hooks 應(yīng)該如何與 Sentry 前端一起使用的建議。
使用庫中的 hooks
如果一個(gè)庫提供了 hooks,你應(yīng)該使用它們。通常,這將是使用庫的唯一方法。例如,dnd-kit 通過鉤子公開了它的所有原語(primitives),我們應(yīng)該按照預(yù)期的方式使用該庫。
我們不喜歡使用不用 hooks 的庫。相反,與具有更大、更復(fù)雜的 API 或更大的包大小的庫相比, 更喜歡具有更清晰、更簡單的 API 和更小的包大小的庫。
使用 react 的內(nèi)置 hooks
useState, useMemo, useCallback, useContext 和 useRef hooks 在任何函數(shù)式組件中都是受歡迎的。在需要少量狀態(tài)或訪問 react 原語(如引用和上下文)的展示組件中,它們通常是一個(gè)不錯(cuò)的選擇。例如,具有滑出(slide-out)或可展開狀態(tài)(expandable state)的組件。
useEffect hook 更復(fù)雜,您需要小心地跟蹤您的依賴項(xiàng)并確保通過清理回調(diào)取消訂閱。應(yīng)避免 useEffect 的復(fù)雜鏈?zhǔn)綉?yīng)用程序,此時(shí) 'controller' 組件應(yīng)保持基于類(class)。
同樣,useReducer 鉤子與目前尚未確定的狀態(tài)管理重疊。我們希望避免 又一個(gè) 狀態(tài)管理模式,因此此時(shí)避免使用useReducer。
使用 context
當(dāng)我們計(jì)劃遠(yuǎn)離 Reflux 的路徑時(shí),useContext hook 提供了一個(gè)更簡單的實(shí)現(xiàn)選項(xiàng)來共享狀態(tài)和行為。當(dāng)您需要?jiǎng)?chuàng)建新的共享狀態(tài)源時(shí),請(qǐng)考慮使用 context 和 useContext 而不是 Reflux。此外,可以利用蟲洞狀態(tài)管理模式來公開共享狀態(tài)和突變函數(shù)。
- https://swizec.com/blog/wormhole-state-management
使用自定義 hooks
可以創(chuàng)建自定義 hooks 來共享應(yīng)用程序中的可重用邏輯。創(chuàng)建自定義 hook 時(shí),函數(shù)名稱必須遵循約定,以 “use” 開頭(例如 useTheme), 并且可以在自定義 hooks 內(nèi)調(diào)用其他 hooks。
注意 hooks 的規(guī)則和注意事項(xiàng)
React hooks 有一些規(guī)則。請(qǐng)注意 hooks 創(chuàng)建的規(guī)則和限制。我們使用 ESLint 規(guī)則來防止大多數(shù) hook 規(guī)則被非法侵入。
- https://reactjs.org/docs/hooks-rules.html
此外,我們建議您盡量少使用 useEffect。使用多個(gè) useEffect 回調(diào)表示您有一個(gè)高度有狀態(tài)的組件, 您應(yīng)該使用類(class)組件來代替。
我們的基礎(chǔ)視圖組件仍然是基于類的
我們的基礎(chǔ)視圖組件(AsyncView 和 AsyncComponent)是基于類的,并且會(huì)持續(xù)很長時(shí)間。在構(gòu)建視圖時(shí)請(qǐng)記住這一點(diǎn)。您將需要額外的 wrapper 組件來訪問 hooks 或?qū)?hook state 轉(zhuǎn)換為您的 AsyncComponent 的 props。
不要為 hooks 重寫
雖然 hooks 可以在新代碼中符合人體工程學(xué),但我們應(yīng)該避免重寫現(xiàn)有代碼以利用 hooks。重寫需要時(shí)間,使我們面臨風(fēng)險(xiǎn),并且為最終用戶提供的價(jià)值很小。
如果您需要重新設(shè)計(jì)一個(gè)組件以使用庫中的 hooks,那么還可以考慮從一個(gè)類轉(zhuǎn)換為一個(gè)函數(shù)組件。
使用 React Testing Library
我們正在將我們的測(cè)試從 Enzyme 轉(zhuǎn)換為 React Testing Library。在本指南中,您將找到遵循最佳實(shí)踐和避免常見陷阱的技巧。
我們有兩個(gè) ESLint 規(guī)則來幫助解決這個(gè)問題:
- 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
我們努力以一種與應(yīng)用程序使用方式非常相似的方式編寫測(cè)試。
我們不是處理渲染組件的實(shí)例,而是以與用戶相同的方式查詢 DOM。我們通過 label 文本找到表單元素(就像用戶一樣),我們從他們的文本中找到鏈接和按鈕(就像用戶一樣)。
作為此目標(biāo)的一部分,我們避免測(cè)試實(shí)現(xiàn)細(xì)節(jié),因此重構(gòu)(更改實(shí)現(xiàn)但不是功能)不會(huì)破壞測(cè)試。
我們通常贊成用例覆蓋而不是代碼覆蓋。
查詢
- 盡可能使用 getBy...
- 僅在檢查不存在時(shí)使用 queryBy...
- 僅當(dāng)期望元素在可能不會(huì)立即發(fā)生的 DOM 更改后出現(xiàn)時(shí)才使用 await findBy...
為確保測(cè)試類似于用戶與我們的代碼交互的方式,我們建議使用以下優(yōu)先級(jí)進(jìn)行查詢:
1.getByRole - 這應(yīng)該是幾乎所有東西的首選選擇器。
作為這個(gè)選擇器的一個(gè)很好的獎(jiǎng)勵(lì),我們確保我們的應(yīng)用程序是可訪問的。它很可能與 name 選項(xiàng) getByRole('button', {name: /save/i}) 一起使用。 name 通常是表單元素的 label 或 button 的文本內(nèi)容,或 aria-label 屬性的值。如果不確定,請(qǐng)使用 logRoles 功能 或查閱可用角色列表。
2.https://testing-library.com/docs/dom-testing-library/api-accessibility/#logroles
3.https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles
4.getByLabelText/getByPlaceholderText - 用戶使用 label 文本查找表單元素,因此在測(cè)試表單時(shí)首選此選項(xiàng)。
getByText - 在表單之外,文本內(nèi)容是用戶查找元素的主要方式。此方法可用于查找非交互式元素(如 div、span 和 paragraph)。
getByTestId - 因?yàn)檫@不反映用戶如何與應(yīng)用交互,所以只推薦用于不能使用任何其他選擇器的情況
如果您仍然無法決定使用哪個(gè)查詢, 請(qǐng)查看 testing-playground.com 以及 screen.logTestingPlaygroundURL() 及其瀏覽器擴(kuò)展。
- https://testing-playground.com/
不要忘記,你可以在測(cè)試中的任何地方放置 screen.debug() 來查看當(dāng)前的 DOM。
在官方文檔中閱讀有關(guān)查詢的更多信息。
- https://testing-library.com/docs/queries/about/
技巧
避免從 render 方法中解構(gòu)查詢函數(shù),而是使用 screen(examples)。當(dāng)您添加/刪除您需要的查詢時(shí),您不必使 render 調(diào)用解構(gòu)保持最新。您只需要輸入 screen 并讓您的編輯器的自動(dòng)完成功能處理其余的工作。
- 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... 變量將拋出更有用的錯(cuò)誤消息。
- 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 等待出現(xiàn),而是使用 findBy...(examples)。這兩個(gè)基本上是等價(jià)的(findBy... 甚至在其里面使用了 waitFor),但是 findBy... 更簡單,我們得到的錯(cuò)誤信息也會(huì)更好。
- 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)。使用這些推薦的斷言的優(yōu)點(diǎn)是更好的錯(cuò)誤消息、整體語義、一致性和統(tǒng)一性。
- 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();
按文本搜索時(shí),最好使用不區(qū)分大小寫的正則表達(dá)式。它將使測(cè)試更能適應(yīng)變化。
- 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 包,它構(gòu)建在 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 已經(jīng)被棄用一年多了,新項(xiàng)目是 reflexbox。為了升級(jí)到最新版本的 emotion,我們需要遷移出 grid-emotion。
要遷移,請(qǐng)使用 emotion 將導(dǎo)入的
組件
用下面的替換組件,然后刪除必要的 props 并移動(dòng)到 styled component。
<Flex>
- const Flex = styled('div')`
- display: flex;
- `;
<Box>
- const Box = styled('div')`
- `;
屬性
如果您正在修改導(dǎo)出的組件,請(qǐng)確保通過該組件的代碼庫進(jìn)行 grep 以確保它沒有被渲染為特定于 grid-emotion 的附加屬性。示例是
margin 和 padding
舊 (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; |
現(xiàn)在只需忽略 grid-emotion 的導(dǎo)入語句,例如 // eslint-disable-line no-restricted-imports