Enzyme `getWrappingComponent()` 深入解析:通过 wrappingComponent 操控 Provider 与上下文
2026/9/20 21:10:28 网站建设 项目流程
  • 测试
  • 前端

【免费下载链接】enzyme

JavaScript Testing utilities for React

项目地址:https://gitcode.com/gh_mirrors/en/enzyme
点击查看免费下载

导读

getWrappingComponent()是 Enzyme 中与wrappingComponent选项配套的核心方法:当你在mount()shallow()时传入wrappingComponent选项,Enzyme 会额外渲染一层“包裹组件”作为被测组件的父级,而getWrappingComponent()则返回这层包裹组件对应的ReactWrapper/ShallowWrapper,让你可以在测试中动态更新它的 props、state,从而操控注入到被测组件中的 Context、Store 等外部依赖。读完本文,你将掌握该方法的完整用法、底层实现原理、联动更新机制以及各种边界条件。

本文以 docs/api/ReactWrapper/getWrappingComponent.md 为骨架,结合 ReactWrapper.js、ShallowWrapper.js 源码与 getWrappingComponent.jsx 测试用例展开。

方法签名与返回值

.getwrappingComponent() => ReactWrapper // 来自 mount() .getwrappingComponent() => ShallowWrapper // 来自 shallow()

返回值:一个包裹着已渲染wrappingComponentReactWrappermount场景)或ShallowWrappershallow场景)。这个返回值可以被用来更新wrappingComponent的 props、state 等——例如通过setProps()替换一个 mock Store。

注意:getWrappingComponent()返回的并不是wrappingComponent自身类,而是它渲染出的组件树;在mount场景下其.type()等于wrappingComponent本身,在shallow场景下则指向它渲染出的最外层元素(详见下文“源码原理”)。

使用前提:wrappingComponentwrappingComponentProps选项

getWrappingComponent()只在构造时传入了wrappingComponent选项的前提下才有意义。根据 mount.md 与 shallow.md 的说明,两个入口共享以下选项:

  • options.wrappingComponentComponentType,可选):一个将渲染为node父级的组件,可用于为被测组件提供 context、Store 等。注意wrappingComponent必须渲染它的 children,否则 Enzyme 会抛错。
  • options.wrappingComponentPropsObject,可选):如果指定了wrappingComponent,则作为传入它的初始 props。

getWrappingComponent()的典型工作流是:

  1. mount()/shallow()传入wrappingComponent(如 Redux 的Provider、React Router 的Router);
  2. 调用wrapper.getWrappingComponent()拿到包裹组件的 wrapper;
  3. setProps()/setState()等 API 动态更新包裹组件,进而影响被测组件读取的 context/store。

官方示例:动态切换 Redux Store

下面是原文档的完整示例,它展示了一个常见场景——测试 React-Redux 组件时,通过getWrappingComponent()将真实的 Store 换成 mock Store:

import { Provider } from 'react-redux'; import { Router } from 'react-router'; import store from './my/app/store'; import mockStore from './my/app/mockStore'; function MyProvider(props) { const { children, customStore } = props; return ( <Provider store={customStore || store}> <Router> {children} </Router> </Provider> ); } MyProvider.propTypes = { children: PropTypes.node, customStore: PropTypes.shape({}), }; MyProvider.defaultProps = { children: null, customStore: null, }; // mount 场景 const wrapper = mount(<MyComponent />, { wrappingComponent: MyProvider, }); const provider = wrapper.getWrappingComponent(); provider.setProps({ customStore: mockStore }); // shallow 场景完全一致,只需把 mount 换成 shallow // const wrapper = shallow(<MyComponent />, { // wrappingComponent: MyProvider, // }); // const provider = wrapper.getWrappingComponent(); // provider.setProps({ customStore: mockStore });

关键点在于MyProvider必须渲染childrenProviderRouter包裹着{children}),因为wrappingComponent只有渲染出 children,被测的MyComponent才能出现在组件树中,getWrappingComponent()也才有意义。shallow()场景下的完整用法可参见 ShallowWrapper/getWrappingComponent.md,两者 API 形态完全一致,仅返回的 wrapper 类型不同。

源码原理:getWrappingComponent() 是如何实现的

ReactWrapper(mount 场景)

在 ReactWrapper.js 中,getWrappingComponent()的实现位于 L255-L263:

getWrappingComponent() { if (this[ROOT] !== this) { throw new Error('ReactWrapper::getWrappingComponent() can only be called on the root'); } if (!this[OPTIONS].wrappingComponent) { throw new Error('ReactWrapper::getWrappingComponent() can only be called on a wrapper that was originally passed a `wrappingComponent` option'); } return this[WRAPPING_COMPONENT]; }

两个前置校验非常明确:

  • 只能在根 wrapper 上调用this[ROOT] !== this时直接抛错;
  • 必须原本就传入了wrappingComponent选项:否则抛错提示“only be called on a wrapper that was originally passed awrappingComponentoption”。

WRAPPING_COMPONENT是在构造时(L122-L134)创建的:当isCustomComponent(options.wrappingComponent, adapter)为真时,Enzyme 会调用渲染器的getWrappingComponentRenderer(),实例化一个内部类WrappingComponentWrapper(L1250-L1263),并把它加入LINKED_ROOTS列表——这意味着包裹组件与主根是“双向联动”的:更新包裹组件会同步更新主根,反之亦然。

WrappingComponentWrapper继承自ReactWrapper,但重写了getWrappingComponent()并直接抛出TypeError,从而阻止对包裹组件 wrapper 再次调用该方法。

ShallowWrapper(shallow 场景)

在 ShallowWrapper.js 中,getWrappingComponent()的实现位于 L536-L543,逻辑与ReactWrapper版本完全对称:

