首页
/ Flutter Web 集成测试实战:web_e2e_tests 包的本地运行、测试矩阵与 CI 自动化原理

Flutter Web 集成测试实战:web_e2e_tests 包的本地运行、测试矩阵与 CI 自动化原理

2026-09-06 11:36:32作者:晏闻田Solitary

本篇围绕 Flutter 仓库中 web_e2e_tests 集成测试包 展开,讲清楚三件事:如何在本地启动 chromedriver 并用 flutter drive 命令运行 Web 端集成测试、如何根据 CI 源码确定每个测试对应的 Web 渲染器与构建模式矩阵、以及 dev/bots/suite_runners/run_web_tests.dart 中 CI 自动化运行这些测试的完整调用链。读完本文,你既能独立在本机复跑 Flutter 官方的 Web E2E 测试,也能理解这些测试如何被 CI 分片调度、以及诸如 tree-shaking 验证等“编译产物级”检查的实现原理。

1. 测试包定位与目录结构

dev/integration_tests/web_e2e_tests 是 Flutter 仓库中专用于 Web 平台的端到端(E2E)集成测试包,验证的是真实浏览器环境下 Flutter Web 引擎的行为,而不是模拟器里的 widget 测试。其目录组织如下:

dev/integration_tests/web_e2e_tests/
├── lib/                      # 各测试对应的应用入口(被测 App)
│   ├── common.dart                 # DOM 查找等公共工具
│   ├── text_editing_main.dart      # 文本输入测试的 App 入口
│   ├── url_strategy_main.dart      # URL 策略测试的 App 入口
│   ├── capabilities_integration_canvaskit 相关入口
│   ├── treeshaking_main.dart       # tree-shaking 验证用最小 App
│   ├── deferred_loading_lib.dart   # 延迟加载验证
│   ├── scroll_wheel_main.dart      # 滚轮滚动测试
│   ├── profile_diagnostics_main.dart
│   ├── screenshot_support.dart     # 截图支持
│   └── target_platform_main.dart
├── test_driver/                  # 驱动端测试代码(flutter drive 执行)
│   ├── text_editing_integration.dart / _test.dart
│   ├── url_strategy_integration.dart / _test.dart
│   ├── capabilities_integration_canvaskit.dart / _test.dart
│   ├── capabilities_integration_skwasm.dart / _test.dart
│   ├── cache_width_cache_height_integration*.dart
│   ├── deferred_loading_integration*.dart
│   ├── profile_diagnostics_integration*.dart
│   ├── scroll_wheel_integration*.dart
│   └── url_strategy_integration*.dart
├── web/                          # Web 宿主页面
│   ├── index.html
│   └── flutter_bootstrap.js
├── pubspec.yaml
└── README.md

从包名与依赖看(pubspec.yaml),该包要求 Dart SDK ^3.11.0-0、使用 resolution: workspace 参与仓库统一 pub workspace,并依赖 flutter_driverflutter_testflutter_web_pluginsintegration_testweb 等 SDK 包,dev_dependencies 中还引入了 flutter_goldenslib/test_driver/ 的“成对文件”结构体现了一个惯例:test_driver/xxx_integration.dart 是驱动端测试,它 import 对应 lib/xxx_main.dart 的 App 入口,在同一个浏览器会话里“一边驱动、一边断言”。

2. 本地运行:chromedriver 与 flutter drive

README 给出的标准本地运行流程分为两步。

2.1 准备 chromedriver

flutter drive 通过 WebDriver 协议控制 Chrome,因此需要先准备与本机 Chrome 版本匹配的 chromedriver:

  1. 打开 chrome://version 查看本机 Chrome 版本,再下载对应版本的 chromedriver;
  2. 启动 chromedriver 并监听 4444 端口:
chromedriver --port=4444

4444 是 WebDriver 的默认端口——CI 侧自动化代码也是连接这个端口的(见第 4 节 _ensureChromeDriverIsRunning 的实现),本地手动运行与 CI 保持一致可以避免踩坑。

2.2 用 flutter drive 运行集成测试

集成测试通过 flutter drive 命令执行。README 给出的官方示例为:

flutter drive --target=test_driver/text_editing_integration.dart \
  -d web-server \
  --browser-name=chrome \
  --profile

各参数含义:

参数 说明
--target=test_driver/text_editing_integration.dart 驱动端测试文件路径。text_editing_integration 是该包中测试数量最多的一个文件,验证 Web 引擎对原生 <input>/<textarea> 的桥接行为
-d web-server 设备类型指定为 Web 服务器目标,flutter drive 会构建 Web 产物并在本地起 HTTP 服务
--browser-name=chrome 指定通过 chromedriver 驱动的浏览器为 Chrome
--profile 构建模式。README 特别注明该示例是在 profile 模式下运行;可选值对应 debug / profile / release 三种构建模式

