ReactLynx Testing Library
@lynx-js/react/testing-library 包适用于对 ReactLynx 组件的渲染结果进行测试。它提供了和 React Testing Library 相同的 API,例如 render、fireEvent、screen 等,其底层使用了 @lynx-js/testing-environment 包来提供 Lynx 环境的 JS 实现,屏蔽了 Lynx 双线程的实现细节。
配置
从 create-rspeedy 创建新项目
使用 create-rspeedy 创建的项目,在创建时可以主动选择是否使用 ReactLynx Testing Library(默认勾选),勾选后创建出来的项目已经配置好了 ReactLynx Testing Library。
在已有项目中配置
ReactLynx Testing Library 集成在 @lynx-js/react 包的 testing-library 子目录中,无需额外安装其他包。
配置 Vitest 时需要使用 @lynx-js/react/testing-library/vitest-config 中的 createVitestConfig 方法来创建 Vitest 配置。你可以通过 mergeConfig 方法将其和其他配置合并。
vitest.config.js
import { defineConfig, mergeConfig } from 'vitest/config';
import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config';
const defaultConfig = await createVitestConfig();
const config = defineConfig({
test: {
// ...
},
});
export default mergeConfig(defaultConfig, config);
示例
快速开始
和 React Testing Library 一样,我们推荐将测试用例分为安排、操作 和断言 三个部分。安排部分用于准备测试数据,操作部分用于执行测试操作,断言部分用于断言测试结果。下面是一个简单的示例:
import '@testing-library/jest-dom';
import { expect, it, vi } from 'vitest';
import { render, fireEvent, screen } from '@lynx-js/react/testing-library';
it('basic', async function () {
const Button = ({ children, onClick }) => {
return <view bindtap={onClick}>{children}</view>;
};
const onClick = vi.fn(() => {});
// 安排
const { container } = render(
<Button onClick={onClick}>
<text data-testid="text">Click me</text>
</Button>,
);
expect(onClick).not.toHaveBeenCalled();
// 操作
fireEvent.tap(container.firstChild);
// 断言
expect(onClick).toBeCalledTimes(1);
expect(screen.getByTestId('text')).toHaveTextContent('Click me');
});
在这个示例中,你可能已经注意到了我们用到了第三方包 @testing-library/jest-dom 中的 toHaveTextContent 方法来断言元素的文本内容。在 React Testing Library 中,你可以使用 @testing-library/jest-dom 是因为测试框架会使用 JSDOM 来创建 DOM 元素;在 ReactLynx Testing Library 中,我们同样使用了 JSDOM 来实现 Element PAPI 的行为,因此和 DOM API 完全兼容。
基础渲染
render 方法用于渲染一个 ReactLynx 组件,并返回一个 RenderResult 对象,其中的 container 字段是一个 LynxElement,表示渲染结果的根元素。
import '@testing-library/jest-dom';
import { expect, it } from 'vitest';
import { render } from '@lynx-js/react/testing-library';
it('basic render', () => {
const WrapperComponent = ({ children }) => (
<view data-testid="wrapper">{children}</view>
);
const Comp = () => {
return <view data-testid="inner" style="background-color: yellow;" />;
};
const { container, getByTestId } = render(<Comp />, {
wrapper: WrapperComponent,
});
expect(getByTestId('wrapper')).toBeInTheDocument();
expect(container.firstChild).toMatchInlineSnapshot(`
<view
data-testid="wrapper"
>
<view
data-testid="inner"
style="background-color: yellow;"
/>
</view>
`);
});
事件触发
在使用 fireEvent 触发事件时,需要显式指定事件的类型。例 如 new Event('catchEvent:tap')(eventType:eventName) 表示触发 catch 类型的 tap 事件,请参考事件处理器属性。eventType 的可能值和使用场景如下:
| 事件类型 | eventType | 事件绑定举例 | 事件触发举例 |
|---|
bind | bindEvent | bindtap | new Event('bindEvent:tap') |
catch | catchEvent | catchtap | new Event('catchEvent:tap') |
capture-bind | capture-bind | capture-bindtap | new Event('capture-bind:tap') |
capture-catch | capture-catch | capture-catchtap | new Event('capture-catch:tap') |
可以直接自己构造 Event 对象,也可以使用直接传入事件类型和初始化参数让 Testing Library 自动构造 Event 对象。
在 render 过程中,事件处理器会被挂载到 LynxElement 的 eventMap 属性上,因此可以通过 eventMap 属性来获取元素的事件处理器,用于断言事件处理器是否被正确挂载。
import { render, fireEvent } from '@lynx-js/react/testing-library';
import { vi, expect } from 'vitest';
it('fireEvent', async () => {
const handler = vi.fn();
const Comp = () => {
return <text catchtap={handler} />;
};
const {
container: { firstChild: button },
} = render(<Comp />);
expect(button).toMatchInlineSnapshot(`<text />`);
expect(button.eventMap).toMatchInlineSnapshot(`
{
"catchEvent:tap": [Function],
}
`);
expect(handler).toHaveBeenCalledTimes(0);
// 方式一:自己构造 Event 对象
const event = new Event('catchEvent:tap');
Object.assign(event, {
eventType: 'catchEvent',
eventName: 'tap',
key: 'value',
});
expect(fireEvent(button, event)).toBe(true);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(event);
expect(handler.mock.calls[0][0].type).toMatchInlineSnapshot(
`"catchEvent:tap"`,
);
expect(handler.mock.calls[0][0]).toMatchInlineSnapshot(`
Event {
"eventName": "tap",
"eventType": "catchEvent",
"isTrusted": false,
"key": "value",
}
`);
// 方式二:传入事件类型和初始化参数
fireEvent.tap(button, {
eventType: 'catchEvent',
key: 'value',
});
expect(handler).toHaveBeenCalledTimes(2);
expect(handler.mock.calls[1][0]).toMatchInlineSnapshot(`
Event {
"eventName": "tap",
"eventType": "catchEvent",
"isTrusted": false,
"key": "value",
}
`);
});
测试 Ref
在 ReactLynx Testing Library 中,可以对渲染结果和元素对应的 ref 对象进行快照测试来判断其是否被正确设置。
import { test, expect } from 'vitest';
import { render } from '@lynx-js/react/testing-library';
import { Component, createRef } from '@lynx-js/react';
it('element ref', async () => {
const ref = createRef();
const Comp = () => {
return <view ref={ref} />;
};
const { container } = render(<Comp />);
// ReactLynx 对于有 ref 的元素会设置 `has-react-ref` 属性
// 因此可以通过快照测试来判断 ref 是否被正确设置
expect(container).toMatchInlineSnapshot(`
<page>
<view
has-react-ref="true"
/>
</page>
`);
// ref.current 是一个 NodesRef 对象
expect(ref.current).toMatchInlineSnapshot(`
NodesRef {
"_nodeSelectToken": {
"identifier": "1",
"type": 2,
},
"_selectorQuery": {},
}
`);
});
it('component ref', async () => {
const ref1 = vi.fn();
const ref2 = createRef();
class Child extends Component {
x = 'x';
render() {
return <view />;
}
}
class Comp extends Component {
render() {
return (
this.props.show && (
<view>
<Child ref={ref1} />
<Child ref={ref2} />
</view>
)
);
}
}
const { container } = render(<Comp show />);
expect(container).toMatchInlineSnapshot(`
<page>
<view>
<view />
<view />
</view>
</page>
`);
expect(ref1).toBeCalledWith(
expect.objectContaining({
x: 'x',
}),
);
// ref2 指向的是 Child 组件实例
expect(ref2.current).toHaveProperty('x', 'x');
});
页面元素查询
你可以使用 screen 对象来查询页面元素,它提供了一些常用的方法,例如 getByText、getByTestId 等。还有像 waitForElementToBeRemoved 这样的方法对页面元素状态进行等待。
import '@testing-library/jest-dom';
import { Component } from '@lynx-js/react';
import { expect } from 'vitest';
// waitForElementToBeRemoved 是 @testing-library/dom 中的一个方法,用于等待元素被移除,这里被重新导出了
import {
render,
screen,
waitForElementToBeRemoved,
} from '@lynx-js/react/testing-library';
const fetchAMessage = () =>
new Promise((resolve) => {
// 我们使用随机超时来模拟一个真实的例子
const randomTimeout = Math.floor(Math.random() * 100);
setTimeout(() => {
resolve({ returnedMessage: 'Hello World' });
}, randomTimeout);
});
class ComponentWithLoader extends Component {
state = { loading: true };
componentDidMount() {
fetchAMessage().then((data) => {
this.setState({ data, loading: false });
});
}
render() {
if (this.state.loading) {
return <text>Loading...</text>;
}
return (
<text data-testid="message">
Loaded this message: {this.state.data.returnedMessage}!
</text>
);
}
}
test('it waits for the data to be loaded', async () => {
render(<ComponentWithLoader />);
// elementTree.root 用于维护页面元素树
expect(elementTree.root).toMatchInlineSnapshot(`
<page>
<text>
Loading...
</text>
</page>
`);
const loading = () => {
return screen.getByText('Loading...');
};
await waitForElementToBeRemoved(loading);
// 由于底层使用的是 JSDOM 来实现 Element PAPI
// 因此可以直接访问 document.body 来获取页面元素
expect(document.body).toMatchInlineSnapshot(`
<body>
<page>
<text
data-testid="message"
>
Loaded this message:
<wrapper>
Hello World
</wrapper>
!
</text>
</page>
</body>
`);
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/);
expect(elementTree.root).toMatchInlineSnapshot(`
<page>
<text
data-testid="message"
>
Loaded this message:
<wrapper>
Hello World
</wrapper>
!
</text>
</page>
`);
});