vant4-testid-webpack-plugin
v1.0.3
Published
Webpack 5 plugin to auto-inject data-testid into Vant 4 (Vue 3) component DOM for E2E testing
Maintainers
Readme
vant4-testid-webpack-plugin
一个 Webpack 5 插件,为 Vue 3 + Vant 4 项目的 DOM 元素自动注入 data-testid 属性,让 Playwright / Cypress / Testing Library 等 E2E 测试更稳定、更易维护。
兼容: Vue 3.2+ · Vant 4.x · Webpack 5.x
环境要求: Node.js
>=18.0.0
本包是 vant-testid-webpack-plugin
(面向 Vue 2 + Vant 2 + Webpack 4)的 Webpack 5 / Vue 3(Vant 4)对应版本,沿用其「铁三角」三层注入架构。
动机
在大型 Vue 3 应用中手动为每个 Vant 组件添加 data-testid 既繁琐又容易遗漏。随着 UI 演进,选择器会失效、测试变脆。本插件通过三层自动注入解决这一问题:
- 编译期:Vue 3
nodeTransforms模块生成稳定、模板作用域内的 testid(van-button-0、van-field-1…)。 - Vue 插件桥接:
createTestIdBridge()从 VNode props 读取编译期 testid 并写入根 DOM 元素,绕过inheritAttrs: false。 - 运行时:基于 MutationObserver 的注入器补全弹层、teleport 入口以及编译期无法触及的子元素(ActionSheet 项、Picker 列、Slider 手柄等)。
安装
pnpm add -D vant4-testid-webpack-plugin
# 或
npm install -D vant4-testid-webpack-plugin
# 或
yarn add -D vant4-testid-webpack-pluginPeer 依赖: vue >=3.2.0, webpack >=5.0.0
快速开始
推荐:编译期 + Vue 插件 + 运行时
可获得跨 re-render 稳定的 testid,并具备完整运行时覆盖。
// webpack.config.js
const { Vant4TestIdWebpackPlugin, testIdTransforms } = require('vant4-testid-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
nodeTransforms: testIdTransforms()
}
}
}]
},
plugins: [new Vant4TestIdWebpackPlugin({ compileTime: true })]
}// main.ts
import { createApp } from 'vue'
import { createTestIdBridge } from 'vant4-testid-webpack-plugin/vue-plugin'
import { setupVantTestIds } from 'vant4-testid-webpack-plugin/runtime'
import App from './App.vue'
const app = createApp(App)
app.use(createTestIdBridge()) // 编译器 testid → DOM
app.mount('#app')
setupVantTestIds() // 面板 / 子元素 / 兜底当
compileTime: true时,插件会自动把testIdTransforms()注入到 vue-loader 的compilerOptions.nodeTransforms,无需手动在每条 rule 上配置。你也可以手动添加 (如上所示),插件会去重合并。
纯运行时
最简单,零配置,但 testid 在运行时由全局计数器生成。
// webpack.config.js
const { Vant4TestIdWebpackPlugin } = require('vant4-testid-webpack-plugin')
module.exports = {
plugins: [new Vant4TestIdWebpackPlugin()]
}// main.ts
import { createApp } from 'vue'
import { setupVantTestIds } from 'vant4-testid-webpack-plugin/runtime'
import App from './App.vue'
createApp(App).mount('#app')
setupVantTestIds()仅编译期
无运行时开销,但弹层/对话框面板及子元素不会获得 testid。
// webpack.config.js
const { testIdTransforms } = require('vant4-testid-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
nodeTransforms: testIdTransforms()
}
}
}]
}
}// main.ts
import { createApp } from 'vue'
import { createTestIdBridge } from 'vant4-testid-webpack-plugin/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(createTestIdBridge())
app.mount('#app')在 Vue CLI(vue.config.js)中使用
Vue CLI(Vue 3 项目底层使用 Webpack 5)通过 vue.config.js 提供 webpack 配置入口。有两种等价方式接入本插件。
方式 A —— 让插件自动注入(推荐)
compileTime: true 时,插件的 apply() 会遍历最终生成的 module.rules,找到 vue-loader 规则,并自动把 testIdTransforms() 合并进其 compilerOptions.nodeTransforms,无需手动修改 vue-loader 的 options。
// vue.config.js
const { Vant4TestIdWebpackPlugin } = require('vant4-testid-webpack-plugin')
module.exports = {
// configureWebpack 接受的对象会与最终 webpack 配置合并。
// 插件被加入 plugins 数组,其 apply() 会自动向 vue-loader 注入 nodeTransforms。
configureWebpack: {
plugins: [
new Vant4TestIdWebpackPlugin({ compileTime: true }),
],
},
}方式 B —— 显式使用 chainWebpack(需要完全掌控时)
当项目对 vue-loader 做了大量自定义,或上面的自动注入未能定位到 loader 时使用。
// vue.config.js
const {
Vant4TestIdWebpackPlugin,
testIdTransforms,
} = require('vant4-testid-webpack-plugin')
module.exports = {
chainWebpack: (config) => {
// 1) 显式向 vue-loader 添加编译期 nodeTransforms
config.module
.rule('vue')
.use('vue-loader')
.tap((options = {}) => {
options.compilerOptions = options.compilerOptions || {}
options.compilerOptions.nodeTransforms = [
...(options.compilerOptions.nodeTransforms || []),
...testIdTransforms(),
]
return options
})
// 2) 注册 webpack 插件,用于全局配置 / 运行时兜底
config
.plugin('vant4-testid')
.use(Vant4TestIdWebpackPlugin, [{ compileTime: false }])
},
}方式 B 中第 1 步已手动添加了
nodeTransforms,因此插件compileTime设为false; 若保留true,插件会再注入一次(合并时自动去重)。
main.ts / main.js 侧(Vue 插件桥接 + 运行时 setupVantTestIds())与上面的快速开始示例完全一致。
API 参考
Vant4TestIdWebpackPlugin(options?)
Webpack 5 插件。存储全局配置,并在 compileTime: true 时自动把 nodeTransforms 注入 vue-loader。
new Vant4TestIdWebpackPlugin({
attributeName?: string // 默认 'data-testid'
prefixCls?: string // 默认 'van'
testIdPrefix?: string // 默认 ''
compileTime?: boolean // 默认 false
compilerModuleOptions?: TransformOptions
components?: Record<string, string> // 自定义 CSS 选择器 → 前缀映射
debug?: boolean // 默认 false
})testIdTransforms(options?) / createCompilerModule(options?)
返回 NodeTransform[],用于在编译期注入 testid,添加到 vue-loader 的
compilerOptions.nodeTransforms。createCompilerModule 为兼容 Vue 2 包命名习惯的别名。
testIdTransforms({
attributeName?: string // 默认 'data-testid'
vantPrefix?: string // 默认 'van-'
customPrefixes?: string[] // 额外的组件 tag 前缀
injectForLoops?: boolean // 默认 true
injectEventElements?: boolean // 默认 true
testIdPrefix?: string // 默认 ''
debug?: boolean // 默认 false
})createTestIdBridge(options?) / createVue3TestIdBridge(options?)
Vue 3 插件,从 VNode props 读取编译期 testid 并写入根 DOM 元素。使用编译期注入时必需,
因为部分 Vant 4 组件使用了 inheritAttrs: false。
app.use(createTestIdBridge({
attributeName?: string // 默认 'data-testid'
}))setupVantTestIds(options?)
启动运行时 MutationObserver 注入器,返回清理函数。
const stop = setupVantTestIds({
attributeName?: string // 默认 'data-testid'
prefixCls?: string // 默认 'van'
injectAllElements?: boolean // 默认 true(为所有 HTML 元素注入 testid)
components?: Record<string, string>
panels?: Record<string, PanelInjectStrategy | undefined>
debug?: boolean
})
// 之后停止监听
stop()injectCurrentPanels(options?)
为已存在或即将出现的面板一次性注入。适用于 SSR/hydration 或不想用 MutationObserver 的场景。
import { injectCurrentPanels } from 'vant4-testid-webpack-plugin/runtime'
// 弹窗打开后调用
injectCurrentPanels()rescanAllTestIds()
重新扫描整个 document.body 并注入 testid。适用于全页导航、HMR 替换 DOM 等场景。
import { rescanAllTestIds } from 'vant4-testid-webpack-plugin/runtime'
document.addEventListener('some-navigation-event', () => rescanAllTestIds())deduplicateForLoopTestIds(root?, attrName?)
为无 index 变量的 v-for 产生的重复 testid 去重,对第 2+ 个追加 -{index} 后缀。
import { nextTick } from 'vue'
import { deduplicateForLoopTestIds } from 'vant4-testid-webpack-plugin/runtime'
nextTick(() => deduplicateForLoopTestIds())工作原理
编译期 transform(三层计数器)
Vue SFC 编译期间,testIdTransforms() 遍历模板 AST,为每个 Vant/自定义组件注入 data-testid,采用三层计数器架构:
- v-for 动态注入(最高优先级)— 组件在
v-for中带有 index 变量(v-for="(item, i) in list")时,生成动态:data-testid表达式,利用 index 生成每轮迭代唯一的 testid,无需运行时去重。 - 条件块子计数器 —
v-if/v-else-if/v-else/v-show块拥有独立子计数器,块内元素的增删不影响外部 testid 稳定性。 - 全局计数器 — 跨模板共享计数器 +
usedIds集合自动跳过已占用 testid。
事件监听元素使用 {tag}-event-{names}-{n} 格式。
<!-- 源码 -->
<van-button type="primary">Submit</van-button>
<van-field v-model="name" placeholder="Enter name" />
<!-- 编译后(简化) -->
<van-button type="primary" data-testid="van-button-0">Submit</van-button>
<van-field v-model="name" data-testid="van-field-0" placeholder="Enter name" />Vue 插件桥接
createTestIdBridge() 通过 app.mixin({ mounted() }) 读取 vnode.props['data-testid'] 并显式调用 el.setAttribute(),绕过 van-popup、van-dialog、van-picker 等组件的 inheritAttrs: false。
运行时注入器
setupVantTestIds() 通过 MutationObserver 监听 DOM 变化。检测到新的 Vant 组件根时,使用计数器注入 testid,并:
- 注入子元素:field 控件、stepper 按钮、slider 手柄、rate 星标、tab 项等。
- 注入弹层:Picker/DatePicker/TimePicker/Area 工具栏、ActionSheet 项、ShareSheet 选项、Dialog 按钮。
- 为编译期遗漏的组件兜底。
调试
在浏览器控制台设置 globalThis.__VANT4_TESTID_DEBUG = true 查看 testid 注入日志:
globalThis.__VANT4_TESTID_DEBUG = true或在插件构造函数中开启 debug 模式:
new Vant4TestIdWebpackPlugin({ debug: true })与 Vue 2 / Webpack 4 版本对比
| | vant4-testid-webpack-plugin | vant-testid-webpack-plugin |
|---|---|---|
| Vue 版本 | Vue 3.2+ | Vue 2.6+ |
| UI 库 | Vant 4 | Vant 2 |
| Webpack | Webpack 5 | Webpack 4 |
| 编译器 | @vue/compiler-core(nodeTransforms) | vue-template-compiler(modules) |
| Vue 插件 | app.mixin + getCurrentInstance | Vue.mixin + $vnode.data.attrs |
| 输出格式 | CJS | CJS |
常见报错
nodeTransforms[i] is not a function(或 nodeTransform[xx] is not a function)
这是 @vue/compiler-core 在编译模板时,发现 compilerOptions.nodeTransforms 数组里
混入了非函数元素而抛出的 TypeError。本插件注入的 transform 本身都是合法函数,
因此问题几乎都出在配置方式上。两种最常见原因:
漏写括号(最高频):把工厂函数本身当成数组传了进去:
// ❌ 错误:testIdTransforms 是函数,nodeTransforms[0] 取到 undefined compilerOptions: { nodeTransforms: testIdTransforms } // ✅ 正确:调用它,拿到 NodeTransform[] compilerOptions: { nodeTransforms: testIdTransforms() }已有的 nodeTransforms 里有脏值:例如从别的构建工具 / 自己代码 spread 进来的 某一项是
undefined、字符串或非函数。
解决方式(任选其一):
- 推荐:使用
compileTime: true,让插件自动把正确的testIdTransforms()注入 vue-loader,不要再手动设置nodeTransforms。插件会自动清洗掉非函数项并打印告警, 不会再让整个构建崩溃。 - 若必须手动配置:务必写成
nodeTransforms: testIdTransforms()(带括号),并确保你已有的nodeTransforms数组每一项都是函数。
许可证
MIT
