React Testing Library测试Hook中脚本onload回调未触发问题
问题:动态插入script标签的onload/onerror回调在Jest测试中不触发
问题描述
开发支持Google身份服务的React库时,因官方登录脚本无npm安装包,需通过动态插入<script>标签的方式加载资源。在React Hook中实现了向文档头部插入script标签、绑定onload/onerror回调的逻辑,但测试时两个回调均未触发,已通过日志确认脚本插入函数完整执行。
测试输出中提示Ignored nodes: comments, <script />, <style />,怀疑和节点过滤配置有关,但未找到修改配置的明确方法。
相关代码
脚本插入函数
const loadGapiScript = (document, src, onLoad, onError) => { const script = document.getElementsByTagName('script')[0] let js = document.createElement('script') js.id = 'google-auth' js.src = src js.async = true js.defer = true js.onerror = onError js.onload = onLoad if (script && script.parentNode) { script.parentNode.insertBefore(js, script) } else { document.head.appendChild(js) } }
测试代码
import { renderHook, waitFor } from '@testing-library/react' import useGoogleLogin from './use-google-login' describe('useGoogleLogin', () => { const clientId = 'somestring' const onLoad = jest.fn(() => {}) it('calls the onLoad callback', async () => { renderHook(() => useGoogleLogin({ clientId, onLoad })) await waitFor(() => expect(onLoad).toHaveBeenCalled()) }) })
测试报错输出
expect(jest.fn()).toHaveBeenCalled() Expected number of calls: >= 1 Received number of calls: 0 Ignored nodes: comments, <script />, <style /> <html> <head /> <body> <div /> </body> </html> 30 | renderHook(() => useGoogleLogin({ clientId, onLoad })) 31 | > 32 | await waitFor(() => { expect(onLoad).toHaveBeenCalled() }) | ^ 33 | }) 34 | }) 35 | }) at toHaveBeenCalled (__tests__/use-google-login-test.js:32:55) at runWithExpensiveErrorDiagnosticsDisabled (node_modules/@testing-library/dom/dist/config.js:50:12) at checkCallback (node_modules/@testing-library/dom/dist/wait-for.js:141:77) at checkRealTimersCallback (node_modules/@testing-library/dom/dist/wait-for.js:133:16) at Timeout.task [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:514:19)
问题根因
测试输出中的Ignored nodes提示不是问题原因:这只是Testing Library打印错误诊断信息时,默认过滤脚本、样式、注释节点减少冗余输出的行为,和回调不触发没有任何关系,不需要修改这部分配置。
真正的原因是Jest默认使用的jsdom环境,默认不会实际加载动态插入script的远程src资源:jsdom只会把script节点插入DOM树,不会发起真实网络请求拉取脚本内容,自然不会触发onload或onerror回调,这是jsdom的默认设计,和业务代码逻辑无关。
解决方案
单元测试的核心是验证自身逻辑正确性,不需要真实请求Google的服务,直接手动模拟脚本加载结果即可:
- 渲染Hook后,通过id获取到插入的script节点
- 手动派发
load或error事件,模拟脚本加载成功/失败的场景 - 验证对应回调是否被调用
修改后的测试用例示例:
describe('useGoogleLogin', () => { const clientId = 'somestring' const onLoad = jest.fn(() => {}) it('calls the onLoad callback when script loads successfully', async () => { renderHook(() => useGoogleLogin({ clientId, onLoad })) // 获取插入的脚本节点 const gapiScript = document.getElementById('google-auth') // 手动触发load事件模拟加载成功 gapiScript.dispatchEvent(new Event('load')) await waitFor(() => expect(onLoad).toHaveBeenCalled()) }) it('calls the onError callback when script fails to load', async () => { const onError = jest.fn() renderHook(() => useGoogleLogin({ clientId, onLoad, onError })) const gapiScript = document.getElementById('google-auth') // 手动触发error事件模拟加载失败 gapiScript.dispatchEvent(new Event('error')) await waitFor(() => expect(onError).toHaveBeenCalled()) }) })
如果是E2E测试需要真实加载脚本,可以在jsdom配置中开启resources: 'usable'选项允许加载外部资源,但这种方式依赖外部服务可用性、测试速度慢,完全不适合单元测试场景。
内容的提问来源于stack exchange,提问作者Dana Scheider
相关产品推荐
相关产品推荐

