Meteor E2E 测试体系详解:用于骨架与打包器集成的隔离 Jest + Playwright 环境
2026/9/19 22:24:11 网站建设 项目流程

Meteor E2E 测试体系详解:用于骨架与打包器集成的隔离 Jest + Playwright 环境

【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteor

Meteor 仓库在 tools/e2e-tests/ 下维护了一套独立的端到端(E2E)测试环境,用于对各类应用骨架(skeleton)和打包器(rspack 等 bundler)集成做真实验证。本文基于该目录的 README 与配套源码,讲清楚这套环境为什么必须"隔离"存在、如何安装运行、Jest 与 Playwright 的关键配置含义,以及从复制 fixture 应用、启动 dev server 到浏览器断言和进程清理的完整执行链路,帮助你在开发 Meteor 工具链时能够运行、调试和扩展这套 E2E 测试。

为什么 E2E 测试需要一个独立目录

tools/e2e-tests/README.md 开篇给出了目录存在的核心理由:

仓库根目录的node_modules/被用来构建 dev bundle——它本身就是 Meteor 工具的一部分。如果把测试依赖(jest、playwright、swc、cheerio、semver、underscore)也装在那里,可能拉入不兼容的传递依赖版本(例如 lru-cache v10 与 v5 冲突),从而悄无声息地破坏 dev bundle 构建甚至已发布的 Meteor 版本。

这正是该子目录设计的第一原则:测试依赖与产品构建依赖物理隔离。E2E 测试的node_modules完全局限在tools/e2e-tests/内,永远不会影响 Meteor 自身的构建与发布。

README 还描述了测试的真实度:测试会创建真实的 Meteor 项目、启动 dev server,并在 headless Chromium 中验证行为——不是 mock 层面的单元测试,而是完整走一遍meteor create/meteor run的集成路径。

从源码可以印证这一点。helpers.js 直接定位到仓库根目录的可执行文件作为被测对象:

const REPO_ROOT = path.resolve(__dirname, '../..'); const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor');

也就是说,E2E 测试驱动的是仓库根目录的 meteor 可执行入口(dev bundle 构建产物),测的正是"真实工具 + 真实浏览器"这条链路。

目录结构:E2E 环境如何组织

tools/e2e-tests/下的关键成员(摘自仓库实际目录):

