Material UI Dialog在Storyshot测试中因Portal兼容问题抛出控制台警告
问题相关依赖版本
"@material-ui/core": "4.12.3" "@storybook/addon-storyshots": "6.3.12", "@storybook/react": "6.3.12", "react": "17.0.2",
警告触发根本原因
该问题仅在Portal被mock或被绕过的Storyshot快照测试场景中才会出现,和业务组件实现无关,核心触发逻辑如下:
- Dialog组件底层依赖Material UI的Modal组件,正常运行时会通过
ReactDOM.createPortal将弹窗内容挂载到独立的DOM容器中,组件挂载后自动聚焦逻辑可以正常命中内部的DialogContent等可聚焦子元素,不会触发警告。 - 社区通用的
createPortal兼容方案(给Dialog加disablePortal属性、mock react-dom的createPortal方法直接返回子元素),本质都是跳过Portal挂载逻辑,将Dialog内容直接嵌套在当前组件树的父节点下渲染。 - 这种渲染结构下,Modal执行初始焦点检测的时机早于内部可聚焦子元素完成挂载,检测到当前容器没有可聚焦目标,就会触发给容器自动添加
tabIndex="-1"的无障碍兜底逻辑,同时抛出如下控制台警告:
Warning: Material-UI: the modal content node does not accept focus. For the benefit of assistive technologies, the tabIndex of the node is being set to "-1".
- 真实浏览器运行环境、普通Storybook预览环境下createPortal可正常工作,DOM结构和渲染时机符合预期,因此不会触发该问题。
无侵入解决方案
不推荐逐个给Dialog加disableAutoFocus的侵入式hack,可从测试配置层统一处理,推荐两种方案:
方案1:替换Storyshots默认渲染器(优先选择)
@storybook/addon-storyshots支持自定义测试渲染器,直接替换默认不支持Portal的react-test-renderer为@testing-library/react的render方法,从根源解决Portal兼容问题,不需要修改任何业务代码或Story代码,渲染逻辑和真实运行环境完全一致:
// storyshots测试入口文件 import initStoryshots from '@storybook/addon-storyshots'; import { render } from '@testing-library/react'; initStoryshots({ renderer: render, });
配置完成后即可移除之前的react-dom mock代码、Dialog上的disablePortal属性,所有弹窗类组件测试都不会再抛出相关警告。
方案2:全局过滤测试场景预期内警告(适配无法替换渲染器的场景)
如果受项目依赖限制暂时无法替换渲染器,可在测试全局配置中过滤该条特定警告,既不会产生冗余日志,也不会影响其他控制台报错、警告的正常输出,同样不需要修改业务代码:
// 测试全局配置文件testSetup.js // 保留原有createPortal mock逻辑 jest.mock("react-dom", () => ({ ...jest.requireActual("react-dom"), createPortal: (children) => children, })); // 过滤特定冗余警告 const nativeConsoleWarn = console.warn; console.warn = (...logArgs) => { if ( typeof logArgs[0] === 'string' && logArgs[0].includes('the modal content node does not accept focus') ) { return; } nativeConsoleWarn.apply(console, logArgs); };
内容的提问来源于stack exchange,提问作者Hoolagon
相关产品推荐
相关产品推荐

