### `waitFor` 超时设置 `waitFor` 函数的默认超时时间设定为 1000 毫秒。目前该值足以满足我们测试的所有渲染逻辑,但若在测试过程中发现某个元素需要更长的渲染时间,则应适当增加该设定值。 ### 替换现有 UI 单元测试 部分组件已具备覆盖组件渲染的单元测试。虽然并非强制要求,但在这些情况下,建议评估将其迁移为集成测试的可能性。 若需同时保留两种测试类型,我们将在集成测试文件名中加入"integration"字样以避免命名冲突,具体示例可参考:[packages/block-library/src/missing/test/edit-integration.native.js](https://github.com/WordPress/gutenberg/blob/9201906891a68ca305daf7f8b6cd006e2b26291e/packages/block-library/src/missing/test/edit-integration.native.js)。 ### 平台选择配置 默认情况下,Jest 中的所有测试均在 Android 平台环境下运行。如需测试特定平台的相关行为,则需要支持多平台测试文件。 若仅需测试由 Platform 对象控制的逻辑,可通过以下代码模拟模块(本例将平台切换为 iOS): ```js jest.mock( 'Platform', () => { const Platform = jest.requireActual( 'Platform' ); Platform.OS = 'ios'; Platform.select = jest.fn().mockImplementation( ( select ) => { const value = select[ Platform.OS ]; return ! value ? select.default : value; } ); return Platform; } ); ``` ### 打开区块设置 点击"打开设置"按钮即可访问区块设置,以下是一个示例: ```js fireEvent.press( block ); const settingsButton = await findByLabelText( '打开设置' ); fireEvent.press( settingsButton ); ``` #### 使用作用域组件方法 当采用作用域组件方法时,我们需要先渲染 `SlotFillProvider` 和 `BottomSheetSettings`(注意我们通过传递 `isVisible` 属性强制显示底部面板)以及区块: ```js ``` 参考示例: - [封面区块](https://github.com/WordPress/gutenberg/blob/b403b977b029911f46247012fa2dcbc42a5aa3cf/packages/block-library/src/cover/test/edit.native.js#L37-L42) ### FlatList 项 `FlatList` 组件根据滚动位置、视图和内容尺寸来渲染其项。这意味着在渲染此组件时,某些项可能因尚未渲染而无法被查询到。为解决此问题,我们需要显式触发事件使 `FlatList` 渲染所有项。 以下是插入器菜单中用于渲染区块列表的 FlatList 示例: ```js const blockList = getByTestId( '插入器界面-区块' ); // 通过 onScroll 事件强制 FlatList 渲染所有项 fireEvent.scroll( blockList, { nativeEvent: { contentOffset: { y: 0, x: 0 }, contentSize: { width: 100, height: 100 }, layoutMeasurement: { width: 100, height: 100 }, }, } ); ``` ### 滑块控件 在底部面板中的滑块应使用其 `testID` 进行查询: ```js const radiusSlider = await findByTestId( '滑块 边框圆角' ); fireEvent( radiusSlider, 'valueChange', '30' ); ``` 注意滑块的 `testID` 是"滑块 " + 标签文本。因此对于标签为"边框圆角"的滑块,其 `testID` 即为"滑块 边框圆角"。 ### 选择内部区块 添加区块时需注意:如果区块包含内部区块,这些内部区块默认不会渲染。以下示例展示如何让按钮区块渲染其内部的按钮区块(假设已获得按钮区块的引用 `buttonsBlock`): ```js const innerBlockListWrapper = await within( buttonsBlock ).findByTestId( '区块列表包装器' ); fireEvent( innerBlockListWrapper, 'layout', { nativeEvent: { layout: { width: 100, }, }, } ); const buttonInnerBlock = await within( buttonsBlock ).findByLabelText( /按钮区块\. 第1行/ ); fireEvent.press( buttonInnerBlock ); ``` ## 工具 ### 使用无障碍检查器 如果难以定位元素的标识符,建议使用 Xcode 的无障碍检查器。大多数标识符是跨平台的,因此即使测试默认在 Android 上运行,仍可通过无障碍检查器查找正确的标识符。 Xcode无障碍检查器应用截图。截图展示了如何在设备下拉菜单中选择正确目标、启用目标模式,以及点击屏幕元素后定位无障碍标签的方法 ## 常见陷阱与注意事项 ### 省略 `waitFor` 前的 `await` 会导致误判 在 `waitFor` 前省略 `await` 可能导致测试通过但未验证预期行为的情况。例如,若使用 `toBeDefined` 来断言 `waitFor` 的调用结果,由于 `waitFor` 始终会返回值,断言将通过——即使该值并非我们想要检查的 `ReactTestInstance`。因此建议使用自定义匹配器 `toBeVisible`,可有效防范此类误判情况。 ### 使用 `find` 类查询 组件渲染或事件触发后,可能因状态更新产生副作用,导致目标元素尚未渲染。此时需要等待元素可用,为此可使用查询函数的 `find*` 版本,这些函数内部采用 `waitFor` 机制周期性检测元素是否出现。 示例如下: ```js const mediaLibraryButton = await findByText( 'WordPress媒体库' ); ``` ```js const missingBlock = await findByLabelText( /不支持的块\. 第1行/ ); ``` ```js const radiusSlider = await findByTestId( '滑块圆角半径' ); ``` 多数情况下我们会使用 `find*` 函数,但需注意应仅限于真正需要等待元素出现的查询场景。 ### `within` 查询 通过 `within` 函数可查询其他元素内部包含的元素,示例如下: ```js const missingBlock = await findByLabelText( /不支持的块\. 第1行/ ); const translatedTableTitle = within( missingBlock ).getByText( '表格' ); ``` ## 触发事件 除了查询元素,触发事件模拟用户交互同样重要。为此可使用 `fireEvent` 函数([文档](https://callstack.github.io/react-native-testing-library/docs/api#fireevent))。 点击事件示例: **点击事件:** ```js fireEvent.press( settingsButton ); ``` 我们也可以触发任意类型事件(包括自定义事件)。以下示例展示如何触发滑块组件的 `onValueChange` 事件([代码参考](https://github.com/WordPress/gutenberg/blob/520cbd9d2af4bbc275d388edf92a6cadb685de56/packages/components/src/mobile/bottom-sheet/range-cell.native.js#L227)): **自定义事件 – onValueChange:** ```js fireEvent( heightSlider, 'valueChange', '50' ); ``` ## 验证元素行为 完成元素查询和事件触发后,需验证逻辑是否符合预期。可使用与单元测试相同的 Jest `expect` 函数,推荐使用自定义匹配器 `toBeVisible` 来确保元素已定义、属于有效React元素且可见。 示例如下: ```js const translatedTableTitle = within( missingBlock ).getByText( '表格' ); expect( translatedTableTitle ).toBeVisible(); ``` 当渲染完整编辑器时,还可验证HTML输出是否符合预期: ```js expect( getEditorHtml() ).toBe( '\n\n' ); ``` ## 清理操作 最后需要清理可能影响后续测试的修改。以下是注册区块后的典型清理示例(需取消注册所有区块): ```js afterAll( () => { // 清理已注册区块 getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); } ); ``` ## 辅助工具 为简化原生版本集成测试的编写,可在[此README文件](https://github.com/WordPress/gutenberg/blob/HEAD/test/native/integration-test-helpers/README.md)中查看辅助函数列表。 ## 常见流程 ### 查询区块 通过无障碍访问标签查询区块是常见方式,示例如下: ```js const spacerBlock = await waitFor( () => getByLabelText( /间距区块\. 第1行/ ) ); ``` 关于区块无障碍访问标签的更多信息,可查阅 [`getAccessibleBlockLabel` 函数](https://github.com/WordPress/gutenberg/blob/520cbd9d2af4bbc275d388edf92a6cadb685de56/packages/blocks/src/api/utils.js#L167-L234)的代码实现。 ### 添加区块 以下是插入段落区块的示例: ```js // 打开插入器菜单 fireEvent.press( await findByLabelText( '添加区块' ) ); const blockList = getByTestId( 'InserterUI-区块列表' ); // 通过onScroll事件强制FlatList渲染所有项 fireEvent.scroll( blockList, { nativeEvent: { contentOffset: { y: 0, x: 0 }, contentSize: { width: 100, height: 100 }, layoutMeasurement: { width: 100, height: 100 }, }, } ); // 插入段落区块 fireEvent.press( await findByText( `段落` ) ); ``` # React Native 集成测试指南 ## 什么是集成测试? 集成测试被定义为将不同部分作为整体进行测试的一种测试类型。在我们的场景中,需要测试的部件是指为特定区块或编辑器逻辑所需渲染的不同组件。最终它们与单元测试非常相似,因为它们都是使用 Jest 库通过相同命令运行的。主要区别在于,对于集成测试,我们将使用特定库 [`react-native-testing-library`](https://testing-library.com/docs/react-native-testing-library/intro/) 来测试编辑器如何渲染不同组件。 ## 集成测试的结构 测试可以包含以下部分: - [设置](#设置) - [渲染](#渲染) - [查询元素](#查询元素) - [触发事件](#触发事件) - [验证元素行为](#验证元素行为) - [清理](#清理) 我们还在后续章节中提供了常见任务示例和技巧: - [辅助工具](#辅助工具) - [常见流程](#常见流程) - [工具](#工具) - [常见陷阱与注意事项](#常见陷阱与注意事项) ## 设置 这部分通常通过使用 Jest 回调函数 `beforeAll` 和 `beforeEach` 来完成,目的是准备测试可能需要的所有内容,比如注册区块或模拟部分逻辑。 以下是一个预期所有核心区块可用时的常见模式示例: ```js beforeAll( () => { // 注册所有核心区块 registerCoreBlocks(); } ); ``` ## 渲染 在引入测试逻辑之前,我们需要先渲染要测试的组件。根据是否使用作用域组件方法或完整编辑器方法,这部分会有所不同。 ### 使用作用域组件方法 以下是渲染封面区块的示例(摘自[此代码](https://github.com/WordPress/gutenberg/blob/86cd187873984f80ddeeec3e82454b486dd1860f/packages/block-library/src/cover/test/edit.native.js#L82-L91)): ```js // 此导入指向区块的索引文件 import { metadata, settings, name } from '../index'; ... const setAttributes = jest.fn(); const attributes = { backgroundType: IMAGE_BACKGROUND_TYPE, focalPoint: { x: '0.25', y: '0.75' }, hasParallax: false, overlayColor: { color: '#000000' }, url: 'mock-url', }; ... // 在插槽内渲染封面编辑器的简化树结构 const CoverEdit = ( props ) => ( ); const { getByText, findByText } = render( ); ``` ### 使用完整编辑器方法 以下是渲染按钮区块的示例(摘自[此代码](https://github.com/WordPress/gutenberg/blob/9201906891a68ca305daf7f8b6cd006e2b26291e/packages/block-library/src/buttons/test/edit.native.js#L32-L39)): ```js const initialHtml = `
`; const { getByLabelText } = initializeEditor( { initialHtml, } ); ``` ## 查询元素 组件渲染完成后,就可以进行元素查询。关于这个主题的一个重要注意事项是:我们应该从用户角度进行测试,这意味着理想情况下应该通过用户可访问的文本或无障碍标签来查询元素。 查询时应遵循以下优先级顺序: 1. `getByText`:通过文本查询是最接近用户视角的操作流程,因为文本是用户识别元素的视觉线索。 2. `getByLabelText`:在某些情况下,我们需要查询不提供文本的元素,这时可以回退到使用无障碍标签。 3. `getByTestId`:如果前面的选项都不适用,并且没有任何可依赖的视觉元素,就必须回退到特定的测试ID,这可以通过 `testID` 属性来定义(参见[此示例](https://github.com/WordPress/gutenberg/blob/e5b387b19ffc50555f52ea5f0b415ab846896def/packages/block-editor/src/components/block-types-list/index.native.js#L80))。 以下是一些示例: ```js const mediaLibraryButton = getByText( 'WordPress媒体库' ); ``` ```js const missingBlock = getByLabelText( /不支持的区块\. 第1行/ ); ``` ```js const radiusSlider = getByTestId( '圆角滑块' ); ``` 请注意,这些查询可以传入纯字符串或正则表达式。正则表达式最适合查询部分字符串(例如,任何包含"不支持的区块. 第1行"的无障碍标签元素)。注意特殊字符如 `.` 需要进行转义。