文件/目录职责
jest.config.jsJest 主配置:preset、超时、转换器、并发
jest-playwright.config.jsPlaywright 浏览器配置与录制开关
jest.setup.js环境修正(端口、NODE_ENV)、重试策略
helpers.js核心测试基建:建应用、跑命令、等输出、杀进程
test-helpers.js高阶测试模式封装(testMeteorSkeleton/testMeteorBundler
assertions.js浏览器断言(标题、H1、console 错误、HTTP 状态)
apps/测试 fixture:14 个预置 Meteor 应用
scripts/辅助脚本:create-app.jslink-rspack.js
package.json隔离安装的测试依赖清单

apps/目录内置了与测试文件一一对应的 fixture 应用:accountsbabelblazecoffeescriptfull-blazemonoreporeactreact-routerserver-onlysolidsveltesymlink-monorepotypescriptvue。同名还有skeleton.test.jsreact.test.jsvue.test.js等 19 个*.test.js测试文件,以及存放回归用例的regressions/目录。

快速开始:安装与运行命令

README 明确要求以下命令都在仓库根目录执行

# Install dependencies (first time) npm run install:e2e # Run all E2E tests npm run test:e2e # Run a specific suite npm run test:e2e -- --testPathPattern skeleton

对照根目录 package.json,这三个脚本的真实展开是:

"install:e2e": "cd tools/e2e-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell", "test:e2e": "cd tools/e2e-tests && npm test -- ", "create-app:e2e": "cd tools/e2e-tests && node scripts/create-app.js"

几个值得注意的点:

  • install:e2e会额外执行npx playwright install --with-deps chromium chromium-headless-shell,安装浏览器二进制及系统依赖。--with-deps意味着需要能安装系统包(CI 容器或具备 sudo 的 Linux 环境),这也是首次安装可能耗时较长的原因。
  • 测试依赖在 tools/e2e-tests/package.json 中声明,核心包括:jest@^29jest-playwright-preset@^3.0.1playwright@1.59.0(精确锁版)、@swc/core+@swc/jest(测试代码转换)、cheerio(HTML 断言)、execa/wait-on/fs-extra(进程与文件基建)、semver/underscore
  • test:e2e末尾的--是 npm 的参数透传分隔符,--testPathPattern skeleton因此能作为 Jest CLI 参数生效,实现"只跑某一个套件"(README 中的第三条命令)。
  • 此外根目录还提供create-app:e2e,指向 scripts/create-app.js,用于手工创建骨架应用做调试。

Jest 与 Playwright 配置详解

jest.config.js:超时、并发与转换器

jest.config.js 的完整内容不长,但每一处都有实战考量:

module.exports = { preset: 'jest-playwright-preset', rootDir: __dirname, testMatch: ["**/*.test.js"], testPathIgnorePatterns: ["<rootDir>/apps/"], setupFilesAfterEach: ['<rootDir>/jest.setup.js'], verbose: true, // Increase timeout for CLI operations (longer on CI to absorb host contention) testTimeout: process.env.CI ? 240_000 : 120_000, transformIgnorePatterns: [ "/node_modules/(?!(execa|wait-on|is-docker|is-stream|human-signals|merge-stream|npm-run-path|onetime|mimic-fn|strip-final-newline|path-key|shebug-command|shebug-regex)/)" ], transform: { "^.+\\.js$": ["@swc/jest", { jsc: { parser: { syntax: "ecmascript" }, target: "es2022" }, module: { type: "commonjs" }, }], }, maxWorkers: 1, forceExit: !process.env.RECORD, reporters: ['default', '<rootDir>/summary-reporter.js'], };

逐项解读:

  • testTimeout: CI ? 240s : 120s:E2E 的每个"测试"内部实际包含了meteor create/npm install/ 启动 dev server 等重型 CLI 操作,本地 120 秒、CI 240 秒,CI 放宽是为了吸收宿主机资源争抢。
  • testPathIgnorePatterns: ["<rootDir>/apps/"]apps/下是 fixture 应用,其中也可能存在*.test.js(Meteor 应用自带测试文件),必须排除,否则会被误当作 Jest 用例。
  • transformIgnorePatterns白名单:默认 Jest 不转换node_modules,但execawait-on等是纯 ESM 包,这里显式放行它们及其传递依赖,否则加载即报错。
  • transform使用@swc/jest:把 ES 语法编译为es2022+ CommonJS,速度远快于 Babel 方案。
  • maxWorkers: 1:串行执行是有意为之——各套件使用固定端口(如 skeleton 套件的 3201–3219),并行会互相抢占端口。
  • forceExit: !process.env.RECORD:强制 Jest 在测试结束后退出,防止孤儿 rspack 子进程挂住进程;但录制视频时(RECORD=1)必须关闭,否则context.close()的视频落盘会未完成。
  • summary-reporter.js:除默认输出外,追加一份 summary-reporter.js 汇总报告。

jest-playwright.config.js:无需改代码的调试开关

Playwright 配置独立在 jest-playwright.config.js(jest-playwright-preset只从该文件读取,写在jest.config.jsglobals里会被忽略)。它提供了一组环境变量开关,调试时无需修改任何代码:

环境变量效果
HEADED=1有头模式运行 Chromium(可见窗口)
SLOWMO=250每个动作之间加 250ms 延迟(配合 HEADED 观察操作)
DEVTOOLS=1自动打开 Chromium DevTools(隐含 HEADED)
RECORD=1每个测试文件录制一个 webm,输出到./test-results/videos
RECORD_DIR=./out覆盖录制输出目录

核心实现(jest-playwright.config.js#L13-L29):

module.exports = { browsers: ['chromium'], launchOptions: { headless: !process.env.HEADED && !process.env.DEVTOOLS, slowMo: process.env.SLOWMO ? Number(process.env.SLOWMO) : 0, devtools: !!process.env.DEVTOOLS, }, contextOptions: process.env.RECORD ? { recordVideo: { dir: process.env.RECORD_DIR || './test-results/videos', size: { width: 1280, height: 720 }, }, viewport: { width: 1280, height: 720 }, } : {}, };

只使用chromium单一浏览器,录制分辨率固定为 1280×720。

jest.setup.js:端口、NODE_ENV 与重试策略

jest.setup.js 在每个测试文件加载前统一修正环境:

  1. 清空NODE_ENVprocess.env.NODE_ENV = ''),防止测试运行器的环境值泄漏进meteor子进程,改变其构建行为。

  2. 固定端口

    process.env.RSPACK_DEVSERVER_PORT = '18080'; process.env.RSDOCTOR_CLIENT_PORT = '8888'; process.env.RSDOCTOR_SERVER_PORT = '8889';

    RSPACK_DEVSERVER_PORT默认取 18080,注释说明了原因:一些骨架(如 Angular CLI)自带 dev server 占用 8080,用错端口会冲突。单个测试可以通过testMeteorSkeleton/testMeteorBundlerdevServerPort选项按需覆盖。

  3. CI 下放宽 Playwright 超时:客户端渲染框架在慢速 CI runner 上水合(hydration)可能较慢,CI 环境对page设置 60s 的默认选择器/动作超时,避免waitForSelector与框架启动竞态。

  4. 重试策略:默认只有 CI 开启 1 次重试(process.env.CI ? 1 : 0),本地运行 fail fast 以便暴露 flake;用METEOR_E2E_TEST_RETRIES可强制覆盖(设为"0"禁用 CI 重试,设为"2"放宽)。setup 文件还维护了一个attemptCountsMap(Jest 29 没有暴露currentTestName的重试次数 API),把"当前是否为重试"写入globalThis.__e2eIsRetryAttempt,供 helpers.js 的isRetryAttempt()使用——这保证重试时能走独立的临时目录,验证"重试隔离"语义。

  5. E2E_FORCE_FLAKY_TEST自检钩子:在afterEach中让匹配模式的测试在第一次尝试时强制失败,用于验证重试隔离机制本身是否有效——这是一个"测试测试框架"的巧妙设计。

从 Fixture 到断言的完整执行链路

E2E 的核心基建集中在 helpers.js,一次典型的骨架测试按以下链路执行。

第 1 步:准备 fixture 应用(setupMeteorApp)

setupMeteorApp 把apps/下的 fixture 复制到系统临时目录:

const tempDir = path.join(os.tmpdir(), `meteortest-${appName}-${randomSuffix}`); await fs.copy(sourceAppDir, tempDir, { dereference: !preserveFixtureSymlinks, // symlink-monorepo 需要保留符号链接 preserveTimestamps: true, overwrite: true });
  • 随机后缀保证每次运行都是全新目录,天然隔离;
  • preserveFixtureSymlinks选项专门服务于symlink-monorepofixture(默认dereference会把符号链接解引用成实体文件);
  • 复制后执行npm install;monorepo fixture(isMonorepo: true)会在根目录和app/子目录各执行一次install,模拟 pnpm/yarn workspaces 类的双级依赖布局。

另一条路径是 createMeteorApp:不复制 fixture,而是直接调用meteor create --{example} {appName}现场生成骨架(校验退出码,失败即抛错),用于测试meteor create本身的行为。

第 2 步:启动 dev server 并等待就绪(runMeteorApp)

runMeteorApp 用execa启动meteor run --port {port}(monorepo 场景在app/子目录中执行),随后做两件事:

(a)输出等待 + MongoDB 看门狗(waitForOutputWithMongoWatchdog):

const mongoTimeout = options.mongoTimeout || (process.env.CI ? 90000 : 45000); const mongoWait = waitForMeteorOutput(outputLines, '=> Started MongoDB.', { timeout: mongoTimeout, meteorProcess });

它并行监听三类信号并取最先到达者:期望的waitForOutput模式、failOnOutput失败模式(匹配即抛错)、以及'=> Started MongoDB.'这一行。看门狗的动机写在注释里:如果mongod卡死(比如复用 CI 容器上残留的锁文件),没有看门狗就要白白烧满整个 240s 输出等待;有看门狗则 45s(CI 90s)内快速失败并给出"疑似 stale mongod / lock file"的诊断信息。若设置了外部MONGO_URL,Meteor 不会启动本地实例,看门狗自动跳过。

(b)端口就绪等待:用wait-on轮询http-get://localhost:{port},超时本地 90s、CI 300s(可用skipWaitOn跳过)。

输出等待的基础原语是 waitForMeteorOutput:按 100ms 间隔轮询已捕获的输出行,支持字符串/正则、negate否定模式(等待某行"不出现")、startIndex(只看某时刻之后的输出,常用于验证热更新后新出现的编译错误),默认超时本地 90s / CI 240s;若 Meteor 进程提前退出则立即失败并附上最后 20 行输出,避免"进程已死还要等满超时"。

第 3 步:浏览器端断言(assertions.js)

assertions.js 提供了assertMeteorAppassertBodyStylesassertRspackScriptTagassertFileExist等断言。以 assertMeteorApp 为例,它做四件事:

  1. 导航到http://localhost:{port}
  2. 校验<title>与 H1(默认期望Welcome to Meteor!);
  3. 收集consoleerror、pageerror与状态码 ≥400 的 HTTP 响应作为失败诊断信息;
  4. CI 下首次加载允许重试一次——注释解释:慢速 CI(如 Docker)上 rspack 代理对客户端 bundle 的首个请求可能返回 504,第二次请求时 dev server 已预热。

此外 helpers 还提供 waitForPlaywrightConsole(等待/否定浏览器 console 消息,支持collectAllLogs全量收集)与resetPlaywrightPage(把共享 Playwright page 导航到about:blank,防止上一个应用进程死后遗留的客户端回调泄漏进下一个测试)。

第 4 步:清理——进程与端口的三重兜底

E2E 最大的工程风险是孤儿进程(dev server、rspack 子进程、mongod)污染后续测试与 CI 容器。helpers 为此设计了层层递进的清理机制:

(a)优雅退出(killMeteorProcess):先发SIGTERM等待最多 12s(graceMs),让 Meteor 的关闭钩子执行(注释点明:rspack 插件的 handler 会在此释放 dev server 端口);未退出才升级SIGKILL

(b)游离进程清扫(killStrayAppProcesses):先杀所有被跟踪的 Meteor 进程,再在 Unix 上按 argv 特征清扫脱离的子进程:

ps -eo pid=,args= | grep -E 'meteortest[-]' | awk '{print $1}' | xargs -r kill -9

正则里'meteortest[-]'的字符组写法是为了避免清扫命令匹配到它自己——这是进程管理里一个经典的细节。

(c)端口占用清理(killProcessByPort / killSingleProcessByPort):Windows 下用netstat + taskkill;Unix 下合并lsofss的结果找监听 PID(最小化容器可能只装了其中一个),尝试杀整个进程组(先对比自身 PGID,绝不误杀 Jest 自己的进程组)、fuser -k兜底,然后循环验证端口确实释放ss -tln检查),最多 5 轮、每轮间隔 400ms——注释强调"不验证就宣称成功会让孤儿进程存活"。

最后 cleanupTempDir 用 rimraf(同步失败则退化为异步重试)删除临时目录。

高阶测试模式:testMeteorSkeleton 与 testMeteorBundler

test-helpers.js 把上述原语组合成两种可直接套用的 Jestdescribe生成器:

testMeteorSkeleton—— 骨架全生命周期模板。测试文件只需声明配置,如 skeleton.test.js 中的 Angular 骨架:

describe('Angular Skeleton /', testMeteorSkeleton({ skeletonName: 'angular', port: 3213, filePaths: { client: 'client/main.ts', server: 'server/main.ts', test: 'tests/main.ts', }, }), );

每个骨架绑定一个固定端口(Angular 3213、Apollo 3201、Babel 3212、Bare 3219、Blaze 3202……),模板内部按 create → run → 浏览器断言 → test → build 的顺序走完整生命周期,并支持checkAppTitleskipTestClientskipBuildCacheCheck等开关(如 Bare 骨架关闭标题与样式检查)。

testMeteorBundler(test-helpers.js#L93)—— 针对 rspack 集成的变体,在beforeAll中:

  1. 把本次测试的RSPACK_DEVSERVER_PORT切到指定值(默认 18080),避免与骨架自带 dev server 撞端口;
  2. 清理端口、复制 fixture 并npm install
  3. 执行meteor add rspack并调用 scripts/link-rspack.js 把仓库本地的npm-packages/meteor-rspack链接进应用——即测试始终跑在最新 dev 版本上,而不是已发布的 npm 版本。可用NPM_LINK_RSPACK=false关闭该行为,改从 npm 安装@meteorjs/rspack(test-helpers.js#L41-L49 有显式警告提示,提醒 CI 失败时需先确认新版本已发布)。

两个细节体现了 CI 工程化经验:isCIGITHUB_ACTIONS === 'true'判定;beforeAll钩子单独拿到更大的超时预算SETUP_HOOK_TIMEOUT_MS = CI ? 600s : 300s(test-helpers.js#L53-L58),因为"create + install + 构建"整体会超出 240s 的testTimeout,而测试体仍保持更紧的超时。

helpers.js 中的runMeteorTests则封装meteor test --driver-package meteortesting:mochatestClient: true时设TEST_BROWSER_DRIVER=playwright驱动浏览器客户端测试,否则设TEST_CLIENT=0只跑服务端,checkTestResults时由 Jest 校验退出码并向上传播失败。

实战示例:在运行中的应用里验证 TypeScript 类型检查

一个能体现"输出等待"能力的真实用例是 skeleton.test.js 中的assertTsgoTypeChecker:向正在运行的应用写入一个故意的类型错误探针文件,然后等待编译器输出对应的诊断码:

const probePath = path.join(tempDir, 'imports/ts-checker-e2e-probe.ts'); try { const errorOutputStart = result.outputLines.length; await fs.outputFile(probePath, 'export const tsCheckerProbe: string = 123;\n'); await waitForMeteorOutput(result.outputLines, /TS2322/, { meteorProcess, startIndex: errorOutputStart, // 只看探针写入之后新增的输出 }); } finally { await fs.remove(probePath); }

这里用startIndex记录了探针写入前输出行数,确保只匹配新增输出——否则历史输出中若恰好存在TS2322会造成误判。helpers 中还有replaceFileContent/appendFileContent两个原语,专门用于在运行中的测试里改动文件以触发文件变更检测(热更新)类断言。

调试建议:有头模式、录像与重试验证

综合以上配置,本地调试这套 E2E 环境的实用姿势是:

# 有头模式 + 慢动作,肉眼观察浏览器行为 HEADED=1 SLOWMO=250 npm run test:e2e -- --testPathPattern react # 自动打开 DevTools 排查前端问题 DEVTOOLS=1 npm run test:e2e -- --testPathPattern skeleton # 录制每个测试文件的 webm 录像(视频存到 tools/e2e-tests/test-results/videos/) RECORD=1 npm run test:e2e -- --testPathPattern vue # 验证某测试的重试隔离是否生效(首次尝试强制失败,重试应通过) E2E_FORCE_FLAKY_TEST="<测试名关键字>" npm run test:e2e

需要牢记的约束:maxWorkers: 1决定了套件串行、端口固定,因此不适合按文件拆分并行跑;forceExitRECORD=1时自动关闭;首次安装依赖可能因浏览器与系统依赖下载而显著耗时。

小结

tools/e2e-tests/为 Meteor 工具链提供了一条高置信度的集成验证通道:

  1. 隔离原则:测试依赖独立安装,不污染用于构建 dev bundle 的根node_modules(README 给出的核心动机);
  2. 真实链路:以仓库根目录的meteor可执行文件为被测对象,覆盖 create → run → test → build 全生命周期,并在 headless Chromium 中做行为断言;
  3. 可观测与可调试:输出等待、MongoDB 看门狗、console/HTTP 失败诊断、HEADED/DEVTOOLS/RECORD开关、重试与E2E_FORCE_FLAKY_TEST自检;
  4. 鲁棒清理:SIGTERM 优雅退出、按 argv 特征清扫游离进程、端口释放循环验证,三层兜底防止污染后续测试。

如果你正在为 Meteor 新增骨架或修改 rspack 打包链路,这套环境就是验收你的改动是否破坏"真实项目体验"的标准工具——先npm run install:e2e,再npm run test:e2e -- --testPathPattern <套件名>,用 skeleton.test.js 与 test-helpers.js 的模式作为新测试的模板即可。

【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteor

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

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

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

立即咨询