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.js | Jest 主配置:preset、超时、转换器、并发 |
| jest-playwright.config.js | Playwright 浏览器配置与录制开关 |
| jest.setup.js | 环境修正(端口、NODE_ENV)、重试策略 |
| helpers.js | 核心测试基建:建应用、跑命令、等输出、杀进程 |
| test-helpers.js | 高阶测试模式封装(testMeteorSkeleton/testMeteorBundler) |
| assertions.js | 浏览器断言(标题、H1、console 错误、HTTP 状态) |
| apps/ | 测试 fixture:14 个预置 Meteor 应用 |
| scripts/ | 辅助脚本:create-app.js、link-rspack.js |
| package.json | 隔离安装的测试依赖清单 |
apps/目录内置了与测试文件一一对应的 fixture 应用:accounts、babel、blaze、coffeescript、full-blaze、monorepo、react、react-router、server-only、solid、svelte、symlink-monorepo、typescript、vue。同名还有skeleton.test.js、react.test.js、vue.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@^29、jest-playwright-preset@^3.0.1、playwright@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,但execa、wait-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.js的globals里会被忽略)。它提供了一组环境变量开关,调试时无需修改任何代码:
| 环境变量 | 效果 |
|---|---|
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 在每个测试文件加载前统一修正环境:
清空
NODE_ENV(process.env.NODE_ENV = ''),防止测试运行器的环境值泄漏进meteor子进程,改变其构建行为。固定端口:
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/testMeteorBundler的devServerPort选项按需覆盖。CI 下放宽 Playwright 超时:客户端渲染框架在慢速 CI runner 上水合(hydration)可能较慢,CI 环境对
page设置 60s 的默认选择器/动作超时,避免waitForSelector与框架启动竞态。重试策略:默认只有 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()使用——这保证重试时能走独立的临时目录,验证"重试隔离"语义。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 提供了assertMeteorApp、assertBodyStyles、assertRspackScriptTag、assertFileExist等断言。以 assertMeteorApp 为例,它做四件事:
- 导航到
http://localhost:{port}; - 校验
<title>与 H1(默认期望Welcome to Meteor!); - 收集
consoleerror、pageerror与状态码 ≥400 的 HTTP 响应作为失败诊断信息; - 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 下合并lsof与ss的结果找监听 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 的顺序走完整生命周期,并支持checkAppTitle、skipTestClient、skipBuildCacheCheck等开关(如 Bare 骨架关闭标题与样式检查)。
testMeteorBundler(test-helpers.js#L93)—— 针对 rspack 集成的变体,在beforeAll中:
- 把本次测试的
RSPACK_DEVSERVER_PORT切到指定值(默认 18080),避免与骨架自带 dev server 撞端口; - 清理端口、复制 fixture 并
npm install; - 执行
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 工程化经验:isCI以GITHUB_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:mocha:testClient: 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决定了套件串行、端口固定,因此不适合按文件拆分并行跑;forceExit在RECORD=1时自动关闭;首次安装依赖可能因浏览器与系统依赖下载而显著耗时。
小结
tools/e2e-tests/为 Meteor 工具链提供了一条高置信度的集成验证通道:
- 隔离原则:测试依赖独立安装,不污染用于构建 dev bundle 的根
node_modules(README 给出的核心动机); - 真实链路:以仓库根目录的
meteor可执行文件为被测对象,覆盖 create → run → test → build 全生命周期,并在 headless Chromium 中做行为断言; - 可观测与可调试:输出等待、MongoDB 看门狗、console/HTTP 失败诊断、
HEADED/DEVTOOLS/RECORD开关、重试与E2E_FORCE_FLAKY_TEST自检; - 鲁棒清理: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),仅供参考