vite-plugin-vue-testid
v1.1.5
Published
Vite plugin to auto-inject data-testid into Vue component templates for E2E testing
Maintainers
Readme
vite-plugin-vue-testid
编译期 + 运行时自动为 Vue 组件模板注入 data-testid 的 Vite 插件,专为 ant-design-vue 4.x 适配,大幅降低 E2E 测试中的元素定位成本。
特性
- 纯运行时全覆盖 — 通过 MutationObserver 为所有 ant-design-vue 组件根元素、子元素、teleport 面板自动注入
data-testid,无需编译期配置即可使用 - 编译期可选增强 — 为业务自定义组件(如
<MyInput />)在模板编译期注入唯一稳定的 testid,通过inheritAttrs自动透传到内部 antd 组件 - 多根组件修复 — 自动为多根子组件的 antd 根元素添加
v-bind="$attrs",确保 testid 正确透传 - 自定义 prefixCls — 完美支持 ConfigProvider 的自定义 CSS 类名前缀
- 自定义组件映射 — 支持为第三方 UI 库或自定义组件扩展匹配规则
- 面板全覆盖 — DatePicker (date/month/year/decade)、RangePicker、TimePicker、Cascader、Select、TreeSelect
- 子元素注入 — InputNumber 内部输入框及增减按钮、Slider handle、Rate 星星、Upload file input、Tabs tab/pane
- Menu 子元素 — 基于文本内容自动注入 menu-item、sub-menu、menu-item-group
- 不会覆盖已有 testId — 手动标注了
data-testid的元素会被跳过 - MutationObserver 动态监听 — 面板切换、树节点展开、异步组件加载后自动注入新 DOM
- 一次性注入模式 — SSE/hydration 场景可直接调用,不使用 MutationObserver
安装
pnpm add -D vite-plugin-vue-testid要求:
vite >= 5.0.0,vue >= 3.3.0
快速开始
推荐方式 A(纯运行时),最简单且覆盖最全。如果需要对业务自定义组件生成稳定不变的 testid,选择方式 B。
方式 A:纯运行时(推荐,零编译期配置)
// vite.config.ts — 不需要任何插件配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()]
})// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
const app = createApp(App)
app.mount('#app')
// 在 mount 之后调用一次即可
setupAntTestIds()方式 B:编译期 + 运行时(业务自定义组件需要稳定 testid)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { testIdTransforms } from 'vite-plugin-vue-testid'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
nodeTransforms: testIdTransforms()
}
}
})
]
})// main.ts — 与方式 A 相同
import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
setupAntTestIds()方式 C:仅编译期(面板不注入 testid,不推荐)
// vite.config.ts
import vue from '@vitejs/plugin-vue'
import { testIdTransforms } from 'vite-plugin-vue-testid'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
nodeTransforms: testIdTransforms()
}
}
})
]
})
// main.ts — 不调用 setupAntTestIds()生成的 testId 示例
运行时注入:组件根元素
<!-- 运行时自动匹配 ant-design-vue 组件,全局递增编号 -->
<div class="ant-input-number" data-testid="a-input-number-0">
<div class="ant-select" data-testid="a-select-1" id="a-select-1">
<div class="ant-picker" data-testid="a-date-picker-2" id="a-date-picker-2">
<button class="ant-btn" data-testid="a-button-3">
<input class="ant-input" data-testid="a-input-4">
<span class="ant-slider" data-testid="a-slider-5">运行时注入:组件子元素
<!-- InputNumber:内部 input + 增减按钮 -->
a-input-number-0-input # 内部 <input>
a-input-number-0-up # 增加按钮
a-input-number-0-down # 减少按钮
<!-- Slider:各 handle -->
a-slider-5-handle-0 # 第1个 handle
a-slider-5-handle-1 # 第2个 handle(range 模式)
<!-- Rate:各星星 -->
a-rate-0-star-0 # 第1颗星
a-rate-0-star-1 # 第2颗星
<!-- Upload:内部 file input -->
a-upload-0-input # <input type="file">
<!-- Tabs:标签项和内容面板 -->
a-tabs-0-tab-0-标签一 # 第1个 tab
a-tabs-0-tab-1-标签二 # 第2个 tab
a-tabs-0-pane-0-标签一 # 第1个 pane
a-tabs-0-add # 新增标签按钮(editable tabs)
a-tabs-0-more # 更多按钮运行时注入:DatePicker 面板
a-date-picker-0-dropdown # 面板容器
a-date-picker-0-dropdown-super-prev # 上一年
a-date-picker-0-dropdown-prev # 上一月
a-date-picker-0-dropdown-next # 下一月
a-date-picker-0-dropdown-super-next # 下一年
a-date-picker-0-dropdown-header-date # header 容器
a-date-picker-0-dropdown-header-month-btn # header 月份按钮
a-date-picker-0-dropdown-header-year-btn # header 年份按钮
a-date-picker-0-dropdown-date-2026-06-15 # 日期单元格
a-date-picker-0-dropdown-ok # 确定按钮切换到月/年/年代面板后(MutationObserver 自动重注入):
a-date-picker-0-dropdown-header-month # 月面板 header
a-date-picker-0-dropdown-month-06 # 6月
a-date-picker-0-dropdown-header-year # 年面板 header
a-date-picker-0-dropdown-year-2026 # 2026年
a-date-picker-0-dropdown-decade-2020-2029 # 2020-2029 年代运行时注入:RangePicker 面板
a-range-picker-0-dropdown-super-prev-left # 左侧面板上一年
a-range-picker-0-dropdown-prev-left # 左侧面板上一月
a-range-picker-0-dropdown-next-right # 右侧面板下一月
a-range-picker-0-dropdown-date-2026-05-15-left # 左侧面板日期
a-range-picker-0-dropdown-date-2026-06-20-right # 右侧面板日期运行时注入:TimePicker
a-time-picker-0-dropdown-time-column-0 # 时列
a-time-picker-0-dropdown-time-0-08 # 08时
a-time-picker-0-dropdown-time-column-1 # 分列
a-time-picker-0-dropdown-time-1-30 # 30分运行时注入:Preset 快捷范围
a-date-picker-0-dropdown-preset-本周 # preset 标签
a-date-picker-0-dropdown-preset-本月 # preset 标签运行时注入:Cascader
a-cascader-0-dropdown # 面板容器
a-cascader-0-dropdown-menu-0 # 第1级菜单
a-cascader-0-dropdown-item-浙江 # 浙江选项
a-cascader-0-dropdown-menu-1 # 第2级菜单
a-cascader-0-dropdown-item-杭州 # 杭州选项运行时注入:Select / TreeSelect
a-select-0-dropdown # 下拉容器
a-select-0-dropdown-option-选项1 # 选项
a-select-0-dropdown-option-选项2 # 选项
a-tree-select-0-dropdown # 下拉容器
a-tree-select-0-dropdown-tree-根节点 # 树节点
a-tree-select-0-dropdown-tree-switcher-expand-根节点 # 展开图标(可展开)
a-tree-select-0-dropdown-tree-switcher-collapse-根节点 # 展开图标(已展开)运行时注入:Menu 子元素
a-menu-0 # 菜单容器(由 injectComponentRoots 注入)
a-menu-item-菜单项1 # 菜单项(运行时,基于文本内容)
a-menu-item-子项3 # 子菜单内的菜单项
a-sub-menu-子菜单 # 子菜单
a-menu-item-group-分组标题 # 菜单分组标题编译期注入:业务自定义组件
格式:{tagName}-{perTemplateIndex}
<!-- 源码 -->
<MyInput />
<MySelect />
<MyInput />
<!-- 编译后 -->
<MyInput data-testid="MyInput-0" />
<MySelect data-testid="MySelect-0" />
<MyInput data-testid="MyInput-1" />设计说明:编译期 transform 只对业务自定义组件注入 testid(通过
inheritAttrs透传到内部 antd 组件),不对 antd 组件直接注入。antd 组件的 testid 全部由运行时负责(全局计数器保证唯一)。
配置
编译期配置
import { testIdTransforms } from 'vite-plugin-vue-testid'
testIdTransforms({
/**
* testId 属性名
* @default 'data-testid'
*/
attributeName: 'data-testid',
/**
* ant-design-vue 的 tag 前缀
* @default 'a-'
*/
antPrefix: 'a-',
/**
* 额外需要注入 testid 的自定义组件前缀
* @default []
*/
customPrefixes: ['myapp-', 'el-'],
})运行时配置
import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
const cleanup = setupAntTestIds({
/**
* testId 属性名,需与编译期配置一致
* @default 'data-testid'
*/
attributeName: 'data-testid',
/**
* ant-design-vue CSS 类名前缀,对应 ConfigProvider 的 prefixCls
* @default 'ant'
*/
prefixCls: 'ant',
/**
* 自定义组件 CSS 选择器 → testid 前缀映射
* 会与默认映射合并(自定义覆盖默认)
*
* @example
* components: {
* '.el-input': 'el-input',
* '.el-button': 'el-button',
* }
*/
components: {
'.el-input': 'el-input',
},
/**
* 自定义面板选择器 → 注入策略映射
* key: CSS 选择器
* value: 注入函数,设为 undefined 可禁用某个默认面板
*/
panels: {
'.ant-picker-dropdown': undefined, // 禁用内置 Picker 面板注入
'.my-custom-dropdown': (ctx) => {
const prefix = `${ctx.triggerTestId}-dropdown`
ctx.container.setAttribute(ctx.attrName, prefix)
// ... 自定义注入逻辑
},
},
})
// 组件卸载时清理 Observer
// onUnmounted(cleanup)一次性手动注入
import { injectCurrentDropdowns } from 'vite-plugin-vue-testid/runtime'
// SSR / hydration 场景下,不想用 MutationObserver 时直接调用
injectCurrentDropdowns({ prefixCls: 'ant' })默认支持的组件
运行时(组件根元素 + 子元素 + 面板)
| 组件 | 根元素 testid | 子元素 | 面板 |
|------|:---:|:---:|:---:|
| Input | a-input-N | — | — |
| Input.Search | a-input-search-N | — | — |
| Input.Password (affix-wrapper) | a-input-N | — | — |
| Textarea | a-textarea-N | — | — |
| InputNumber | a-input-number-N | input / up / down | — |
| Select | a-select-N | — | ✅ 下拉面板 |
| Cascader | a-cascader-N | — | ✅ 级联面板 |
| TreeSelect | a-tree-select-N | — | ✅ 树选择面板 |
| DatePicker | a-date-picker-N | — | ✅ 日期/月/年/年代面板 |
| RangePicker | a-range-picker-N | — | ✅ 双面板 + 时间列 |
| TimePicker | a-time-picker-N | — | ✅ 时间列 |
| Radio.Group | a-radio-group-N | — | — |
| Radio | a-radio-N | — | — |
| Checkbox.Group | a-checkbox-group-N | — | — |
| Checkbox | a-checkbox-N | — | — |
| Switch | a-switch-N | — | — |
| Slider | a-slider-N | handle-0 / handle-1 | — |
| Rate | a-rate-N | star-0 / star-1 / ... | — |
| Upload | a-upload-N | file input | — |
| Button | a-button-N | — | — |
| Form.Item | a-form-item-N | — | — |
| Modal | a-modal-N | — | — |
| Menu | a-menu-N | menu-item / sub-menu / group | — |
| Tabs | a-tabs-N | tab / pane / add / more | — |
所有带
id注入的组件(Select、Cascader、DatePicker、RangePicker、TimePicker、Modal、Upload)同时设置id属性,值等于 testid。
编译期(业务自定义组件)
编译期 transform 对满足以下条件的组件注入 testid:
tagType === Component的自定义组件(如<MyInput />)- 匹配
customPrefixes中任一前缀的组件 - 不包括 antd 组件(
a-*),这些由运行时负责
自定义面板注入策略
如果需要支持其他 UI 库的弹出面板,可以自定义策略:
import type { PanelInjectStrategy } from 'vite-plugin-vue-testid/runtime'
import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
// 示例:为 Element Plus 的弹出层注入 testId
const injectElDropdown: PanelInjectStrategy = (ctx) => {
const { container, triggerTestId, attrName } = ctx
const prefix = `${triggerTestId}-dropdown`
container.setAttribute(attrName, prefix)
container.querySelectorAll('.el-select-dropdown__item').forEach((item) => {
const el = item as HTMLElement
const text = el.textContent?.trim() || ''
el.setAttribute(attrName, `${prefix}-option-${text}`)
})
}
setupAntTestIds({
panels: {
'.el-select-dropdown': injectElDropdown,
'.el-cascader-dropdown': injectElDropdown,
},
})API 参考
编译期
| 导出 | 类型 | 说明 |
|------|------|------|
| testIdTransforms(opts?) | () => NodeTransform[] | 返回 [multiRootFixTransform, testIdInjectTransform],注册到 vue() 的 compilerOptions.nodeTransforms |
| createTestIdTransforms(opts?) | () => NodeTransform[] | 同上,完整导出名 |
| createTestIdInjectTransform(opts?) | () => NodeTransform | 仅创建 testid 注入 transform(不含 multi-root fix) |
| createMultiRootFixTransform(opts?) | () => NodeTransform | 仅创建多根组件修复 transform |
| TransformOptions | interface | { attributeName?, antPrefix?, customPrefixes? } |
运行时(/runtime 子路径)
| 导出 | 类型 | 说明 |
|------|------|------|
| setupAntTestIds(opts?) | () => () => void | 启动 MutationObserver,返回清理函数 |
| injectCurrentDropdowns(opts?) | () => void | 一次性手动注入当前 DOM 中已有的组件和面板 |
| RuntimeOptions | interface | { attributeName?, prefixCls?, components?, panels? } |
| PanelContext | interface | { container, trigger, triggerTestId, attrName } |
| PanelInjectStrategy | (ctx: PanelContext) => void | 面板注入策略函数类型 |
Vite 插件
| 导出 | 类型 | 说明 |
|------|------|------|
| vueTestIdPlugin(opts?) | Plugin | Vite 插件(占位符,当前无实际功能) |
已废弃
| 导出 | 替代 |
|------|------|
| setupAntDropdownTestIds | 使用 setupAntTestIds |
| createVueTestIdTransform | 使用 testIdTransforms |
原理
运行时(核心)
setupAntTestIds() 通过 MutationObserver 监听 document.body 的 DOM 变化,对新插入的节点依次执行:
- 组件根注入 — 遍历 CSS 选择器匹配 antd 组件根元素,注入全局递增编号的 testid。同时通过 skip 列表排除组件包装器内部的 input/textarea
- 面板注入 — 检测 teleport 面板容器(picker-dropdown / cascader-dropdown / select-dropdown 等),通过
findActiveTrigger找到触发面板的组件,关联注入 - 子元素注入 — 对 InputNumber / Slider / Rate / Upload / Tabs 的特殊子元素注入带后缀的 testid
- Menu 注入 — 由于 Menu 组件的
inheritAttrs: false,额外遍历 menu-item / sub-menu / menu-item-group 注入基于文本的 testid
面板容器内也使用 MutationObserver 监听子节点变化(如面板模式切换、树节点展开),自动重新注入。
编译期(可选)
通过 Vue 编译器的 nodeTransform 钩子在 AST 阶段为业务自定义组件注入 data-testid="{tagName}-{index}"。由于注入在父模板中完成,testid 通过 Vue 的 fallthroughAttrs 机制自动透传到子组件的根元素。
对于多根组件(Fragment),inheritAttrs 不生效,createMultiRootFixTransform 自动为第一个 antd 组件根元素添加 v-bind="$attrs" 以确保透传。
License
MIT
