vite-plugin-vue-testid
v1.0.3
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 测试中的元素定位成本。
特性
- 编译期注入 — 通过 Vue 编译器
nodeTransform自动为组件模板添加data-testid - 运行时注入 — 为 ant-design-vue teleport 弹出层(DatePicker、Cascader、Select、TreeSelect 等)动态注入 testId
- 适配 — bodyCell 插槽内组件自动生成「列名 + 行号 + 序号」的唯一 testId
- 自动补全 index 解构 — 即使
#bodyCell="{ column, record }"未写index,插件也会自动补上 - 不会覆盖已有 testId — 手动标注了
data-testid的元素会被跳过 - teleport 组件双注入 — DatePicker / RangePicker / Modal 同时注入
id+data-testid - 日期面板模式感知 — date / month / year / decade 面板自动区分
- MutationObserver 重注入 — 面板切换、树节点展开/折叠时自动检测并注入新 DOM
- 可扩展 — 支持自定义组件列表、testId 生成规则、弹出面板注入策略
安装
pnpm add -D vite-plugin-vue-testid要求:
vite >= 5.0.0,vue >= 3.3.0,@vue/compiler-core >= 3.5.0
快速开始
1. 编译期注入(vite.config.ts)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createVueTestIdTransform } from 'vite-plugin-vue-testid'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
nodeTransforms: [createVueTestIdTransform()]
}
}
})
]
})2. 运行时注入(main.ts)
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { setupAntDropdownTestIds } from 'vite-plugin-vue-testid/runtime'
const app = createApp(App)
app.mount('#app')
// 在 mount 之后调用,监听 teleport 面板并自动注入 testId
setupAntDropdownTestIds()生成的 testId 示例
普通组件
<!-- 源码 -->
<a-input />
<a-select />
<a-button />
<!-- 编译后 -->
<a-input data-testid="a-input-0" />
<a-select data-testid="a-select-1" />
<a-button data-testid="a-button-2" />teleport 组件(双注入)
<!-- 源码 -->
<a-date-picker />
<a-modal v-model:visible="show" title="提示" />
<!-- 编译后 -->
<a-date-picker id="a-date-picker-0" data-testid="a-date-picker-0" />
<a-modal id="a-modal-1" data-testid="a-modal-1" />bodyCell 内组件
格式:${column.dataIndex}-${index}-${组件名}-${序号}
示例:
- label-0-a-input-4 (label 列,第 0 行)
- value-2-a-input-5 (value 列,第 2 行)运行时 DatePicker 面板
a-date-picker-0-dropdown # 面板容器
a-date-picker-0-dropdown-prev # 上一月
a-date-picker-0-dropdown-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-prev-left # 左侧面板上一月
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-08 # 08时
a-time-picker-0-dropdown-time-column-1 # 分列
a-time-picker-0-dropdown-time-30 # 30分运行时 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
a-select-0-dropdown # 下拉容器
a-select-0-dropdown-option-选项1 # 选项
a-select-0-dropdown-option-选项2 # 选项运行时 TreeSelect
a-tree-select-0-dropdown # 下拉容器
a-tree-select-0-dropdown-tree-root-1 # 根节点
a-tree-select-0-dropdown-tree-switcher-expand-root-1 # 展开图标(可展开状态)
a-tree-select-0-dropdown-tree-switcher-collapse-root-1 # 展开图标(已展开状态)
a-tree-select-0-dropdown-tree-parent-1 # 父节点(展开后注入)
a-tree-select-0-dropdown-tree-my-leaf # 叶子节点配置
编译期配置
import { createVueTestIdTransform } from 'vite-plugin-vue-testid'
createVueTestIdTransform({
/**
* 需要注入 testId 的组件标签列表(kebab-case)
* @default 见下方「默认支持的组件」
*/
components: ['a-input', 'a-select', 'my-button'],
/**
* 自定义 testId 生成函数
* @param tagName 组件标签名
* @param count 全局递增序号
* @returns testId 字符串
*/
generateId: (tagName, count) => `myapp-${tagName}-${count}`,
/**
* testId 属性名(可改为 data-cy 适配 Cypress)
* @default 'data-testid'
*/
attributeName: 'data-cy',
/**
* 需要同时注入 id + testId 的组件
* @default ['a-date-picker', 'a-range-picker', 'a-time-picker', 'a-modal']
*/
idComponents: ['a-date-picker', 'a-modal'],
/**
* id 属性名
* @default 'id'
*/
idAttributeName: 'id',
})运行时配置
import { setupAntDropdownTestIds } from 'vite-plugin-vue-testid/runtime'
// 默认配置
const cleanup = setupAntDropdownTestIds()
// 自定义配置
const cleanup = setupAntDropdownTestIds({
/**
* testId 属性名,需与编译期配置一致
* @default 'data-testid'
*/
attributeName: 'data-testid',
/**
* 自定义面板选择器 → 注入策略映射
* key: CSS 选择器
* value: 注入函数,设为 undefined 可禁用某个默认面板
*/
panels: {
'.ant-picker-dropdown': undefined, // 禁用内置 Picker 面板注入
'.my-custom-dropdown': (ctx) => {
// ctx.container — 面板容器 HTMLElement
// ctx.trigger — 触发器 HTMLElement | null
// ctx.triggerTestId — 触发器的 testId 值
// ctx.attrName — testId 属性名
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({ attributeName: 'data-testid' })默认支持的组件
| 组件 | 编译期注入 | 运行时注入 |
|------|:---------:|:---------:|
| a-input | ✅ | — |
| a-textarea | ✅ | — |
| a-input-number | ✅ | — |
| a-select | ✅ | ✅ 下拉面板 |
| a-cascader | ✅ | ✅ 级联面板 |
| a-tree-select | ✅ | ✅ 树选择面板 |
| a-radio-group | ✅ | — |
| a-checkbox-group | ✅ | — |
| a-date-picker | ✅ id+testid | ✅ 日期面板 |
| a-range-picker | ✅ id+testid | ✅ 双面板 |
| a-time-picker | ✅ id+testid | ✅ 时间面板 |
| a-switch | ✅ | — |
| a-slider | ✅ | — |
| a-rate | ✅ | — |
| a-upload | ✅ | — |
| a-form-item | ✅ | — |
| a-button | ✅ | — |
| a-modal | ✅ id+testid | — |
自定义面板注入策略
如果需要支持其他 UI 库的弹出面板,可以自定义策略:
import type { PanelInjectStrategy } 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}`)
})
}
setupAntDropdownTestIds({
panels: {
'.el-select-dropdown': injectElDropdown,
'.el-cascader-dropdown': injectElDropdown,
},
})API 参考
编译期
| 导出 | 类型 | 说明 |
|------|------|------|
| createVueTestIdTransform(opts?) | (node: RootNode \| TemplateChildNode) => void | 创建 Vue 编译器 nodeTransform |
| VueTestIdTransformOptions | interface | 编译期配置类型 |
运行时(/runtime 子路径)
| 导出 | 类型 | 说明 |
|------|------|------|
| setupAntDropdownTestIds(opts?) | () => void | 启动 MutationObserver,自动注入弹出面板 testId,返回清理函数 |
| injectCurrentDropdowns(opts?) | void | 一次性手动注入当前 DOM 中已有的弹出面板 |
| RuntimeOptions | interface | 运行时配置类型 |
| PanelContext | interface | 面板注入上下文类型 |
| PanelInjectStrategy | (ctx: PanelContext) => void | 面板注入策略函数类型 |
原理
编译期
通过 Vue 编译器的 nodeTransform 钩子在 AST 阶段遍历模板节点,匹配目标组件标签后直接追加 data-testid 属性节点。由于是编译期注入,对运行时没有性能影响。
运行时
ant-design-vue 4.x 的 DatePicker / Select / Cascader 等组件使用 Teleport 将弹出层渲染到 document.body,编译期无法触及这些 DOM。运行时模块通过 MutationObserver 监听 body 下的 DOM 变化,匹配 ant-design 面板的 CSS class,自动为面板内元素注入与触发器关联的 testId。
对于 DatePicker 这种 inheritAttrs: false 的组件,data-testid 在编译期注入后无法到达 DOM。插件通过同时注入 id 属性(该属性被 ant-design-vue 显式转发到内部 <input>)作为兜底标识。
License
MIT
