vite-plugin-vue-testid 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +302 -198
  2. package/README.zh.md +481 -0
  3. package/package.json +3 -2
  4. package/README.en.md +0 -377
package/README.zh.md ADDED
@@ -0,0 +1,481 @@
1
+ # vite-plugin-vue-testid
2
+
3
+ [English](./README.md)
4
+
5
+ 编译期 + 运行时自动为 Vue 组件模板注入 `data-testid` 的 Vite 插件,专为 ant-design-vue 4.x 适配,大幅降低 E2E 测试中的元素定位成本。
6
+
7
+ ## 特性
8
+
9
+ - **纯运行时全覆盖** — 通过 MutationObserver 为所有 ant-design-vue 组件根元素、子元素、teleport 面板自动注入 `data-testid`,无需编译期配置即可使用
10
+ - **编译期可选增强** — 为业务自定义组件(如 `<MyInput />`)在模板编译期注入唯一稳定的 testid,通过 `inheritAttrs` 自动透传到内部 antd 组件
11
+ - **多根组件修复** — 自动为多根子组件的 antd 根元素添加 `v-bind="$attrs"`,确保 testid 正确透传
12
+ - **自定义 prefixCls** — 完美支持 ConfigProvider 的自定义 CSS 类名前缀
13
+ - **自定义组件映射** — 支持为第三方 UI 库或自定义组件扩展匹配规则
14
+ - **面板全覆盖** — DatePicker (date/month/year/decade)、RangePicker、TimePicker、Cascader、Select、TreeSelect
15
+ - **子元素注入** — InputNumber 内部输入框及增减按钮、Slider handle、Rate 星星、Upload file input、Tabs tab/pane
16
+ - **Menu 子元素** — 基于文本内容自动注入 menu-item、sub-menu、menu-item-group
17
+ - **不会覆盖已有 testId** — 手动标注了 `data-testid` 的元素会被跳过
18
+ - **MutationObserver 动态监听** — 面板切换、树节点展开、异步组件加载后自动注入新 DOM
19
+ - **一次性注入模式** — SSE/hydration 场景可直接调用,不使用 MutationObserver
20
+
21
+ ---
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ pnpm add -D vite-plugin-vue-testid
27
+ ```
28
+
29
+ > 要求:`vite >= 5.0.0`,`vue >= 3.3.0`
30
+
31
+ ---
32
+
33
+ ## 快速开始
34
+
35
+ 推荐**方式 A**(纯运行时),最简单且覆盖最全。如果需要对业务自定义组件生成稳定不变的 testid,选择**方式 B**。
36
+
37
+ ### 方式 A:纯运行时(推荐,零编译期配置)
38
+
39
+ ```ts
40
+ // vite.config.ts — 不需要任何插件配置
41
+ import { defineConfig } from 'vite'
42
+ import vue from '@vitejs/plugin-vue'
43
+
44
+ export default defineConfig({
45
+ plugins: [vue()]
46
+ })
47
+ ```
48
+
49
+ ```ts
50
+ // main.ts
51
+ import { createApp } from 'vue'
52
+ import App from './App.vue'
53
+ import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
54
+
55
+ const app = createApp(App)
56
+ app.mount('#app')
57
+
58
+ // 在 mount 之后调用一次即可
59
+ setupAntTestIds()
60
+ ```
61
+
62
+ ### 方式 B:编译期 + 运行时(业务自定义组件需要稳定 testid)
63
+
64
+ ```ts
65
+ // vite.config.ts
66
+ import { defineConfig } from 'vite'
67
+ import vue from '@vitejs/plugin-vue'
68
+ import { testIdTransforms } from 'vite-plugin-vue-testid'
69
+
70
+ export default defineConfig({
71
+ plugins: [
72
+ vue({
73
+ template: {
74
+ compilerOptions: {
75
+ nodeTransforms: testIdTransforms()
76
+ }
77
+ }
78
+ })
79
+ ]
80
+ })
81
+ ```
82
+
83
+ ```ts
84
+ // main.ts — 与方式 A 相同
85
+ import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
86
+ setupAntTestIds()
87
+ ```
88
+
89
+ ### 方式 C:仅编译期(面板不注入 testid,不推荐)
90
+
91
+ ```ts
92
+ // vite.config.ts
93
+ import vue from '@vitejs/plugin-vue'
94
+ import { testIdTransforms } from 'vite-plugin-vue-testid'
95
+
96
+ export default defineConfig({
97
+ plugins: [
98
+ vue({
99
+ template: {
100
+ compilerOptions: {
101
+ nodeTransforms: testIdTransforms()
102
+ }
103
+ }
104
+ })
105
+ ]
106
+ })
107
+ // main.ts — 不调用 setupAntTestIds()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 生成的 testId 示例
113
+
114
+ ### 运行时注入:组件根元素
115
+
116
+ ```html
117
+ <!-- 运行时自动匹配 ant-design-vue 组件,全局递增编号 -->
118
+ <div class="ant-input-number" data-testid="a-input-number-0">
119
+ <div class="ant-select" data-testid="a-select-1" id="a-select-1">
120
+ <div class="ant-picker" data-testid="a-date-picker-2" id="a-date-picker-2">
121
+ <button class="ant-btn" data-testid="a-button-3">
122
+ <input class="ant-input" data-testid="a-input-4">
123
+ <span class="ant-slider" data-testid="a-slider-5">
124
+ ```
125
+
126
+ ### 运行时注入:组件子元素
127
+
128
+ ```
129
+ <!-- InputNumber:内部 input + 增减按钮 -->
130
+ a-input-number-0-input # 内部 <input>
131
+ a-input-number-0-up # 增加按钮
132
+ a-input-number-0-down # 减少按钮
133
+
134
+ <!-- Slider:各 handle -->
135
+ a-slider-5-handle-0 # 第1个 handle
136
+ a-slider-5-handle-1 # 第2个 handle(range 模式)
137
+
138
+ <!-- Rate:各星星 -->
139
+ a-rate-0-star-0 # 第1颗星
140
+ a-rate-0-star-1 # 第2颗星
141
+
142
+ <!-- Upload:内部 file input -->
143
+ a-upload-0-input # <input type="file">
144
+
145
+ <!-- Tabs:标签项和内容面板 -->
146
+ a-tabs-0-tab-0-标签一 # 第1个 tab
147
+ a-tabs-0-tab-1-标签二 # 第2个 tab
148
+ a-tabs-0-pane-0-标签一 # 第1个 pane
149
+ a-tabs-0-add # 新增标签按钮(editable tabs)
150
+ a-tabs-0-more # 更多按钮
151
+ ```
152
+
153
+ ### 运行时注入:DatePicker 面板
154
+
155
+ ```
156
+ a-date-picker-0-dropdown # 面板容器
157
+ a-date-picker-0-dropdown-super-prev # 上一年
158
+ a-date-picker-0-dropdown-prev # 上一月
159
+ a-date-picker-0-dropdown-next # 下一月
160
+ a-date-picker-0-dropdown-super-next # 下一年
161
+ a-date-picker-0-dropdown-header-date # header 容器
162
+ a-date-picker-0-dropdown-header-month-btn # header 月份按钮
163
+ a-date-picker-0-dropdown-header-year-btn # header 年份按钮
164
+ a-date-picker-0-dropdown-date-2026-06-15 # 日期单元格
165
+ a-date-picker-0-dropdown-ok # 确定按钮
166
+ ```
167
+
168
+ 切换到月/年/年代面板后(MutationObserver 自动重注入):
169
+
170
+ ```
171
+ a-date-picker-0-dropdown-header-month # 月面板 header
172
+ a-date-picker-0-dropdown-month-06 # 6月
173
+ a-date-picker-0-dropdown-header-year # 年面板 header
174
+ a-date-picker-0-dropdown-year-2026 # 2026年
175
+ a-date-picker-0-dropdown-decade-2020-2029 # 2020-2029 年代
176
+ ```
177
+
178
+ ### 运行时注入:RangePicker 面板
179
+
180
+ ```
181
+ a-range-picker-0-dropdown-super-prev-left # 左侧面板上一年
182
+ a-range-picker-0-dropdown-prev-left # 左侧面板上一月
183
+ a-range-picker-0-dropdown-next-right # 右侧面板下一月
184
+ a-range-picker-0-dropdown-date-2026-05-15-left # 左侧面板日期
185
+ a-range-picker-0-dropdown-date-2026-06-20-right # 右侧面板日期
186
+ ```
187
+
188
+ ### 运行时注入:TimePicker
189
+
190
+ ```
191
+ a-time-picker-0-dropdown-time-column-0 # 时列
192
+ a-time-picker-0-dropdown-time-0-08 # 08时
193
+ a-time-picker-0-dropdown-time-column-1 # 分列
194
+ a-time-picker-0-dropdown-time-1-30 # 30分
195
+ ```
196
+
197
+ ### 运行时注入:Preset 快捷范围
198
+
199
+ ```
200
+ a-date-picker-0-dropdown-preset-本周 # preset 标签
201
+ a-date-picker-0-dropdown-preset-本月 # preset 标签
202
+ ```
203
+
204
+ ### 运行时注入:Cascader
205
+
206
+ ```
207
+ a-cascader-0-dropdown # 面板容器
208
+ a-cascader-0-dropdown-menu-0 # 第1级菜单
209
+ a-cascader-0-dropdown-item-浙江 # 浙江选项
210
+ a-cascader-0-dropdown-menu-1 # 第2级菜单
211
+ a-cascader-0-dropdown-item-杭州 # 杭州选项
212
+ ```
213
+
214
+ ### 运行时注入:Select / TreeSelect
215
+
216
+ ```
217
+ a-select-0-dropdown # 下拉容器
218
+ a-select-0-dropdown-option-选项1 # 选项
219
+ a-select-0-dropdown-option-选项2 # 选项
220
+
221
+ a-tree-select-0-dropdown # 下拉容器
222
+ a-tree-select-0-dropdown-tree-根节点 # 树节点
223
+ a-tree-select-0-dropdown-tree-switcher-expand-根节点 # 展开图标(可展开)
224
+ a-tree-select-0-dropdown-tree-switcher-collapse-根节点 # 展开图标(已展开)
225
+ ```
226
+
227
+ ### 运行时注入:Menu 子元素
228
+
229
+ ```
230
+ a-menu-0 # 菜单容器(由 injectComponentRoots 注入)
231
+ a-menu-item-菜单项1 # 菜单项(运行时,基于文本内容)
232
+ a-menu-item-子项3 # 子菜单内的菜单项
233
+ a-sub-menu-子菜单 # 子菜单
234
+ a-menu-item-group-分组标题 # 菜单分组标题
235
+ ```
236
+
237
+ ### 编译期注入:业务自定义组件
238
+
239
+ ```
240
+ 格式:{tagName}-{perTemplateIndex}
241
+
242
+ <!-- 源码 -->
243
+ <MyInput />
244
+ <MySelect />
245
+ <MyInput />
246
+
247
+ <!-- 编译后 -->
248
+ <MyInput data-testid="MyInput-0" />
249
+ <MySelect data-testid="MySelect-0" />
250
+ <MyInput data-testid="MyInput-1" />
251
+ ```
252
+
253
+ > **设计说明**:编译期 transform 只对**业务自定义组件**注入 testid(通过 `inheritAttrs` 透传到内部 antd 组件),不对 antd 组件直接注入。antd 组件的 testid 全部由运行时负责(全局计数器保证唯一)。
254
+
255
+ ---
256
+
257
+ ## 配置
258
+
259
+ ### 编译期配置
260
+
261
+ ```ts
262
+ import { testIdTransforms } from 'vite-plugin-vue-testid'
263
+
264
+ testIdTransforms({
265
+ /**
266
+ * testId 属性名
267
+ * @default 'data-testid'
268
+ */
269
+ attributeName: 'data-testid',
270
+
271
+ /**
272
+ * ant-design-vue 的 tag 前缀
273
+ * @default 'a-'
274
+ */
275
+ antPrefix: 'a-',
276
+
277
+ /**
278
+ * 额外需要注入 testid 的自定义组件前缀
279
+ * @default []
280
+ */
281
+ customPrefixes: ['myapp-', 'el-'],
282
+ })
283
+ ```
284
+
285
+ ### 运行时配置
286
+
287
+ ```ts
288
+ import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
289
+
290
+ const cleanup = setupAntTestIds({
291
+ /**
292
+ * testId 属性名,需与编译期配置一致
293
+ * @default 'data-testid'
294
+ */
295
+ attributeName: 'data-testid',
296
+
297
+ /**
298
+ * ant-design-vue CSS 类名前缀,对应 ConfigProvider 的 prefixCls
299
+ * @default 'ant'
300
+ */
301
+ prefixCls: 'ant',
302
+
303
+ /**
304
+ * 自定义组件 CSS 选择器 → testid 前缀映射
305
+ * 会与默认映射合并(自定义覆盖默认)
306
+ *
307
+ * @example
308
+ * components: {
309
+ * '.el-input': 'el-input',
310
+ * '.el-button': 'el-button',
311
+ * }
312
+ */
313
+ components: {
314
+ '.el-input': 'el-input',
315
+ },
316
+
317
+ /**
318
+ * 自定义面板选择器 → 注入策略映射
319
+ * key: CSS 选择器
320
+ * value: 注入函数,设为 undefined 可禁用某个默认面板
321
+ */
322
+ panels: {
323
+ '.ant-picker-dropdown': undefined, // 禁用内置 Picker 面板注入
324
+ '.my-custom-dropdown': (ctx) => {
325
+ const prefix = `${ctx.triggerTestId}-dropdown`
326
+ ctx.container.setAttribute(ctx.attrName, prefix)
327
+ // ... 自定义注入逻辑
328
+ },
329
+ },
330
+ })
331
+
332
+ // 组件卸载时清理 Observer
333
+ // onUnmounted(cleanup)
334
+ ```
335
+
336
+ ### 一次性手动注入
337
+
338
+ ```ts
339
+ import { injectCurrentDropdowns } from 'vite-plugin-vue-testid/runtime'
340
+
341
+ // SSR / hydration 场景下,不想用 MutationObserver 时直接调用
342
+ injectCurrentDropdowns({ prefixCls: 'ant' })
343
+ ```
344
+
345
+ ---
346
+
347
+ ## 默认支持的组件
348
+
349
+ ### 运行时(组件根元素 + 子元素 + 面板)
350
+
351
+ | 组件 | 根元素 testid | 子元素 | 面板 |
352
+ |------|:---:|:---:|:---:|
353
+ | Input | `a-input-N` | — | — |
354
+ | Input.Search | `a-input-search-N` | — | — |
355
+ | Input.Password (affix-wrapper) | `a-input-N` | — | — |
356
+ | Textarea | `a-textarea-N` | — | — |
357
+ | InputNumber | `a-input-number-N` | input / up / down | — |
358
+ | Select | `a-select-N` | — | ✅ 下拉面板 |
359
+ | Cascader | `a-cascader-N` | — | ✅ 级联面板 |
360
+ | TreeSelect | `a-tree-select-N` | — | ✅ 树选择面板 |
361
+ | DatePicker | `a-date-picker-N` | — | ✅ 日期/月/年/年代面板 |
362
+ | RangePicker | `a-range-picker-N` | — | ✅ 双面板 + 时间列 |
363
+ | TimePicker | `a-time-picker-N` | — | ✅ 时间列 |
364
+ | Radio.Group | `a-radio-group-N` | — | — |
365
+ | Radio | `a-radio-N` | — | — |
366
+ | Checkbox.Group | `a-checkbox-group-N` | — | — |
367
+ | Checkbox | `a-checkbox-N` | — | — |
368
+ | Switch | `a-switch-N` | — | — |
369
+ | Slider | `a-slider-N` | handle-0 / handle-1 | — |
370
+ | Rate | `a-rate-N` | star-0 / star-1 / ... | — |
371
+ | Upload | `a-upload-N` | file input | — |
372
+ | Button | `a-button-N` | — | — |
373
+ | Form.Item | `a-form-item-N` | — | — |
374
+ | Modal | `a-modal-N` | — | — |
375
+ | Menu | `a-menu-N` | menu-item / sub-menu / group | — |
376
+ | Tabs | `a-tabs-N` | tab / pane / add / more | — |
377
+
378
+ > 所有带 `id` 注入的组件(Select、Cascader、DatePicker、RangePicker、TimePicker、Modal、Upload)同时设置 `id` 属性,值等于 testid。
379
+
380
+ ### 编译期(业务自定义组件)
381
+
382
+ 编译期 transform 对满足以下条件的组件注入 testid:
383
+ - `tagType === Component` 的自定义组件(如 `<MyInput />`)
384
+ - 匹配 `customPrefixes` 中任一前缀的组件
385
+ - **不包括** antd 组件(`a-*`),这些由运行时负责
386
+
387
+ ---
388
+
389
+ ## 自定义面板注入策略
390
+
391
+ 如果需要支持其他 UI 库的弹出面板,可以自定义策略:
392
+
393
+ ```ts
394
+ import type { PanelInjectStrategy } from 'vite-plugin-vue-testid/runtime'
395
+ import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
396
+
397
+ // 示例:为 Element Plus 的弹出层注入 testId
398
+ const injectElDropdown: PanelInjectStrategy = (ctx) => {
399
+ const { container, triggerTestId, attrName } = ctx
400
+ const prefix = `${triggerTestId}-dropdown`
401
+
402
+ container.setAttribute(attrName, prefix)
403
+
404
+ container.querySelectorAll('.el-select-dropdown__item').forEach((item) => {
405
+ const el = item as HTMLElement
406
+ const text = el.textContent?.trim() || ''
407
+ el.setAttribute(attrName, `${prefix}-option-${text}`)
408
+ })
409
+ }
410
+
411
+ setupAntTestIds({
412
+ panels: {
413
+ '.el-select-dropdown': injectElDropdown,
414
+ '.el-cascader-dropdown': injectElDropdown,
415
+ },
416
+ })
417
+ ```
418
+
419
+ ---
420
+
421
+ ## API 参考
422
+
423
+ ### 编译期
424
+
425
+ | 导出 | 类型 | 说明 |
426
+ |------|------|------|
427
+ | `testIdTransforms(opts?)` | `() => NodeTransform[]` | 返回 `[multiRootFixTransform, testIdInjectTransform]`,注册到 `vue()` 的 `compilerOptions.nodeTransforms` |
428
+ | `createTestIdTransforms(opts?)` | `() => NodeTransform[]` | 同上,完整导出名 |
429
+ | `createTestIdInjectTransform(opts?)` | `() => NodeTransform` | 仅创建 testid 注入 transform(不含 multi-root fix) |
430
+ | `createMultiRootFixTransform(opts?)` | `() => NodeTransform` | 仅创建多根组件修复 transform |
431
+ | `TransformOptions` | `interface` | `{ attributeName?, antPrefix?, customPrefixes? }` |
432
+
433
+ ### 运行时(`/runtime` 子路径)
434
+
435
+ | 导出 | 类型 | 说明 |
436
+ |------|------|------|
437
+ | `setupAntTestIds(opts?)` | `() => () => void` | 启动 MutationObserver,返回清理函数 |
438
+ | `injectCurrentDropdowns(opts?)` | `() => void` | 一次性手动注入当前 DOM 中已有的组件和面板 |
439
+ | `RuntimeOptions` | `interface` | `{ attributeName?, prefixCls?, components?, panels? }` |
440
+ | `PanelContext` | `interface` | `{ container, trigger, triggerTestId, attrName }` |
441
+ | `PanelInjectStrategy` | `(ctx: PanelContext) => void` | 面板注入策略函数类型 |
442
+
443
+ ### Vite 插件
444
+
445
+ | 导出 | 类型 | 说明 |
446
+ |------|------|------|
447
+ | `vueTestIdPlugin(opts?)` | `Plugin` | Vite 插件(占位符,当前无实际功能) |
448
+
449
+ ### 已废弃
450
+
451
+ | 导出 | 替代 |
452
+ |------|------|
453
+ | `setupAntDropdownTestIds` | 使用 `setupAntTestIds` |
454
+ | `createVueTestIdTransform` | 使用 `testIdTransforms` |
455
+
456
+ ---
457
+
458
+ ## 原理
459
+
460
+ ### 运行时(核心)
461
+
462
+ `setupAntTestIds()` 通过 `MutationObserver` 监听 `document.body` 的 DOM 变化,对新插入的节点依次执行:
463
+
464
+ 1. **组件根注入** — 遍历 CSS 选择器匹配 antd 组件根元素,注入全局递增编号的 testid。同时通过 skip 列表排除组件包装器内部的 input/textarea
465
+ 2. **面板注入** — 检测 teleport 面板容器(picker-dropdown / cascader-dropdown / select-dropdown 等),通过 `findActiveTrigger` 找到触发面板的组件,关联注入
466
+ 3. **子元素注入** — 对 InputNumber / Slider / Rate / Upload / Tabs 的特殊子元素注入带后缀的 testid
467
+ 4. **Menu 注入** — 由于 Menu 组件的 `inheritAttrs: false`,额外遍历 menu-item / sub-menu / menu-item-group 注入基于文本的 testid
468
+
469
+ 面板容器内也使用 MutationObserver 监听子节点变化(如面板模式切换、树节点展开),自动重新注入。
470
+
471
+ ### 编译期(可选)
472
+
473
+ 通过 Vue 编译器的 `nodeTransform` 钩子在 AST 阶段为业务自定义组件注入 `data-testid="{tagName}-{index}"`。由于注入在父模板中完成,testid 通过 Vue 的 `fallthroughAttrs` 机制自动透传到子组件的根元素。
474
+
475
+ 对于多根组件(Fragment),`inheritAttrs` 不生效,`createMultiRootFixTransform` 自动为第一个 antd 组件根元素添加 `v-bind="$attrs"` 以确保透传。
476
+
477
+ ---
478
+
479
+ ## License
480
+
481
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-vue-testid",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Vite plugin to auto-inject data-testid into Vue component templates for E2E testing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
- "README.en.md"
21
+ "README.md",
22
+ "README.zh.md"
22
23
  ],
23
24
  "scripts": {
24
25
  "build": "tsup && tsc -p tsconfig.build.json",