vant-testid-webpack-plugin
v1.0.13
Published
Webpack 4 plugin to auto-inject data-testid into Vant 2 (Vue 2) component DOM for E2E testing
Maintainers
Readme
vant-testid-webpack-plugin
一个 Webpack 4 插件,自动为 Vant 2 UI 组件的 DOM 元素注入 data-testid 属性,使 Playwright、Cypress、Testing Library 等 E2E 测试工具的选择器稳定可靠、维护成本低。
兼容: Vue 2.6+ · Vant 2.x · Webpack 4.x
背景动机
在大型 Vue 2 应用中,为每个 Vant 组件手动添加 data-testid 既繁琐又容易出错。随着 UI 迭代变化,选择器容易失效,测试变得不稳定。本插件通过两层注入解决该问题:
- 编译期: 一个
vue-template-compiler模块生成稳定的、模板范围内唯一且可复现的 testid(van-button-0、van-field-1...)。 - 运行时: 基于 MutationObserver 的注入器可以覆盖编译期变换无法处理的弹出面板、传送门、Portal 和子元素(ActionSheet 选项、Picker 列、Slider 滑块等)。
安装
pnpm add -D vant-testid-webpack-plugin
# or
npm install -D vant-testid-webpack-plugin
# or
yarn add -D vant-testid-webpack-pluginPeer 依赖: vue >=2.6.0, webpack >=4.0.0
快速开始
webpack.config.js 用户
推荐用法:编译器 + Vue 插件 + 运行时
这种方式提供稳定的 testid,不随渲染顺序变化,同时拥有完整的运行时覆盖。
// webpack.config.js
const { VantTestIdWebpackPlugin, createCompilerModule } = require('vant-testid-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
modules: [createCompilerModule()]
}
}
}]
},
plugins: [new VantTestIdWebpackPlugin({ debug: false })]
}// main.js
import Vue from 'vue'
import { createVue2TestIdBridge } from 'vant-testid-webpack-plugin/vue-plugin'
import { setupVantTestIds } from 'vant-testid-webpack-plugin/runtime'
import App from './App.vue'
Vue.use(createVue2TestIdBridge()) // 编译器 testid → DOM
new Vue({ render: h => h(App) }).$mount('#app')
setupVantTestIds() // 面板 / 子元素 / 兜底仅运行时
最简单的配置 — 零模板配置,但 testid 由运行时全局计数器生成。
// webpack.config.js
const { VantTestIdWebpackPlugin } = require('vant-testid-webpack-plugin')
module.exports = {
plugins: [new VantTestIdWebpackPlugin()]
}// main.js
import Vue from 'vue'
import { setupVantTestIds } from 'vant-testid-webpack-plugin/runtime'
import App from './App.vue'
new Vue({ render: h => h(App) }).$mount('#app')
setupVantTestIds()仅编译时
无运行时性能开销,但弹出面板/对话框和子元素不会被注入 testid。
// webpack.config.js
const { VantTestIdWebpackPlugin, createCompilerModule } = require('vant-testid-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
modules: [createCompilerModule()]
}
}
}]
},
plugins: [new VantTestIdWebpackPlugin()]
}// main.js
import Vue from 'vue'
import { createVue2TestIdBridge } from 'vant-testid-webpack-plugin/vue-plugin'
import App from './App.vue'
Vue.use(createVue2TestIdBridge())
new Vue({ render: h => h(App) }).$mount('#app')Vue CLI 用户(vue.config.js)
Vue CLI 项目使用 vue.config.js 而非 webpack.config.js。通过 configureWebpack 和 chainWebpack 配置插件。
Vue CLI:推荐用法(编译器 + Vue 插件 + 运行时)
// vue.config.js
const { VantTestIdWebpackPlugin, createCompilerModule } = require('vant-testid-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [new VantTestIdWebpackPlugin({ debug: false })]
},
chainWebpack(config) {
// 将编译器模块注入 vue-loader
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = options.compilerOptions || {}
options.compilerOptions.modules = [
...(options.compilerOptions.modules || []),
createCompilerModule()
]
return options
})
}
}// main.js(与 webpack.config.js 版本相同)
import Vue from 'vue'
import { createVue2TestIdBridge } from 'vant-testid-webpack-plugin/vue-plugin'
import { setupVantTestIds } from 'vant-testid-webpack-plugin/runtime'
import App from './App.vue'
Vue.use(createVue2TestIdBridge())
new Vue({ render: h => h(App) }).$mount('#app')
setupVantTestIds()Vue CLI:仅运行时
// vue.config.js
const { VantTestIdWebpackPlugin } = require('vant-testid-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [new VantTestIdWebpackPlugin()]
}
}// main.js
import Vue from 'vue'
import { setupVantTestIds } from 'vant-testid-webpack-plugin/runtime'
import App from './App.vue'
new Vue({ render: h => h(App) }).$mount('#app')
setupVantTestIds()Vue CLI:仅编译时
// vue.config.js
const { VantTestIdWebpackPlugin, createCompilerModule } = require('vant-testid-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [new VantTestIdWebpackPlugin()]
},
chainWebpack(config) {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = options.compilerOptions || {}
options.compilerOptions.modules = [
...(options.compilerOptions.modules || []),
createCompilerModule()
]
return options
})
}
}// main.js
import Vue from 'vue'
import { createVue2TestIdBridge } from 'vant-testid-webpack-plugin/vue-plugin'
import App from './App.vue'
Vue.use(createVue2TestIdBridge())
new Vue({ render: h => h(App) }).$mount('#app')API 文档
VantTestIdWebpackPlugin(options?)
Webpack 插件。将全局配置注入构建流程,用于存储配置和调试日志。
new VantTestIdWebpackPlugin({
attributeName?: string // 默认值: 'data-testid'
prefixCls?: string // 默认值: 'van'
compileTime?: boolean // 默认值: false
compilerModuleOptions?: TransformModuleOptions
components?: Record<string, string> // 自定义 CSS 选择器 → testid 前缀映射
debug?: boolean // 默认值: false
})createCompilerModule(options?)
返回一个 vue-template-compiler 模块(含 postTransformNode 钩子),用于编译期注入 testid。需添加到 vue-loader 的 compilerOptions.modules 数组中。
createCompilerModule({
attributeName?: string // 默认值: 'data-testid'
vantPrefix?: string // 默认值: 'van-'
customPrefixes?: string[] // 额外的自定义组件标签前缀
})createVue2TestIdBridge(options?)
Vue 2 插件,在组件 mounted 时读取 vnode.data.attrs 中编译期注入的 testid,并手动写入组件的根 DOM 元素。使用编译期变换时必须同时注册此插件,因为部分 Vant 组件使用了 inheritAttrs: false。
Vue.use(createVue2TestIdBridge({
attributeName?: string // 默认值: 'data-testid'
}))setupVantTestIds(options?)
启动基于 MutationObserver 的运行时注入器。返回一个清理函数。
const stop = setupVantTestIds({
attributeName?: string // 默认值: 'data-testid'
prefixCls?: string // 默认值: 'van'
components?: Record<string, string> // 自定义 CSS 选择器 → testid 前缀映射
panels?: Record<string, PanelInjectStrategy | undefined>
})
// 需要时停止监听
stop()injectCurrentPanels(options?)
一次性为当前已存在或即将出现的面板注入 testId。适用于 SSR/hydration 场景,或不想使用 MutationObserver 的场景。
import { injectCurrentPanels } from 'vant-testid-webpack-plugin/runtime'
// 在弹窗/面板打开后调用
injectCurrentPanels()工作原理
编译期变换
在 Vue SFC 编译期间,createCompilerModule() 遍历模板 AST,为找到的每个 Vant 组件标签注入 data-testid="{tagName}-{index}"。所有模板共享同一个全局计数器,确保 testid 全局唯一。
<!-- 源码 -->
<van-button type="primary">提交</van-button>
<van-field v-model="name" placeholder="请输入姓名" />
<!-- 编译后 -->
<van-button type="primary" data-testid="van-button-0">提交</van-button>
<van-field v-model="name" data-testid="van-field-0" placeholder="请输入姓名" />Vue 插件桥接
createVue2TestIdBridge() 通过全局 mounted mixin,从 vm.$vnode.data.attrs 中读取编译期注入的 testid,并显式调用 el.setAttribute() 写入 DOM。这使得 van-popup、van-dialog、van-picker 等 inheritAttrs: false 的组件也能正确带上 testid。
运行时注入器
setupVantTestIds() 使用 MutationObserver 监听 DOM 变化。当检测到新的 Vant 组件根元素时,使用计数器注入 testid。此外还会:
- 注入子元素:输入框控件、步进器按钮、滑块手柄、评分星星、标签页、上传预览等。
- 注入弹出面板:Picker/DatetimePicker/Area 的工具栏、ActionSheet 选项、ShareSheet 选项、Dialog 按钮等。
- 为编译期变换遗漏的组件提供兜底注入。
运行时 Testid 覆盖范围
组件根元素(60+ 组件)
每个 Vant 2 组件根元素都会获得 van-{组件名}-{序号} 格式的 testid:
van-field、van-search、van-password-input、van-number-keyboard、van-stepper、van-switch、van-slider、van-rate、van-uploader、van-checkbox、van-checkbox-group、van-radio、van-radio-group、van-button、van-popup、van-dialog、van-action-sheet、van-share-sheet、van-notify、van-overlay、van-dropdown-menu、van-picker、van-datetime-picker、van-area、van-tabs、van-nav-bar、van-tabbar、van-sidebar、van-index-bar、van-cell-group、van-cell、van-card、van-tag、van-collapse、van-image、van-image-preview、van-progress、van-circle、van-steps、van-divider、van-calendar、van-panel、van-loading、van-skeleton、van-empty、van-list、van-grid、van-swipe、van-swipe-cell、van-coupon、van-coupon-list、van-coupon-cell、van-submit-bar、van-goods-action、van-contact-card、van-contact-list、van-contact-edit、van-address-list、van-address-edit、van-tree-select、van-pull-refresh、van-notice-bar、van-lazyload
子元素
| 组件 | 子元素 | testid 模式 |
|-----------|-------------|----------------|
| Field | 输入框/文本域 | {testId}-control |
| Field | 清除按钮 | {testId}-clear |
| Field | 左图标 | {testId}-left-icon |
| Field | 右图标 | {testId}-right-icon |
| Field | 错误提示 | {testId}-error |
| Field | 字数限制 | {testId}-word-limit |
| Search | 搜索输入框 | {testId}-search-input |
| Search | 清除按钮 | {testId}-clear |
| Search | 操作按钮 | {testId}-action-{按钮文本} |
| Search | 标签文本 | {testId}-label |
| Stepper | 减号按钮 | {testId}-minus |
| Stepper | 加号按钮 | {testId}-plus |
| Stepper | 输入框 | {testId}-input |
| Switch | 节点元素 | {testId}-node |
| Switch | 加载中 | {testId}-loading |
| Slider | 滑块按钮 | {testId}-button-{i} |
| Slider | 进度条 | {testId}-bar |
| Rate | 星星图标 | {testId}-star-{i} |
| Tabs | 标签导航项 | {testId}-tab-{i}-{标签文本} |
| Tabs | 内容面板 | {testId}-pane-{i} |
| Tabs | 导航容器 | {testId}-wrap |
| Uploader | 文件输入框 | {testId}-input |
| Uploader | 预览项 | {testId}-preview-{i} |
| Uploader | 删除按钮 | {testId}-preview-delete-{i} |
| Uploader | 预览遮罩 | {testId}-preview-cover-{i} |
| Uploader | 上传按钮 | {testId}-upload-btn |
| Steps | 步骤项 | {testId}-step-{i}-{标题} |
| PasswordInput | 密码圆点 | {testId}-item-{i} |
| PasswordInput | 光标 | {testId}-cursor |
| Tabbar | 标签栏项 | {testId}-item-{i}-{文本} |
| Sidebar | 侧边栏项 | {testId}-item-{i}-{文本} |
| IndexBar | 锚点 | {testId}-anchor-{索引} |
| IndexBar | 侧边索引 | {testId}-index-{文本} |
| DropdownMenu | 下拉选项 | {testId}-item-{i}-{标题} |
| NavBar | 左侧区域 | {testId}-left |
| NavBar | 右侧区域 | {testId}-right |
| NavBar | 标题 | {testId}-title |
| NavBar | 文本 | {testId}-text |
| Card | 底部区域 | {testId}-footer |
| Card | 缩略图 | {testId}-thumb |
| Card | 标签 | {testId}-tag |
| Card | 描述 | {testId}-desc |
| Collapse | 折叠项 | {testId}-item-{i}-{标题} |
| Collapse | 折叠包装器 | {testId}-item-{i}-wrapper |
| Collapse | 折叠内容 | {testId}-item-{i}-content |
| Swipe | 轮播项 | {testId}-item-{i} |
| Swipe | 指示器 | {testId}-indicator-{i} |
| SwipeCell | 右侧操作 | {testId}-right-{i} |
| SwipeCell | 左侧操作 | {testId}-left-{i} |
| GoodsAction | 图标按钮 | {testId}-icon-{i}-{文本} |
| GoodsAction | 按钮 | {testId}-button-{i}-{文本} |
| SubmitBar | 价格 | {testId}-price |
| SubmitBar | 按钮 | {testId}-button-{文本} |
| Grid | 宫格项 | {testId}-item-{i}-{文本} |
| Panel | 头部 | {testId}-header |
| Panel | 底部 | {testId}-footer |
| NoticeBar | 左图标 | {testId}-left-icon |
| NoticeBar | 右图标 | {testId}-right-icon |
| Calendar | 头部标题 | {testId}-header-title |
| Calendar | 头部副标题 | {testId}-header-subtitle |
| Calendar | 月份标题 | {testId}-month-{i}-{文本} |
| Calendar | 星期 | {testId}-month-{i}-weekday-{w} |
| Calendar | 日期格子 | {testId}-month-{i}-day-{日期} |
| Calendar | 确定按钮 | {testId}-confirm |
| Picker | 取消按钮 | {testId}-cancel |
| Picker | 确定按钮 | {testId}-confirm |
| Picker | 标题 | {testId}-title |
| Picker | 列 | {testId}-column-{i} |
| Picker | 列选项 | {testId}-column-{i}-item-{文本} |
弹出面板
在 popup/portal 中渲染的 Picker、ActionSheet、ShareSheet、DropdownMenu 和 Dialog 都会自动注入 testid,覆盖它们的工具栏、列、选项和操作按钮。
调试
在浏览器控制台中设置 globalThis.VANT_TESTID_DEBUG = true 可查看 testid 注入日志:
globalThis.VANT_TESTID_DEBUG = true或在插件构造函数中开启调试模式:
new VantTestIdWebpackPlugin({ debug: true })与 Vite 版对比
| | Vite 插件 | Webpack 插件 |
|---|---|---|
| Vue 版本 | Vue 3.3+ | Vue 2.6+ |
| UI 库 | Vant 4 | Vant 2 |
| 编译器 | @vue/compiler-core (nodeTransforms) | vue-template-compiler (modules) |
| Vue 插件 | app.mixin + getCurrentInstance | Vue.mixin + $vnode.data.attrs |
| 输出格式 | ESM | CJS |
许可证
MIT
