资讯动态

@cypress/vite-plugin-cypress-esm 深入解析:让 Vite 驱动的 Cypress 组件测试中的 ESM 模块可被 stub 与 spy

发布时间:2026/9/8 23:36:16 来源:尧图企业网站定制
cypress/vite-plugin-cypress-esm 深入解析让 Vite 驱动的 Cypress 组件测试中的 ESM 模块可被 stub 与 spy【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypressCypress 组件测试中的cy.stub()/cy.spy()需要改写模块命名空间的成员而 ES Module 规范强制命名空间被“密封sealed”导致 mock 在浏览器端无从下手。cypress/vite-plugin-cypress-esm本仓库位于 npm/vite-plugin-cypress-esm通过服务端 Vite 转换 浏览器端Proxy包装两层协作来解决这一矛盾。读完本文你将掌握该插件的安装与集成方式、ignoreModuleList与ignoreImportList两类逃生配置的用法、底层重写与代理原理以及其已知边界与排障方法。问题背景为什么组件测试需要这个插件ESM 规范ECMA-262 的模块章节要求运行时将模块命名空间视为“密封”对象禁止任何对命名空间成员的修改这会带来安全与性能收益。但对于测试而言它恰恰阻止了 mock 库用替换命名空间成员的方式注入假实现。Cypress 内部正是基于 Sinon 来实现cy.stub与cy.spy因此组件测试在 Vite 环境里默认无法直接对 ESM 导出的函数、组件进行打桩。该插件通过在服务端把模块导入拦截并重写在客户端把所有模块包装进一个特殊的Proxy实现中使原本只读的 ESM 模块命名空间变得可改写从而让cy.stub()、cy.spy()能够正常工作。需要注意该包当前处于alpha 预发布阶段AGENTS.md 与 README 均明确标注API 与行为可能变化README 建议生产使用前应接受潜在的不稳定性。架构总览两层分工从 AGENTS.md 与目录结构看该插件按职责拆成三个部分src/index.ts—— Vite 插件入口注册 Vite 的 transform 钩子负责把 ESM 的静态/动态导入重写为经过模块缓存module cache的访问形式client/moduleCache.js—— 浏览器端运行时实现把模块命名空间包装成可写Proxy的核心逻辑并以script typemodule注入测试页面dist/—— 编译产物由tsc输出。在 Cypress 的 Vite 组件测试体系中它常与cypress/vite-dev-server搭配使用本仓库 npm/vite-dev-server 即提供 Vite 作为组件测试 dev server 的桥接能力。这是一个面向测试的专用插件README 明确建议只在运行 Cypress 测试时把它合入 Vite 配置。快速开始构建、检查与运行从 package.json 可以看到该包的关键命令# 编译输出到 dist/tsc即使有类型错误也会输出并提示 yarn build # 仅做 TypeScript 类型检查不产出文件 yarn check-ts # ESLint 检查 yarn lint # 运行指定的组件测试 spec yarn cypress:run -- --spec path-to-spec # 交互式打开组件测试 yarn cypress:open包本身声明了debug与picomatch锁 2.3.0两个运行时依赖picomatch用于决定哪些模块需要被包装的 glob 模式匹配。Vite 未在 dependencies 中显式声明而是作为使用者项目里的 peer 依赖被消费AGENTS.md 明确说明它是“被 Vite 插件身份隐含的依赖”。模块类型为type: module入口指向dist/index.js对外导出的类型位于dist/index.d.ts。集成方式在 cypress.config 中按需合并插件README 推荐只在 Cypress 测试运行时启用插件一种做法是在cypress.config中通过 Vite 的mergeConfig把插件合入已有配置import { defineConfig } from cypress import viteConfig from ./vite.config import { mergeConfig } from vite import { CypressEsm } from cypress/vite-plugin-cypress-esm export default defineConfig({ component: { devServer: { bundler: vite, framework: react, viteConfig: () { return mergeConfig( viteConfig, { plugins: [ CypressEsm(), ], }, ) }, }, }, })本仓库自测用配置 cypress.config.ts 是同样的思路且在react()插件之后启用CypressEsm(...)并配置了ignoreModuleList: [**/ignoreModuleList.cy.ts, *MyAsync*]与ignoreImportList: [**/ImmutableModuleB*, **/react-dom/client]后者注释说明React 18 下使用cypress/react需要跳过对react-dom/client库的转换。底层原理Vite transform 阶段如何重写导入插件入口导出的工厂函数CypressEsm返回一个名为cypress:mocks、enforce: post的 Vite 插件见 src/index.ts核心逻辑集中在transform钩子与transformIndexHtml钩子中。转换前会依次跳过三类文件命中ignoreModuleList的模块、命中正则的非 JS 资源如.svg|png|jpe?g|gif|tiff|webp|json|md|txt|xml|x?html?|css|less|sass|scss等因为动态 import 图片、数据资源无需也不应代理、以及被ignoreImportList点名的导入目标。静态导入的重写mapImportsToCache对每个被处理的模块插件把形如下面的语法import DefaultExport, { NamedExport, Other as Alias } from module改写为import * as cypress_module_1 from module; const DefaultExport __cypressModule(moduleId#module, cypress_module_1, isDebug); const { NamedExport, Other: Alias } __cypressModule(moduleId#module, cypress_module_1, isDebug);实现上插件先用正则捕获import ... from ...声明该正则以行首或空格为前置约束避免误伤Refresh.__hmr_import()之类的调用再把导入变量按“解构块”与“非解构逗号块”分别切分处理默认导入、命名导入、import * as foo、import { foo as bar }会转成const { foo: bar }等多种形态最终把每条声明重写为import * as cypress_xxx_N ...const ... __cypressModule(...)的组合。模块标识符会把原 moduleId 中的非字母数字字符替换为下划线用于生成唯一的变量名。动态导入的重写mapDynamicImportsToCacheimport(...)动态导入通过另一套正则被包上一层运行时包装例如const m import(./mod_1) const m await import(lodash) import(./mod_2).then(mod mod)会被改写为const m __cypressDynamicModule(import(./mod_1)) const m await __cypressDynamicModule(import(lodash)) __cypressDynamicModule(import(./mod_2)).then(mod mod)__cypressDynamicModule返回一个 Promise它等待原始 import 完成后把得到的模块交给同一套“代理化”逻辑处理见 client/moduleCache.js。index.html 与运行时注入transformIndexHtmltransformIndexHtml会对测试页面的 HTML 做同样的静态/动态导入映射并以fs.readFileSync把 client/moduleCache.js 的源码整体作为script typemodule内联标签注入页面src/index.ts 中通过MODULE_CACHE_FILEPATH指向../client/moduleCache.js。这样一来重写后的__cypressModule/__cypressDynamicModule两个全局函数就有了浏览器端实现。浏览器端运行时用 Proxy 让密封命名空间可写client/moduleCache.js 是整个方案的另一半核心函数createProxyModule的要点如下默认导出优先代理基座选择module.default || module以同时兼容import DefaultValue from module的默认导入若默认导出是函数则生成一个可被new的包装函数保证类与函数组件都能实例化。数组不代理数组默认导出直接原样返回因为无法对数组做 spy也无需代理。重定义属性描述符通过Object.getOwnPropertyDescriptors遍历属性以writable: true、configurable: true重新defineProperty实现“解封”prototype等个别键被NO_REDEFINE_LIST跳过。对值为函数的成员若判定为 class通过toString()正则探测^class\s.?\{.?\}则用Reflect.construct支持new否则用保留调用上下文apply的普通包装函数。Proxy陷阱的协同get陷阱在发现调用栈包含Sandbox.spy即 Sinon 正在创建 spy时返回真实函数而非包装版本使 spy 能透传真实实现set陷阱把新值写回 target 并为新函数补建包装defineProperty会忽略带isSinonProxy标记的写入防止 Sinon 覆盖掉包装函数deleteProperty一律返回true阻止删除——Sinon 清理时会尝试删除属性会破坏已包装的函数。模块缓存去重cacheAndProxifyModule以原模块对象为 key 缓存代理结果若代理化过程抛错则回退到原模块并在控制台警告“将不支持 stub/spy”。两个全局入口moduleCache.js同时接收_debug布尔参数用于控制上述log输出的开关。配置项ignoreModuleList 与 ignoreImportList源码中CypressEsmOptionssrc/index.ts定义了三个选项配置项类型语义ignoreModuleListstring[]picomatch 模式匹配的模块本身不做代理化处理原样放行。例如把react-router加入后该模块内部的导出、及其内部对依赖的使用都无法被 stubignoreImportListstring[]picomatch 模式匹配的导入目标在任意导入方中都不走自定义映射直接使用未改写的模块ignoreListstring[]已废弃兼容遗留配置任何条目会被并入ignoreModuleList所有值若非数组或含非字符串元素会直接抛错assertIsArrayOrUndefined。底层通过picomatch编译 matcher对导入路径匹配时还会先剥掉开头的./见isImportOnIgnoreList对 picomatch#77 的规避。典型用法// 跳过整个模块支持 glob CypressEsm({ ignoreModuleList: [react-router, react-router-dom], }) CypressEsm({ ignoreModuleList: [*react*], }) // 跳过某一处具体导入 CypressEsm({ ignoreImportList: [**/internal/problematic-file.js], }) // 使用 cypress/react 测试 React 18 时通常需要 CypressEsm({ ignoreImportList: [**/react-dom/client], })README 提醒两处需要分辨的语义细节其一加入ignoreModuleList的模块若被其他文件导入导入动作仍会被插件处理只是这些导入方拿到的是未改写的原始版本其二React 这类第三方依赖与 Proxy 实现存在已知冲突且你通常并不想 stub React 自身因此建议把 React 放进ignoreModuleList。何时用ignoreImportList的三种典型诉求验证未改写行为的测试、排除行为不受支持的依赖、以及避开下述自动提升auto-hoisting破坏的代码。调试手段插件内置了基于debug库的日志命名空间cypress:vite-plugin-cypress-esmDEBUGcypress:vite-plugin-cypress-esm yarn cypress:run -- --spec path-to-spec按 README 与 AGENTS.md 的说明开启后你会在终端看到服务端代码转换日志例如Remapping imports for module ...、Mapping import N (...) in module ...、跳过资产/忽略列表的⏭️/提示在浏览器控制台看到模块拦截与Proxy包装日志 creating proxy module for ...、✅ created proxy module for ...、️ Detected ... being defined as a Sinon spy等。重写逻辑会把debug.enabled状态随调用一并传入客户端运行时从而控制浏览器端日志输出。测试与能力验证仓库自带的组件测试是理解插件行为边界的最佳样本cypress/component涵盖stub.cy.tsx—— 对命名空间导入的模块做 stub先用cy.stub(M, add)把加法改成乘法断言行为被替换且下一个测试用例又回到真实实现也验证了 React 类组件、函数组件被整体 stub替换成h1Stub Component/h1以及从node_modules静态导入的 lodash 方法可被 stubspy.cy.ts—— 验证cy.spy场景下调用栈检测路径能拿到真实函数实现dynamicImport.cy.ts—— 验证import(./mod_1)、await import(lodash)等动态导入场景ignoreModuleList.cy.ts/ignoreImportList.cy.ts—— 分别验证模块级、导入级豁免配置的效果importSyntax.cy.ts—— 覆盖默认导出、命名导出、别名、import * as等各类 import 语法对应 fixtures 目录里的defaultExportArray.ts、namedExportArray.ts、exportDefaultConst.tsx、kitchenSink.ts、class.ts等assetTypes.cy.ts、edgeCases.cy.tsx、reactQuery.cy.tsx—— 覆盖资源类型导入、边界用例与真实第三方库TanStack React Query。已知问题与边界README 列出了四类关键限制使用时需格外注意自动提升Auto-hoisting缺失ESM 规范会把 import 提升到模块顶部而本插件不做任何 hoisting只是把 import 就地转为变量引用。若代码在 import 声明前引用被导入值则会报错典型如 Svelte 项目中的 HMR 逻辑通常表现为 “use before define” 错误。可考虑用ignoreImportList绕过。正则匹配的局限当前转换基于正则而非 ASTREADME 表示未来会探索更稳健的 AST 方案。它无法区分真实代码与字符串内的示例代码片段可能误改字符串常量。模块内自引用self-reference不生效插件只拦截“进入模块”的外部调用模块内部对自身函数的直接调用/引用比较不会被代理。例如mod_1.js中bar(mod) { return mod foo }外部经Proxy传入的是包装后的foo而内部mod_1的foo是原始未包装函数比较结果为false。React Router 懒加载路由等场景可能因此出问题可用ignoreModuleList绕开。Sinon 兼容范围插件只面向 Cypress 内部使用的 Sinoncy.stub/cy.spy设计使用其他 mock 库或直接改写模块均不属于支持范围通常无法按预期工作。此外 import 语法虽基本全覆盖README 称“所有已知 import 语法均已支持”仍可能存在未发现的边角情况。排障建议README 给出的 Alpha 阶段排障流程先确保插件与 Cypress 均为最新版再临时把插件从测试用的 Vite 配置中移除若问题依旧则与本插件无关接着核对是否命中上述已知问题然后用ignoreModuleList/ignoreImportList收缩范围判断是否与某个具体模块/依赖相关仍无法定位时带上终端与浏览器 devtools 的 Debug 日志提交 bug 报告附上可复现的最小工程最有助于定位。兼容性与状态说明按 package.json 与 README该包遵循 MIT 许可对外发布名为cypress/vite-plugin-cypress-esmREADME 给出的版本兼容矩阵为 v1对应 Cypress v12 v2仅 ESM 模块版对应 Cypress v16。它是一个pre-release alpha官方提示存在 bug 与边界场景属于预期报告问题请遵循上述排障流程。集成到真实项目时建议仅在 Cypress 组件测试的 Vite 配置中启用并始终为已知不兼容的第三方依赖尤其是 React 与react-dom/client保留豁免配置。【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价