Capybara使用无头Chromium运行e2e测试时无法渲染React代码
问题描述
在Rails/React应用中使用Capybara运行端到端(e2e)测试时,所有基于React编写的JavaScript代码均无法正常执行:页面中<div id="root"></div>节点始终为空,但相同代码在本地开发环境、Docker环境下均可正常渲染,本地直接运行Capybara测试也可稳定复现该问题。
异常现象具体表现为:
- 若手动添加
document.getElementById("root").innerText = "Foo bar"原生JS代码,该行逻辑可正常执行 ReactDOM.render相关逻辑要么无法被识别执行、要么完全不运行- 针对Stimulus框架编写的测试代码可正常渲染
- 为排查问题尝试将React版本降级至16后,问题仍未解决
背景信息
- 项目使用Vite.js进行JavaScript代码打包,初步判断该问题与Vite无关但不排除其关联性
- 应用运行于Alpine Docker环境,因本地环境可直接复现问题,排除环境特异性导致故障的可能
- 项目中Rails路由对应空白端点,仅返回包含
#root节点的空白HTML页面,供React完成水合与路由跳转逻辑,项目未使用react-railsgem - 测试运行时返回的HTML页面中,静态资源路径均指向正确的JS文件,且React业务代码确实存在于对应加载的JS文件中
复现代码
以下为Capybara测试相关核心运行代码,包含测试用最小React片段、Capybara测试用例、print page.html输出结果:
- 入口文件
app/javascript/entrypoints/test.jsx
import React from "react" import ReactDOM from "react-dom" // 取消注释后该行可正常执行,但不会被render方法中的"Hello World"替换 // document.getElementById("root").innerText = "Foo bar" // 以下逻辑始终无法正常执行 ReactDOM.render( <div>Hello World</div>, document.getElementById("root") )
- 视图模板
test.html.haml(原作者备注:已知晓HAML存在使用缺陷)
!!! %html{lang: :en} %head = vite_client_tag = vite_react_refresh_tag = vite_javascript_tag "test.jsx" %body #root
- 测试用例
react_test_spec.rb
require "rails_helper" RSpec.describe "Testing react", type: :feature, js: true do describe "just checking", :with_csrf do before { visit test_home_path } subject { page } it "renders react" do print page.html expect(page).to have_content "Hello World" end end end
print page.html输出结果
<html lang="en"><head> <script src="/vite-test/assets/test.92ee76c9.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite-test/assets/jsx-dev-runtime.ddafb254.js" as="script" crossorigin="anonymous"> </head> <body> <div id="root"></div> </body></html>
相关配置
以下为项目配置与依赖信息,包含包版本、Capybara/Vite配置等内容:
package.json相关依赖片段
{ // react 相关依赖 "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "6", // vite 相关依赖 "stimulus-vite-helpers": "^3.0.0", "vite": "^2.9.1", "vite-plugin-ruby": "^3.0.9", "vite-plugin-stimulus-hmr": "^3.0.0", "@vitejs/plugin-react": "^1.3.2", // babel 相关依赖 "@babel/core": "^7.0.0-0", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.17.12", "@babel/eslint-parser": "^7.17.0", "@babel/plugin-transform-runtime": "^7.18.2", "@babel/preset-env": "^7.17.10", "babel-jest": "^27.5.1", "babel-plugin-macros": "^3.1.0" }
- Capybara配置
capybara.rb
Capybara.register_driver :chrome_headless do |app| options = ::Selenium::WebDriver::Chrome::Options.new options.add_argument("--headless") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--window-size=1400,1400") Capybara::Selenium::Driver.new(app, browser: :chrome, capabilities: [options]) end Capybara.javascript_driver = :chrome_headless
- Vite配置
vite.config.ts
export default defineConfig({ build: { sourcemap: true, }, plugins: [RubyPlugin(), react(), StimulusHMR()], })
- Vite环境配置
vite.json
{ "all": { "sourceCodeDir": "app/javascript", "watchAdditionalPaths": [] }, "development": { "autoBuild": true, "publicOutputDir": "vite-dev", "port": 3036 }, "test": { "autoBuild": true, "publicOutputDir": "vite-test", "port": 3037 } }
Dockerfile.development相关依赖安装片段(本地使用chromedriver运行测试时同样可复现该问题)
RUN apk add \ build-base \ chromium \ chromium-chromedriver \
问题原因
核心问题是测试环境下错误加载了仅开发环境需要的Vite热更新相关标签,导致React代码执行中断:
vite_client_tag和vite_react_refresh_tag是Vite开发服务器运行时才需要的标签,用于热更新、React快速刷新功能,测试环境下Vite不会启动开发服务器,仅通过autoBuild构建静态资源,这两个标签指向的资源会加载失败。@vitejs/plugin-react@1.3.2在非production模式(包括test模式)下构建React代码时,会自动注入React Refresh相关的运行时调用($RefreshReg$、$RefreshSig$全局函数),但测试环境下没有加载对应的刷新运行时脚本,会直接抛出$RefreshReg$ is not defined的JS错误。- 错误发生在React相关代码初始化阶段,因此写在React逻辑前的原生JS可以正常执行,不依赖React Refresh的Stimulus代码也可正常运行,但
ReactDOM.render逻辑因前面的报错中断,始终无法执行,降级React版本也无法解决该问题。
解决方案
- 修正视图模板,仅在Vite开发服务器实际运行时加载热更新相关标签,修改
test.html.haml:
!!! %html{lang: :en} %head -# 仅开发环境热更新需要以下两个标签,测试/生产环境不加载 - if ViteRuby.instance.dev_server_running? = vite_client_tag = vite_react_refresh_tag = vite_javascript_tag "test.jsx" %body #root
- (可选)优化Vite测试环境配置,避免构建时注入开发环境专属代码,修改
vite.config.ts:
export default defineConfig(({ mode }) => ({ build: { sourcemap: true, }, plugins: [RubyPlugin(), react(), StimulusHMR()], // 测试环境下模拟React Refresh全局变量,避免极端场景下的未定义报错 define: mode === 'test' ? { $RefreshReg$: '() => {}', $RefreshSig$: '() => () => {}' } : {} }))
- (可选)测试执行前预构建Vite测试环境资源,避免autoBuild延迟导致的资源404问题,在测试启动命令前增加执行:
bin/vite build --mode test
- (排查辅助)如果修改后仍有问题,在RSpec配置中增加浏览器日志打印,可直接看到具体JS报错:
RSpec.configure do |config| config.after(type: :feature, js: true) do |example| if example.exception puts "=== 浏览器控制台日志 ===" page.driver.browser.manage.logs.get(:browser).each do |log| puts "[#{log.level}] #{log.message}" end end end end
内容的提问来源于stack exchange,提问作者Robby Kim
相关产品推荐
相关产品推荐