2.3 选择正确的渲染器与构建模式

不同测试是针对特定 Web 渲染器(CanvasKit 或 Skwasm)和/或特定构建模式编写的。README 要求在运行前先到 CI 脚本中确认该测试应在哪种组合下执行。需要注意的是:README 中提到的 _runWebLongRunningTests 是历史函数名,从当前仓库源码结构看,对应的测试矩阵已经迁移到 dev/bots/suite_runners/run_web_tests.dartwebLongRunningTestsRunner 方法中(第 4 节详述)。以该文件为事实依据,各测试的当前 CI 运行组合为:

测试(test_driver/ 下的文件名) CI 中的构建模式 × 编译器组合
profile_diagnostics_integration debug / profile / release,均不使用 WASM
scroll_wheel_integration 仅 debug(源码注释:“only known to work in debug mode”)
text_editing_integration debug / profile / release(注释说明该测试极不稳定,目前按此组合临时收敛)
url_strategy_integration debug / profile / release
capabilities_integration_canvaskit debug / profile / release
cache_width_cache_height_integration debug / profile
deferred_loading_integration release × 普通 JS 与 --wasm 两种
treeshaking_main.dart flutter drive,走 flutter build web --profile 后检查产物

因此本地运行 deferred_loading_integration 时应带 --release(WASM 场景再加 --wasm),运行 scroll_wheel_integration 时应带 --debug;而 capabilities_integration_skwasm 虽然存在于 test_driver/ 目录中,但从 CI 矩阵看当前并未被 webLongRunningTestsRunner 纳入常规调度,本地复跑时需自行指定 Skwasm 渲染器(通过 --dart-define=FLUTTER_WEB_USE_SKIA=false 一类配置选择 Skwasm 路径,具体以渲染器官方文档为准)。

3. 测试是如何“驱动真实 DOM”的:源码级剖析

理解这些测试的实现方式,比记住命令更重要。以下结合源码逐点说明。

3.1 绑定初始化与 DOM 断言

所有驱动端测试的 main() 第一行都是:

IntegrationTestWidgetsFlutterBinding.ensureInitialized();

这让 testWidgets 在真浏览器中运行(而非纯 mock 的测试 binding),并且可以在测试体内直接触碰 DOM。例如 text_editing_integration.dart 验证“聚焦 TextFormField 后,Web 引擎会向 DOM 追加一个原生 <input> 元素,且其 value 与 Flutter 侧文本同步”:

// Focus on a TextFormField.
await tester.tap(find.byKey(const Key('input')));

// A native input element will be appended to the DOM.
final web.NodeList nodeList = findElements('input');
expect(nodeList.length, equals(1));
final input = nodeList.item(0)! as web.HTMLInputElement;
expect(input.value, 'Text1');

// Change the value of the TextFormField.
textFormField.controller?.text = 'New Value';
// DOM element's value also changes.
expect(input.value, 'New Value');

DOM 查询由公共工具 common.dart 提供。findElements(selector) 会先 document.querySelector('flutter-view') 定位 Flutter Web 引擎挂载的 <flutter-view> 根节点,再在其下查询选择器(若找不到根节点会 fail 并提示“应用可能未启动”或“调用过早”),从而保证断言的是 Flutter 渲染出来的 DOM 而非页面其他内容。

同一文件还演示了向 DOM 元素派发合成键盘事件的写法——用 package:web 构造 KeyboardEventdispatchEvent

web.KeyboardEvent dispatchKeyboardEvent(
  web.EventTarget target,
  String type,
  web.KeyboardEventInit eventInitDict,
) {
  final event = web.KeyboardEvent(type, eventInitDict);
  target.dispatchEvent(event);
  return event;
}

该文件覆盖的用例包括:Enter 键触发 onFieldSubmitted、Tab 键在两个 TextField 间移动焦点(含 CapsLock 激活后仍有效)、只读 SelectableText 拖拽选中时对应的 <textarea>readonly 属性且选区正确。这些都是纯 Dart 单测无法覆盖、必须依赖真实浏览器输入管线才能验证的行为。

3.2 URL 策略:用内存假 History 做端到端验证

url_strategy_integration.dart 展示了另一类典型 E2E 手法:测试在 App 内部实现了一个纯内存的 TestUrlStrategy(完整模拟浏览器 history 语义:pushState 时截断分支历史、go(count) 延迟到下一个事件循环并触发 popstate 监听器),再通过 package:flutter_web_pluginssetUrlStrategy(strategy) 注入:

final strategy = TestUrlStrategy.fromEntry(const TestHistoryEntry('initial state', null, '/'));
setUrlStrategy(strategy);
app.main();
// ...
navigator.pushNamed('/foo');
await tester.pump();
expect(strategy.getPath(), '/foo');

