发送事件
注意
大多数项目都有几种使用
fireEvent
的情况,但大部分时间你应该使用@testing-library/user-event。
fireEvent
fireEvent(node: HTMLElement, event: Event)
触发 DOM 事件。
// <button>Submit</button>
fireEvent(
getByText(container, 'Submit'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
)
fireEvent[eventName]
fireEvent[eventName](node: HTMLElement, eventProperties: Object)
用于触发 DOM 事件的便捷方法。可以在 src/event-map.js 中查看完整的事件列表以及默认的 eventProperties
。
target:当在元素上派发事件时,事件在名为 target
的属性上具有该元素。为了方便起见,如果在 eventProperties
(第二个参数)中提供了 target
属性,则这些属性将被分配给接收事件的节点。
这对于触发 change 事件特别有用:
fireEvent.change(getByLabelText(/username/i), {target: {value: 'a'}})
// 注意:试图手动设置 HTMLInputElement 的 files 属性会导致错误,因为 files 属性是只读的。
// 这个特性通过使用 Object.defineProperty 来解决这个问题。
fireEvent.change(getByLabelText(/picture/i), {
target: {
files: [new File(['(⌐□_□)'], 'chucknorris.png', {type: 'image/png'})],
},
})
// 注意:当在类型为 "date" 的输入框上触发 change 事件时,'value' 属性必须使用 ISO 8601 格式,否则元素不会反映更改后的值。
// 无效:
fireEvent.change(input, {target: {value: '24/05/2020'}})
// 有效:
fireEvent.change(input, {target: {value: '2020-05-24'}})
dataTransfer:拖放事件具有一个名为 dataTransfer
的属性,其中包含在操作期间传输的数据。为了方便起见,如果在 eventProperties
(第二个参数)中提供了 dataTransfer
属性,则这些属性将被添加到事件中。
这主要用于测试拖放交互。
fireEvent.drop(getByLabelText(/drop files here/i), {
dataTransfer: {
files: [new File(['(⌐□_□)'], 'chucknorris.png', {type: 'image/png'})],
},
})
键盘事件:与键盘输入相关的事件类型有三种 - keyPress
、keyDown
和 keyUp
。当触发这些事件时,你需要引用 DOM 中的一个元素以及要触发的键。
fireEvent.keyDown(domNode, {key: 'Enter', code: 'Enter', charCode: 13})
fireEvent.keyDown(domNode, {key: 'A', code: 'KeyA'})
你可以在https://www.toptal.com/developers/keycode找到要使用的键码。
createEvent[eventName]
createEvent[eventName](node: HTMLElement, eventProperties: Object)
用于创建可以由 fireEvent
触发的 DOM 事件的便捷方法,允许你引用创建的事件:如果你需要访问无法通过编程方式初始化的事件属性(例如 timeStamp
),这可能会很有用。
const myEvent = createEvent.click(node, {button: 2})
fireEvent(node, myEvent)
// 可以像访问其他事件属性一样访问 myEvent.timeStamp
// 注意:通过 `createEvent` 创建的事件访问基于原生事件 API,
// 因此,应该使用 Object.defineProperty 来设置 HTMLEvent 对象的原生属性(例如 `timeStamp`、`cancelable`、`type`)。
// 更多信息请参见:https://developer.mozilla.org/en-US/docs/Web/API/Event
你还可以创建通用事件:
// 模拟文件输入上的 'input' 事件
fireEvent(
input,
createEvent('input', input, {
target: {files: inputFiles},
...init,
}),
)
使用 Jest 函数模拟
Jest 的模拟函数可以用于测试组件是否会在特定事件响应中调用其绑定的回调函数。
import {render, screen, fireEvent} from '@testing-library/react'
const Button = ({onClick, children}) => (
<button onClick={onClick}>{children}</button>
)
test('点击时调用 onClick 属性', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click Me</Button>)
fireEvent.click(screen.getByText(/click me/i))
expect(handleClick).toHaveBeenCalledTimes(1)
}
异步方法
在测试中,提供了几个处理异步代码的实用工具。这些工具对于等待元素出现或消失以响应事件、用户操作、超时或 Promise 很有用。使用这些工具时,异步方法返回的是 Promise,因此在调用它们时要确保使用 await
或 .then
处理异步行为。
以下是一些常用的异步方法:
findBy 查询
findBy
方法是 getBy
查询 和 waitFor
的结合。它们接受 waitFor
选项作为最后一个参数(例如 await screen.findByText('text', queryOptions, waitForOptions)
)。
findBy
查询适用于当你期望一个元素出现,但对 DOM 的更改可能不会立即发生的情况。
const button = screen.getByRole('button', {name: 'Click Me'})
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')
waitFor
function waitFor<T>(
callback: () => T | Promise<T>,
options?: {
container?: HTMLElement
timeout?: number
interval?: number
onTimeout?: (error: Error) => Error
mutationObserverOptions?: MutationObserverInit
},
): Promise<T>
当需要等待一段时间时,你可以使用 waitFor
,等待满足你的预期条件。返回一个 falsy 条件不足以触发重试,回调函数必须抛出错误才能重试条件。以下是一个简单的示例:
// ...
// 等待直到回调函数不抛出错误。在此示例中,它将等待模拟函数被调用一次。
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
// ...
waitFor
可能会多次运行回调函数,直到达到超时时间为止。请注意,调用次数受限于 timeout
和 interval
选项。
如果在 waitFor
回调函数中返回一个 Promise(显式或隐式使用 async
语法),则 waitFor
实用程序在该 Promise 拒绝之前不会再次调用回调函数。这允许你 waitFor
异步检查的内容。
默认的 container
是全局的 document
。确保你等待的元素是 container
的后代元素。
默认的 interval
是 50ms
。然而,它会在开始间隔之前立即运行回调函数。
默认的 timeout
是 1000ms
。
默认的 onTimeout
接收错误并将 container
的打印状态附加到错误消息中,这应该能够更容易地找出导致超时的原因。
默认的 mutationObserverOptions
是 {subtree: true, childList: true, attributes: true, characterData: true}
,它会检测 container
及其后代中的子元素(包括文本节点)的添加和删除。它还检测属性更改。当发生任何这些更改时,它会重新运行回调函数。
waitForElementToBeRemoved
function waitForElementToBeRemoved<T>(
callback: (() => T) | T,
options?: {
container?: HTMLElement
timeout?: number
interval?: number
onTimeout?: (error: Error) => Error
mutationObserverOptions?: MutationObserverInit
},
): Promise<void>
要等待从 DOM 中删除元素,你可以使用 waitForElementToBeRemoved
。waitForElementToBeRemoved
函数是对 waitFor
实用程序的简单包装。
第一个参数必须是一个元素、元素数组或返回元素、元素数组的回调函数。
以下是一个示例,其中的 Promise 在元素被删除时解析:
const el = document.querySelector('div.getOuttaHere')
waitForElementToBeRemoved(document.querySelector('div.getOuttaHere')).then(() =>
console.log('Element no longer in DOM'),
)
el.setAttribute('data-neat', true)
// 其他突变被忽略...
el.parentElement.removeChild(el)
// 输出 'Element no longer in DOM'
如果第一个参数为 null
或空数组,waitForElementToBeRemoved
会抛出错误:
waitForElementToBeRemoved(null).catch(err => console.log(err))
waitForElementToBeRemoved(queryByText(/not here/i)).catch(err =>
console.log(err),
)
waitForElementToBeRemoved(queryAllByText(/not here/i)).catch(err =>
console.log(err),
)
waitForElementToBeRemoved(() => getByText(/not here/i)).catch(err =>
console.log(err),
)
// 错误:传递给 waitForElementToBeRemoved 的元素已经被移除。在等待移除之前,waitForElementToBeRemoved 要求元素存在。
选项对象会传递给 waitFor
。
出现消失
有时候,你需要测试一个元素是否存在,然后消失,或者相反。
等待元素出现
如果你需要等待一个元素出现,异步等待工具允许你在继续之前等待一个断言得到满足。等待工具会重试直到查询通过或超时。异步方法返回一个 Promise,因此在调用它们时必须始终使用 await
或 .then(done)
。
- 使用
findBy
查询
test('电影标题出现', async () => {
// 元素初始状态下不存在...
// 等待元素出现并返回该元素
const movie = await findByText('狮子王')
})
- 使用
waitFor
test('电影标题出现', async () => {
// 元素初始状态下不存在...
// 在断言中等待元素出现
await waitFor(() => {
expect(getByText('狮子王')).toBeInTheDocument()
})
})
等待元素消失
waitForElementToBeRemoved
异步助手函数 使用回调函数在每次 DOM 变化时查询元素,并在元素被移除时返回 true
。
test('电影标题在 DOM 中不再存在', async () => {
// 元素已被移除
await waitForElementToBeRemoved(() => queryByText('木乃伊'))
})
使用 MutationObserver
比使用 waitFor
在固定时间间隔内轮询 DOM 更高效。
waitFor
异步助手函数 会重试,直到包装函数不再抛出错误。这可用于断言一个元素从页面中消失。
test('电影标题消失了', async () => {
// 元素初始状态下存在...
// 注意使用 queryBy 而不是 getBy,这样在查询时返回 null 而不是抛出异常
await waitFor(() => {
expect(queryByText('机器人总动员')).not.toBeInTheDocument()
})
})
断言元素不存在
标准的 getBy
方法在找不到元素时会抛出错误,所以如果你想断言一个元素在 DOM 中 不存在,你可以使用 queryBy
API:
const submitButton = screen.queryByText('提交')
expect(submitButton).toBeNull() // 它不存在
queryAll
版本的 API 返回匹配节点的数组。数组的长度在元素被添加或从 DOM 中删除后进行断言时很有用。
const submitButtons = screen.queryAllByText('提交')
expect(submitButtons).toHaveLength(0) // 没有元素
not.toBeInTheDocument
jest-dom
实用工具库提供了 .toBeInTheDocument()
匹配器,它可用于断言一个元素是否在文档的 body 中存在或不存在。。这比断言查询结果为 null 更有意义。
import '@testing-library/jest-dom'
// 使用 `queryBy` 避免使用 `getBy` 时抛出错误
const submitButton = screen.queryByText('提交')
expect(submitButton).not.toBeInTheDocument()
fireEvent 的注意事项
交互 vs.事件
根据指导原则,你的测试应尽可能地模拟用户与你的代码(组件、页面等)的交互方式。在这方面,你应该知道 fireEvent
并不完全等同于用户与应用程序的实际交互方式,但对于大多数场景来说,它已经足够接近了。
以 fireEvent.click
为例,它会创建一个点击事件并在指定的 DOM 节点上触发该事件。这在大多数情况下适用,当你只是想测试在元素被点击时会发生什么时。但是,当用户实际点击你的元素时,通常会依次触发以下事件:
- fireEvent.mouseOver(element)
- fireEvent.mouseMove(element)
- fireEvent.mouseDown(element)
- element.focus()(如果该元素可获取焦点)
- fireEvent.mouseUp(element)
- fireEvent.click(element)
如果该元素恰好是 label
元素的子元素,则还会将焦点移动到 label
标记的表单控件上。因此,即使你实际上只是想测试点击处理程序,但只使用 fireEvent.click
就会忽略用户在此过程中触发的其他几个潜在重要的事件。
同样,大多数情况下,这对于测试来说并不是关键,使用 fireEvent.click
这种简便方式是值得的。
其他选择
我们将介绍一些简单的调整方法,可以增加你对组件交互行为的信心。对于其他交互,你可能还可以考虑使用 user-event 或在实际环境中测试组件(例如,手动测试、使用 Cypress 自动化测试等)。
Keydown
当前获取焦点的元素、body 元素或文档元素会触发 keydown 事件。根据这个原则,你应该优先使用以下方式:
- fireEvent.keyDown(getByText('click me'));
+ getByText('click me').focus();
+ fireEvent.keyDown(document.activeElement || document.body);
这样还可以测试所涉及的元素是否可以接收键盘事件。
Focus/Blur
如果一个元素获取焦点,会触发 focus 事件,文档中的活动元素会发生变化,并且之前获取焦点的元素会失去焦点。要模拟这种行为,你只需用直接的方式设置焦点,而不使用 fireEvent
:
- fireEvent.focus(getByText('focus me'));
+ getByText('focus me').focus();
这种方法的一个好处是,如果元素无法获取焦点,则对触发的 focus 事件进行的任何断言都会失败。这在你随后使用 keydown 事件时尤其重要。
使用 Fake Timers
在某些情况下,当你的代码使用定时器(setTimeout
、setInterval
、clearTimeout
、clearInterval
)时,你的测试可能会变得不可预测、缓慢和不稳定。
为了解决这些问题,或者如果你的代码需要依赖特定的时间戳,大多数测试框架都提供了用假定时器替换真实定时器的选项。这应该只偶尔使用,而不是经常使用,因为使用假定时器会带来一些开销。
在测试中使用假定时器时,测试中的所有代码都使用假定时器。
设置假定时器的常见模式通常在 beforeEach
中进行,例如:
// 使用 Jest 设置假定时器
beforeEach(() => {
jest.useFakeTimers()
})
在使用假定时器时,你需要记得在测试运行结束后恢复真实定时器。
这样做的主要原因是防止第三方库在你的测试完成后(例如清理函数)仍然依赖于你的假定时器并使用真实定时器。
通常你会在 afterEach
中调用 useRealTimers
来实现这一点。
在切换到真实定时器之前,还重要的一点是调用 runOnlyPendingTimers
。这样做可以确保在切换到真实定时器之前刷新所有挂起的定时器。如果你不推进定时器的进度,只是切换到真实定时器,那么计划的任务将不会被执行,导致意外行为。这对于在你不知情的情况下调度任务的第三方库来说尤为重要。
以下是使用 Jest 实现上述操作的示例:
// 运行所有挂起的定时器并切换到真实定时器,使用 Jest
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
评论 (0)