getWrappingComponent() { if (this[ROOT] !== this) { throw new Error('ShallowWrapper::getWrappingComponent() can only be called on the root'); } if (!this[OPTIONS].wrappingComponent) { throw new Error('ShallowWrapper::getWrappingComponent() can only be called on a wrapper that was originally passed a `wrappingComponent` option'); } return this[WRAPPING_COMPONENT]; }

Shallow 场景的构造过程更特殊:makeShallowOptions(L344-L370)在传入wrappingComponent时,会调用适配器的wrapWithWrappingComponent()把被测节点包进RootFinder,然后创建WrappingComponentWrapper,并通过getContextFromWrappingComponent()深度渲染包裹组件,把包裹组件产生的 legacy context 与createContext()的 Provider 值合并进主 wrapper 的options.context(L362-L369)。这正是“wrappingComponent 为被测组件提供 context”这一能力的实现基础。

Shallow 版WrappingComponentWrapper(L1769-L1800)额外重写了rerender()setState():每次调用都会先执行父类逻辑,再调用updatePrimaryRootContext()(L1731-L1761),重新从包裹组件提取 context 并同步给主 wrapper——因此当包裹组件的setState改变了 context 值时,被测组件会随之重新渲染并获得新值。

底层支撑:adapter-utils 的 wrapWithWrappingComponent

各 React 版本适配器共享的 enzyme-adapter-utils/src/Utils.js 中,wrapWithWrappingComponent()(L340-L350)揭示了包装的最终形态:

export function wrapWithWrappingComponent(createElement, node, options) { const { wrappingComponent, wrappingComponentProps } = options; if (!wrappingComponent) { return node; } return createElement( wrappingComponent, wrappingComponentProps, createElement(RootFinder, null, node), ); }

即渲染出的树为:wrappingComponent(props 来自wrappingComponentProps)→RootFinder→ 被测节点。而getNodeFromRootFinder()(L329-L338)在找不到RootFinder时会抛出`wrappingComponent` must render its children!——再次印证了“包裹组件必须渲染 children”的硬性要求,否则测试会在早期直接失败。

边界条件与错误场景(来自测试用例)

仓库的共享测试 packages/enzyme-test-suite/test/shared/methods/getWrappingComponent.jsx 覆盖了该方法的完整行为,以下是几个必须注意的边界:

场景行为
React ≤ 0.13 适配器构造时即抛TypeError: your adapter does not support \wrappingComponent`. Try upgrading it!`
在非根 wrapper 上调用(如wrapper.find('div').getWrappingComponent()ReactWrapper::getWrappingComponent() can only be called on the root
在包裹组件自己的 wrapper 上再次调用同样抛“only be called on the root”错误(内部类重写了该方法)
未传入wrappingComponent就调用...only be called on a wrapper that was originally passed a \`wrappingComponent\` option
包裹组件不渲染 children`wrappingComponent` must render its children!
根已 unmount 后更新包裹组件(mount 场景)The wrapping component may not be updated if the root is unmounted.
wrappingComponent未渲染 children`wrappingComponent` must render its children!

测试同时验证了关键联动语义:

  • wrappingComponent.setProps()会影响被测组件:更新contextValue后,wrapper.text()随之变为新值;
  • wrappingComponent.setState()会驱动主 wrapper 重新渲染:包裹组件 state 变化后,被测组件树中出现了新增的StateTester节点;
  • 主根 unmount 后,包裹组件 wrapper 的 children 变为空,验证了两者“链接”关系的解除。

实战技巧:用 getWrappingComponent() 操控 createContext

针对 React ≥ 16.3 的React.createContext(),测试用例演示了更现代的用法——直接把 Provider 放在wrappingComponent中,通过setProps切换 Provider 的 value:

const Context = React.createContext(); function WrappingComponent(props) { const { value, children } = props; return <Context.Provider value={value}>{children}</Context.Provider>; } const wrapper = mount( <Context.Consumer> {(value) => <div>{value}</div>} </Context.Consumer>, { wrappingComponent: WrappingComponent, wrappingComponentProps: { value: 'hello!' }, }, ); const wrappingComponent = wrapper.getWrappingComponent(); expect(wrapper.text()).to.equal('hello!'); wrappingComponent.setProps({ value: 'goodbye!' }); expect(wrapper.text()).to.equal('goodbye!');

这里利用了wrappingComponentProps提供初始值,再借getWrappingComponent()返回的 wrapper 完成运行中切换。由于浅渲染场景会经由updatePrimaryRootContext()同步 Provider 值(源码中通过 referential comparison 判断值是否变化以决定是否重渲染),Consumer 能立即读取到新值。

总结

getWrappingComponent()是把“外部依赖注入”与“测试中动态操控”衔接起来的关键 API。记住三条规则即可熟练使用:

  1. 先传wrappingComponent选项,才能调用getWrappingComponent(),且只能在根 wrapper 上调用;
  2. wrappingComponent必须渲染 children,否则无法挂载被测组件;
  3. 返回值与mount/shallow对应ReactWrapperShallowWrapper,均可使用setPropssetState等常规 API,且更新会自动联动主 wrapper。

相关参考:ReactWrapper 文档目录,ShallowWrapper 文档目录,以及wrappingComponent选项的完整说明见 mount.md 与 shallow.md。

  • 测试
  • 前端

【免费下载链接】enzyme

JavaScript Testing utilities for React

项目地址:https://gitcode.com/gh_mirrors/en/enzyme
点击查看免费下载

相关推荐

上一篇:MinecraftForge异常处理最佳实践:优雅应对运行时错误
下一篇:黑苹果安装革命:OpCore Simplify一键自动化配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询