这样在 CI 无头浏览器里也能稳定复现“导航 → URL 变化”的完整链路,而不依赖浏览器真实 history 的可观测性。

3.3 渲染器能力断言

capabilities_integration_canvaskit.dart 只有几条断言,但意义在于锁定“当前构建确实使用了预期的渲染器”:

expect(isCanvasKit, true);
expect(isSkwasm, false);
expect(isSkiaWeb, true);

这正是第 2.3 节“渲染器 × 构建模式矩阵”存在的意义:如果构建参数选错渲染器,测试会立刻失败而不是产生误报。

4. CI 自动化:flutter drive 的完整调用链

上述测试在 CI 中由 dev/bots/suite_runners/run_web_tests.dart 调度。该文件提供了几个值得本地开发者借鉴的自动化细节。

4.1 chromedriver 的自动拉起与探活

_ensureChromeDriverIsRunning(约 L666-L695)先尝试对 localhost:4444 建 TCP 连接(复用已存在的 chromedriver,否则自建进程 chromedriver --port=4444 --log-level=INFO --enable-chrome-logs),轮询等待其就绪,随后通过 HTTP GET http://localhost:4444/status 解析 JSON 中的 value.ready 字段确认 WebDriver 真正可用——比“端口能连通”更强的一层校验。

4.2 _runWebE2eTest:本包测试的通用驱动入口

包内每个 test_driver/xxx_integration.dart 都由同一个函数驱动(约 L256-L268):

/// Runs one of the `dev/integration_tests/web_e2e_tests` tests.
Future<void> _runWebE2eTest(String name, {required String buildMode, required bool useWasm}) async {
  await _runFlutterDriverWebTest(
    target: path.join('test_driver', '$name.dart'),
    buildMode: buildMode,
    useWasm: useWasm,
    testAppDirectory: path.join(flutterRoot, 'dev', 'integration_tests', 'web_e2e_tests'),
  );
}

底层 _runFlutterDriverWebTest(约 L270-L329)的完整命令构造为:先 flutter clean、删除上轮遗留的 build/integration_response_data.json,再执行:

flutter drive \
  --target=test_driver/<name>.dart \
  --browser-name=chrome \
  -d web-server \
  --<buildMode>            # 即 --debug / --profile / --release
  [--wasm]                 # useWasm 为 true 时追加
  --no-web-resources-cdn   # 禁用引擎资源 CDN,使用本地缓存产物

并以环境变量 FLUTTER_WEB=true 运行。这解释了第 2 节矩阵表中的每一行:本地手动运行只要复现这套参数组合即可。

4.3 tree-shaking 验证:对编译产物做“字符串级”检查

_runWebTreeshakeTest(约 L336-L386)不走 flutter drive,而是把 treeshaking_main.dart 这个最小 App 用 flutter build web --profile --no-web-resources-cdn 编译(profile 模式可避免 minify 导致符号名不可见),然后读取 build/web/main.dart.js 做三项断言:

  1. 产物包含 RootElement——确认 JS 未被 minify,否则测试会假阳性通过;
  2. timeline.dart 中未使用的类(AggregatedTimedBlockAggregatedTimedTimings_BlockBuffer_StringListChain_Float64ListChain 等)不得出现在产物中——证明无用代码被正确剔除;
  3. debugFillProperties 出现次数不得超过 11 次——限制调试辅助代码泄漏进发布产物的规模。

这是“把编译器行为当作被测对象”的典型 E2E 手法,本地也可以照此对任意 Web 构建产物做瘦身验证。

4.4 分片与调度

webLongRunningTestsRunner 末尾用固定种子 math.Random(0) 对所有任务做 shuffle 后再交给 runShardRunnerIndexOfTotalSubshard(tests) 分片执行——注释解释了动机:让快慢测试交错分布,使各分片耗时大致均衡。这也意味着本地跑单个测试(第 2.2 节的 flutter drive 命令)与 CI 的整矩阵运行是完全兼容的两种粒度。

5. 参考与延伸阅读

6. 小结

web_e2e_tests 包用一套很轻量的约定——lib/ 放被测入口、test_driver/ 放驱动端断言、flutter drive -d web-server 在真实 Chrome 中执行——覆盖了文本输入桥接、URL 策略、渲染器能力、缓存尺寸、延迟加载、profile 诊断等 Web 专属行为;而 run_web_tests.dart 则展示了这些测试如何在“渲染器 × 构建模式”矩阵中被自动化调度,以及如何用产物字符串检查验证 tree-shaking。本地复跑时,牢记三要素:匹配版本的 chromedriver 监听 4444、-d web-server --browser-name=chrome、以及按 CI 矩阵选取 --debug/--profile/--release(必要时 --wasm),即可与官方 CI 得到一致的行为验证。

登录后查看全文
热门项目推荐
相关项目推荐