vite-plugin-vue-testid 1.1.3 → 1.1.5
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.
- package/README.md +147 -23
- package/dist/index.d.ts +51 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +293 -17
- package/dist/plugin.d.ts +34 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +27 -0
- package/dist/runtime.d.ts +5 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +203 -2
- package/dist/transform.d.ts +64 -21
- package/dist/transform.d.ts.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -6,15 +6,21 @@ A Vite plugin that auto-injects `data-testid` into Vue component templates via c
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
+
- **Compile-time three-layer injection** — Prioritized counter architecture ensures stable, unique testids:
|
|
10
|
+
- **v-for dynamic injection** (最高) — uses loop index variable for per-iteration uniqueness without runtime dedup
|
|
11
|
+
- **Conditional block sub-counters** (第二) — v-if/v-else/v-show blocks own independent counters, child testids prefixed with parent's testid for stability
|
|
12
|
+
- **Global counter** (第三) — cross-template shared counter with used-id set for uniqueness
|
|
9
13
|
- **Full runtime coverage** — MutationObserver-based injection for all ant-design-vue component roots, sub-elements, and teleported panels, no compile-time config required
|
|
10
|
-
- **
|
|
14
|
+
- **Vue plugin bridge** — `createTestIdBridge()` reads compile-time testid from VNode.props and writes to `$el`, bypassing `inheritAttrs: false`
|
|
11
15
|
- **Multi-root component fix** — Auto-adds `v-bind="$attrs"` to the first antd root element in multi-root child components, ensuring testid pass-through
|
|
12
16
|
- **Custom prefixCls** — Full support for ConfigProvider's custom CSS class prefix
|
|
13
17
|
- **Custom component mapping** — Extendable matcher rules for third-party UI libraries or custom components
|
|
14
18
|
- **Full panel coverage** — DatePicker (date/month/year/decade), RangePicker, TimePicker, Cascader, Select, TreeSelect
|
|
15
19
|
- **Sub-element injection** — InputNumber internal input & up/down buttons, Slider handles, Rate stars, Upload file input, Tabs tabs/panes
|
|
16
20
|
- **Menu sub-elements** — Text-based testids for menu-item, sub-menu, and menu-item-group
|
|
17
|
-
- **
|
|
21
|
+
- **Event listener marking** — `{tag}-event-{names}-{n}` format for elements with event handlers, making interactive elements easy to locate
|
|
22
|
+
- **Configurable prefix** — `testIdPrefix` option to prepend a common prefix to all compile-time testids
|
|
23
|
+
- **Preserves existing testIds** — Elements with manually assigned `data-testid` are skipped (all 3 layers are idempotent)
|
|
18
24
|
- **MutationObserver dynamic watch** — Panel switching, tree node expand/collapse, and async component loading all auto-trigger re-injection
|
|
19
25
|
- **One-shot injection mode** — SSR/hydration friendly, call directly without MutationObserver
|
|
20
26
|
|
|
@@ -59,31 +65,42 @@ app.mount('#app')
|
|
|
59
65
|
setupAntTestIds()
|
|
60
66
|
```
|
|
61
67
|
|
|
62
|
-
### Mode B: Compile-time + Runtime (for
|
|
68
|
+
### Mode B: Compile-time + Runtime (recommended for stable, predictable testids)
|
|
63
69
|
|
|
64
70
|
```ts
|
|
65
71
|
// vite.config.ts
|
|
66
72
|
import { defineConfig } from 'vite'
|
|
67
73
|
import vue from '@vitejs/plugin-vue'
|
|
68
|
-
import { testIdTransforms } from 'vite-plugin-vue-testid'
|
|
74
|
+
import { testIdTransforms, vueTestIdPlugin } from 'vite-plugin-vue-testid'
|
|
69
75
|
|
|
70
76
|
export default defineConfig({
|
|
71
77
|
plugins: [
|
|
72
78
|
vue({
|
|
73
79
|
template: {
|
|
74
80
|
compilerOptions: {
|
|
75
|
-
nodeTransforms: testIdTransforms(
|
|
81
|
+
nodeTransforms: testIdTransforms({
|
|
82
|
+
testIdPrefix: 'static-', // optional: common prefix
|
|
83
|
+
debug: false, // optional: debug logging
|
|
84
|
+
})
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
|
-
})
|
|
87
|
+
}),
|
|
88
|
+
vueTestIdPlugin(),
|
|
79
89
|
]
|
|
80
90
|
})
|
|
81
91
|
```
|
|
82
92
|
|
|
83
93
|
```ts
|
|
84
|
-
// main.ts
|
|
94
|
+
// main.ts
|
|
95
|
+
import { createApp } from 'vue'
|
|
96
|
+
import App from './App.vue'
|
|
97
|
+
import { createTestIdBridge } from 'vite-plugin-vue-testid/plugin'
|
|
85
98
|
import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
|
|
86
|
-
|
|
99
|
+
|
|
100
|
+
const app = createApp(App)
|
|
101
|
+
app.use(createTestIdBridge()) // compile-time testid → DOM
|
|
102
|
+
app.mount('#app')
|
|
103
|
+
setupAntTestIds() // panels / sub-elements / fallback
|
|
87
104
|
```
|
|
88
105
|
|
|
89
106
|
### Mode C: Compile-time only (panels not injected, not recommended)
|
|
@@ -234,23 +251,73 @@ a-sub-menu-Submenu # submenu
|
|
|
234
251
|
a-menu-item-group-Group-Title # menu item group
|
|
235
252
|
```
|
|
236
253
|
|
|
237
|
-
### Compile-time:
|
|
254
|
+
### Compile-time: Three-Layer Injection
|
|
255
|
+
|
|
256
|
+
The compile-time transform uses three priority-ordered injection layers:
|
|
257
|
+
|
|
258
|
+
| Priority | Layer | Format | Example |
|
|
259
|
+
|----------|-------|--------|---------|
|
|
260
|
+
| 1st (highest) | v-for with index | `{tag}_in_for-{i0}[_{i1}]` | `a-card_in_for-0_1` |
|
|
261
|
+
| 2nd | Conditional block child | `{parentTestId}__{tag}-{n}` | `a-button-1__a-input-0` |
|
|
262
|
+
| 3rd | Global counter | `{tag}-{n}` | `a-input-2` |
|
|
238
263
|
|
|
264
|
+
#### v-for Dynamic Injection
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<!-- Source -->
|
|
268
|
+
<a-card v-for="(item, i) in list" :key="i"> ... </a-card>
|
|
269
|
+
|
|
270
|
+
<!-- Compiled (dynamic expression, index auto-resolves per iteration) -->
|
|
271
|
+
<a-card v-for="(item, i) in list" :key="i" data-testid="a-card_in_for-{{i}}"> ... </a-card>
|
|
272
|
+
|
|
273
|
+
<!-- Without index variable → static suffix -->
|
|
274
|
+
<!-- Source -->
|
|
275
|
+
<a-card v-for="item in list" :key="item.id"> ... </a-card>
|
|
276
|
+
<!-- Compiled -->
|
|
277
|
+
<a-card v-for="item in list" :key="item.id" data-testid="a-card-in_for-0"> ... </a-card>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Conditional Block Sub-counters
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<!-- Source -->
|
|
284
|
+
<a-card v-if="visible">
|
|
285
|
+
<a-input />
|
|
286
|
+
<a-button>Submit</a-button>
|
|
287
|
+
</a-card>
|
|
288
|
+
<a-card v-else>
|
|
289
|
+
<a-input />
|
|
290
|
+
</a-card>
|
|
291
|
+
|
|
292
|
+
<!-- Compiled (each conditional branch gets independent sub-counter) -->
|
|
293
|
+
<a-card v-if="visible" data-testid="a-card-0">
|
|
294
|
+
<a-input data-testid="a-card-0__a-input-0" />
|
|
295
|
+
<a-button data-testid="a-card-0__a-button-event-click-0">Submit</a-button>
|
|
296
|
+
</a-card>
|
|
297
|
+
<a-card v-else data-testid="a-card-1">
|
|
298
|
+
<a-input data-testid="a-card-1__a-input-0" />
|
|
299
|
+
</a-card>
|
|
239
300
|
```
|
|
240
|
-
Format: {tagName}-{perTemplateIndex}
|
|
241
301
|
|
|
302
|
+
Child elements inside v-if/v-else/v-show blocks use `{parentTestId}__{tag}-{n}` format, ensuring:
|
|
303
|
+
- Testids are **stable** — parent element count changes don't affect child indices
|
|
304
|
+
- **Predictable** — testers can locate nested elements by chaining parent→child testids
|
|
305
|
+
|
|
306
|
+
#### Global Counter
|
|
307
|
+
|
|
308
|
+
```html
|
|
242
309
|
<!-- Source -->
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
<
|
|
310
|
+
<a-input placeholder="Name" />
|
|
311
|
+
<a-button>Save</a-button>
|
|
312
|
+
<a-input placeholder="Email" />
|
|
246
313
|
|
|
247
314
|
<!-- Compiled -->
|
|
248
|
-
<
|
|
249
|
-
<
|
|
250
|
-
<
|
|
315
|
+
<a-input placeholder="Name" data-testid="a-input-0" />
|
|
316
|
+
<a-button data-testid="a-button-event-click-0">Save</a-button>
|
|
317
|
+
<a-input placeholder="Email" data-testid="a-input-1" />
|
|
251
318
|
```
|
|
252
319
|
|
|
253
|
-
> **Design note**:
|
|
320
|
+
> **Design note**: Global counters are shared across all templates, with a used-id Set to skip already-occupied testids. Conditional block sub-counters are independent — adding/removing elements within a v-if block does not shift other elements' testids.
|
|
254
321
|
|
|
255
322
|
---
|
|
256
323
|
|
|
@@ -279,6 +346,45 @@ testIdTransforms({
|
|
|
279
346
|
* @default []
|
|
280
347
|
*/
|
|
281
348
|
customPrefixes: ['myapp-', 'el-'],
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Common prefix prepended to all compile-time testids
|
|
352
|
+
* e.g., 'static-' produces 'static-a-button-0'
|
|
353
|
+
* @default ''
|
|
354
|
+
*/
|
|
355
|
+
testIdPrefix: 'static-',
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Enable v-for dynamic injection (requires index variable)
|
|
359
|
+
* @default true
|
|
360
|
+
*/
|
|
361
|
+
injectForLoops: true,
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Enable event listener marking on elements with @click, @input, etc.
|
|
365
|
+
* Produces '{tag}-event-{names}-{n}' format
|
|
366
|
+
* @default true
|
|
367
|
+
*/
|
|
368
|
+
injectEventElements: true,
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Enable debug logging
|
|
372
|
+
* @default false
|
|
373
|
+
*/
|
|
374
|
+
debug: false,
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Vite Plugin Options
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { vueTestIdPlugin } from 'vite-plugin-vue-testid'
|
|
382
|
+
|
|
383
|
+
vueTestIdPlugin({
|
|
384
|
+
attributeName: 'data-testid',
|
|
385
|
+
testIdPrefix: 'static-',
|
|
386
|
+
prefixCls: 'ant',
|
|
387
|
+
debug: false,
|
|
282
388
|
})
|
|
283
389
|
```
|
|
284
390
|
|
|
@@ -377,12 +483,13 @@ injectCurrentDropdowns({ prefixCls: 'ant' })
|
|
|
377
483
|
|
|
378
484
|
> Components with `id` injection (Select, Cascader, DatePicker, RangePicker, TimePicker, Modal, Upload) also have the `id` attribute set, with the same value as testid.
|
|
379
485
|
|
|
380
|
-
### Compile-time
|
|
486
|
+
### Compile-time
|
|
381
487
|
|
|
382
488
|
The compile-time transform injects testids on components matching:
|
|
383
|
-
-
|
|
489
|
+
- Any component with `tagType === Component` (e.g., `<MyInput />`)
|
|
490
|
+
- Components matching `antPrefix` (default: `a-*`) — with three-layer counter architecture
|
|
384
491
|
- Components matching any prefix in `customPrefixes`
|
|
385
|
-
- **Excluding**
|
|
492
|
+
- **Excluding** native HTML elements, `<slot>`, `<template>`, and text-only components
|
|
386
493
|
|
|
387
494
|
---
|
|
388
495
|
|
|
@@ -428,7 +535,13 @@ setupAntTestIds({
|
|
|
428
535
|
| `createTestIdTransforms(opts?)` | `() => NodeTransform[]` | Same as above, full export name |
|
|
429
536
|
| `createTestIdInjectTransform(opts?)` | `() => NodeTransform` | Create only the testid inject transform (without multi-root fix) |
|
|
430
537
|
| `createMultiRootFixTransform(opts?)` | `() => NodeTransform` | Create only the multi-root fix transform |
|
|
431
|
-
| `TransformOptions` | `interface` | `{ attributeName?, antPrefix?, customPrefixes? }` |
|
|
538
|
+
| `TransformOptions` | `interface` | `{ attributeName?, antPrefix?, customPrefixes?, testIdPrefix?, injectForLoops?, injectEventElements?, debug? }` |
|
|
539
|
+
|
|
540
|
+
### Vue Plugin Bridge (`/plugin` subpath)
|
|
541
|
+
|
|
542
|
+
| Export | Type | Description |
|
|
543
|
+
|--------|------|-------------|
|
|
544
|
+
| `createTestIdBridge(opts?)` | `Plugin` | Vue 3 plugin — reads testid from VNode.props and writes to `$el` |
|
|
432
545
|
|
|
433
546
|
### Runtime (`/runtime` subpath)
|
|
434
547
|
|
|
@@ -444,7 +557,8 @@ setupAntTestIds({
|
|
|
444
557
|
|
|
445
558
|
| Export | Type | Description |
|
|
446
559
|
|--------|------|-------------|
|
|
447
|
-
| `vueTestIdPlugin(opts?)` | `Plugin` | Vite plugin
|
|
560
|
+
| `vueTestIdPlugin(opts?)` | `Plugin` | Vite plugin — config placeholder; actual injection via `testIdTransforms()` (compile-time) + `setupAntTestIds()` (runtime) |
|
|
561
|
+
| `VueTestIdPluginOptions` | `interface` | `{ attributeName?, testIdPrefix?, prefixCls?, debug? }` |
|
|
448
562
|
|
|
449
563
|
### Deprecated
|
|
450
564
|
|
|
@@ -470,7 +584,17 @@ Panel containers also have their own MutationObserver to handle child node chang
|
|
|
470
584
|
|
|
471
585
|
### Compile-time (Optional)
|
|
472
586
|
|
|
473
|
-
Uses Vue's compiler `nodeTransform` hook at AST stage
|
|
587
|
+
Uses Vue's compiler `nodeTransform` hook at the AST stage with a **three-layer counter/priority architecture**:
|
|
588
|
+
|
|
589
|
+
1. **v-for Dynamic Injection** — When a component inside `v-for` has an index variable (e.g., `v-for="(item, i) in list"`), a dynamic JS expression is generated using the index variable chain (e.g., `a-card_in_for-{{i}}`). This produces unique per-iteration testids without runtime deduplication. Without index, falls back to `{tag}-in_for-{n}` static suffix.
|
|
590
|
+
|
|
591
|
+
2. **Conditional Block Sub-counters** — v-if / v-else-if / v-else / v-show blocks own independent per-block counters. Child elements use `{parentTestId}__{tag}-{n}` format, where `parentTestId` is the containing block's testid. Adding/removing elements inside a conditional block does not affect testids in other blocks or outside.
|
|
592
|
+
|
|
593
|
+
3. **Global Counter** — Non-conditional elements use a cross-template global counter with a `usedIds` Set to automatically skip already-occupied testids.
|
|
594
|
+
|
|
595
|
+
All three layers check for existing `data-testid` before injecting (idempotent). Native HTML elements, `<slot>`, `<template>`, and text-only components are skipped.
|
|
596
|
+
|
|
597
|
+
Since injection happens in the parent template, testids are automatically passed through to the child component's root element via Vue's `fallthroughAttrs` mechanism.
|
|
474
598
|
|
|
475
599
|
For multi-root components (Fragment), `inheritAttrs` is not effective. `createMultiRootFixTransform` automatically adds `v-bind="$attrs"` to the first antd component root element to ensure pass-through.
|
|
476
600
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 为 Vue (ant-design-vue) 组件的 DOM 元素自动注入 data-testid 属性。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 三层注入架构("铁三角"):
|
|
7
|
+
* 1. 编译期 transform — 为模板中的 antd/自定义组件注入稳定的 testid
|
|
8
|
+
* 2. Vue 插件桥接 — 将编译期注入的 testid 从 VNode.props 写入 DOM
|
|
9
|
+
* 3. 运行时 MutationObserver — 兜底覆盖弹层面板、子元素及动态内容
|
|
7
10
|
*
|
|
8
11
|
* === 使用方式 A:纯运行时 ===
|
|
9
12
|
*
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
* setupAntTestIds()
|
|
20
23
|
* ```
|
|
21
24
|
*
|
|
22
|
-
* === 使用方式 B
|
|
25
|
+
* === 使用方式 B:编译器 + Vue 插件 + 运行时(推荐,稳定 testid) ===
|
|
23
26
|
*
|
|
24
27
|
* ```ts
|
|
25
28
|
* // vite.config.ts
|
|
@@ -30,7 +33,11 @@
|
|
|
30
33
|
* vue({
|
|
31
34
|
* template: {
|
|
32
35
|
* compilerOptions: {
|
|
33
|
-
* nodeTransforms: testIdTransforms(
|
|
36
|
+
* nodeTransforms: testIdTransforms({
|
|
37
|
+
* antPrefix: 'a-',
|
|
38
|
+
* testIdPrefix: 'static-', // 可选:公共前缀
|
|
39
|
+
* debug: false, // 可选:调试日志
|
|
40
|
+
* })
|
|
34
41
|
* }
|
|
35
42
|
* }
|
|
36
43
|
* }),
|
|
@@ -38,9 +45,14 @@
|
|
|
38
45
|
* ]
|
|
39
46
|
* })
|
|
40
47
|
*
|
|
41
|
-
* // main.ts
|
|
48
|
+
* // main.ts
|
|
49
|
+
* import { createTestIdBridge } from 'vite-plugin-vue-testid/plugin'
|
|
42
50
|
* import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
|
|
43
|
-
*
|
|
51
|
+
*
|
|
52
|
+
* const app = createApp(App)
|
|
53
|
+
* app.use(createTestIdBridge()) // 编译器 testid → DOM(稳定)
|
|
54
|
+
* app.mount('#app')
|
|
55
|
+
* setupAntTestIds() // 面板 / 子元素 / 兜底
|
|
44
56
|
* ```
|
|
45
57
|
*
|
|
46
58
|
* === 使用方式 C:仅编译期(无运行时,面板不会注入 testid) ===
|
|
@@ -63,20 +75,50 @@
|
|
|
63
75
|
* })
|
|
64
76
|
* // main.ts 不需要调用 setupAntTestIds()
|
|
65
77
|
* ```
|
|
78
|
+
*
|
|
79
|
+
* === 编译期 testid 格式说明 ===
|
|
80
|
+
*
|
|
81
|
+
* | 场景 | 格式 | 示例 |
|
|
82
|
+
* |-----------------------------|----------------------------------------------|---------------------------------------|
|
|
83
|
+
* | 普通组件 | `{tag}-{n}` | `a-button-0`, `a-input-2` |
|
|
84
|
+
* | v-if/v-show 条件块的子组件 | `{parentTestId}__{tag}-{n}` | `a-button-1__a-input-0` |
|
|
85
|
+
* | v-for + index(动态) | `{tag}_in_for-{i0}[_{i1}]` | `a-card_in_for-0_1` |
|
|
86
|
+
* | v-for 内部组件(无 index) | `{tag}-in_for-{n}` | `a-button-in_for-0` |
|
|
87
|
+
* | 事件监听组件 | `{tag}-event-{names}-{n}` | `a-button-event-click-0` |
|
|
88
|
+
*
|
|
89
|
+
* 全局计数器跨所有模板共享,条件块拥有独立子计数器,确保条件渲染的组件
|
|
90
|
+
* 增删不影响外部组件的 testid 稳定性。
|
|
66
91
|
*/
|
|
67
92
|
import type { Plugin } from 'vite';
|
|
68
93
|
export type { RuntimeOptions, PanelContext, PanelInjectStrategy } from './runtime';
|
|
69
94
|
export type { TransformOptions } from './transform';
|
|
95
|
+
export type { TestIdBridgeOptions } from './plugin';
|
|
70
96
|
import { createTestIdTransforms } from './transform';
|
|
71
97
|
/** 等同于 `createTestIdTransforms()`,更短的导出名 */
|
|
72
98
|
export declare const testIdTransforms: typeof createTestIdTransforms;
|
|
73
99
|
export { createTestIdTransforms, createTestIdInjectTransform, createMultiRootFixTransform, } from './transform';
|
|
100
|
+
export { createTestIdBridge } from './plugin';
|
|
74
101
|
export interface VueTestIdPluginOptions {
|
|
75
102
|
/**
|
|
76
103
|
* testId 属性名
|
|
77
104
|
* @default 'data-testid'
|
|
78
105
|
*/
|
|
79
106
|
attributeName?: string;
|
|
107
|
+
/**
|
|
108
|
+
* 所有 testid 的公共前缀
|
|
109
|
+
* @default ''
|
|
110
|
+
*/
|
|
111
|
+
testIdPrefix?: string;
|
|
112
|
+
/**
|
|
113
|
+
* ant-design-vue CSS 类名前缀,对应 ConfigProvider 的 prefixCls
|
|
114
|
+
* @default 'ant'
|
|
115
|
+
*/
|
|
116
|
+
prefixCls?: string;
|
|
117
|
+
/**
|
|
118
|
+
* 启用调试日志
|
|
119
|
+
* @default false
|
|
120
|
+
*/
|
|
121
|
+
debug?: boolean;
|
|
80
122
|
}
|
|
81
123
|
/**
|
|
82
124
|
* Vite 插件。
|
|
@@ -84,8 +126,11 @@ export interface VueTestIdPluginOptions {
|
|
|
84
126
|
* 当前作为配置占位符存在;实际的 testid 注入由以下组件完成:
|
|
85
127
|
* - 编译期:`testIdTransforms()` → 注册到 `vue()` 的 compilerOptions
|
|
86
128
|
* - 运行时:`setupAntTestIds()` → 在 main.ts 中调用
|
|
129
|
+
*
|
|
130
|
+
* 可以通过 `config` 钩子将 testid 配置注入到构建上下文中,
|
|
131
|
+
* 供运行时与编译期共享配置(如 prefixCls、attributeName 等)。
|
|
87
132
|
*/
|
|
88
|
-
export declare function vueTestIdPlugin(
|
|
133
|
+
export declare function vueTestIdPlugin(opts?: VueTestIdPluginOptions): Plugin;
|
|
89
134
|
/**
|
|
90
135
|
* @deprecated 此函数已废弃。请使用新的编译期 API:
|
|
91
136
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAIlC,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAEpD,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,+BAAyB,CAAA;AAEtD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAI7C,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,sBAAsB,GAAG,MAAM,CAYrE;AAID;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,IAAI,CASrD"}
|
package/dist/index.js
CHANGED
|
@@ -134,6 +134,30 @@ var NATIVE_TAGS = /* @__PURE__ */ new Set([
|
|
|
134
134
|
"var",
|
|
135
135
|
"wbr"
|
|
136
136
|
]);
|
|
137
|
+
var globalCounters = {};
|
|
138
|
+
var globalUsedIds = {};
|
|
139
|
+
function generateUniqueId(basePattern) {
|
|
140
|
+
let idx = globalCounters[basePattern] || 0;
|
|
141
|
+
let candidate;
|
|
142
|
+
do {
|
|
143
|
+
candidate = basePattern + "-" + idx;
|
|
144
|
+
idx++;
|
|
145
|
+
} while (globalUsedIds[candidate]);
|
|
146
|
+
globalCounters[basePattern] = idx;
|
|
147
|
+
globalUsedIds[candidate] = true;
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
function generateSubUniqueId(basePattern, state) {
|
|
151
|
+
let idx = state.counter[basePattern] || 0;
|
|
152
|
+
let candidate;
|
|
153
|
+
do {
|
|
154
|
+
candidate = basePattern + "-" + idx;
|
|
155
|
+
idx++;
|
|
156
|
+
} while (state.usedIds[candidate]);
|
|
157
|
+
state.counter[basePattern] = idx;
|
|
158
|
+
state.usedIds[candidate] = true;
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
137
161
|
function isAntdComponent(tag, antPrefix) {
|
|
138
162
|
if (!tag.startsWith(antPrefix)) return false;
|
|
139
163
|
if (tag === antPrefix.slice(0, -1)) return false;
|
|
@@ -143,7 +167,7 @@ function isTargetComponent(el, antPrefix, customPrefixes) {
|
|
|
143
167
|
const { tag, tagType } = el;
|
|
144
168
|
if (tagType === ElementTypes.ELEMENT && NATIVE_TAGS.has(tag)) return false;
|
|
145
169
|
if (tagType === ElementTypes.SLOT || tagType === ElementTypes.TEMPLATE) return false;
|
|
146
|
-
if (isAntdComponent(tag, antPrefix)) return
|
|
170
|
+
if (isAntdComponent(tag, antPrefix)) return true;
|
|
147
171
|
if (tagType === ElementTypes.COMPONENT) return true;
|
|
148
172
|
if (customPrefixes.some((p) => tag.startsWith(p))) return true;
|
|
149
173
|
return false;
|
|
@@ -177,7 +201,6 @@ function createVBindAttrsDirective(loc) {
|
|
|
177
201
|
content: "$attrs",
|
|
178
202
|
isStatic: true,
|
|
179
203
|
constType: 3,
|
|
180
|
-
// CAN_STRINGIFY
|
|
181
204
|
loc: loc || {}
|
|
182
205
|
},
|
|
183
206
|
exp: {
|
|
@@ -185,13 +208,127 @@ function createVBindAttrsDirective(loc) {
|
|
|
185
208
|
content: "$attrs",
|
|
186
209
|
isStatic: false,
|
|
187
210
|
constType: 0,
|
|
188
|
-
// NOT_CONSTANT
|
|
189
211
|
loc: loc || {}
|
|
190
212
|
},
|
|
191
213
|
modifiers: [],
|
|
192
214
|
loc: loc || {}
|
|
193
215
|
};
|
|
194
216
|
}
|
|
217
|
+
function hasVShow(el) {
|
|
218
|
+
return el.props.some(
|
|
219
|
+
(p) => p.type === NodeTypes.DIRECTIVE && p.name === "show"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
function hasEventListeners(el) {
|
|
223
|
+
return el.props.some(
|
|
224
|
+
(p) => p.type === NodeTypes.DIRECTIVE && p.name === "on"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
function getEventNames(el) {
|
|
228
|
+
const names = [];
|
|
229
|
+
for (const p of el.props) {
|
|
230
|
+
if (p.type === NodeTypes.DIRECTIVE && p.name === "on" && p.arg) {
|
|
231
|
+
const arg = p.arg;
|
|
232
|
+
if (arg.content) names.push(arg.content);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return names.sort().join("_").slice(0, 40) || "unknown";
|
|
236
|
+
}
|
|
237
|
+
function injectTestId(el, attrName, testId, applyPrefix, debug) {
|
|
238
|
+
const finalTestId = applyPrefix(testId);
|
|
239
|
+
el.props.push(createStaticAttr(attrName, finalTestId, el.loc));
|
|
240
|
+
if (debug) {
|
|
241
|
+
console.log("[testid] INJECT", el.tag, "\u2192", finalTestId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildVForTestIdExpression(tag, indexChain, testIdPrefix, extra) {
|
|
245
|
+
let expr = "";
|
|
246
|
+
if (testIdPrefix) {
|
|
247
|
+
expr = "'" + testIdPrefix + "' + ";
|
|
248
|
+
}
|
|
249
|
+
expr += "'" + tag + "_in_for-'";
|
|
250
|
+
for (let k = 0; k < indexChain.length; k++) {
|
|
251
|
+
if (k > 0) {
|
|
252
|
+
expr += " + '_'";
|
|
253
|
+
}
|
|
254
|
+
expr += " + " + indexChain[k];
|
|
255
|
+
}
|
|
256
|
+
if (extra) {
|
|
257
|
+
expr += " + '" + extra + "'";
|
|
258
|
+
}
|
|
259
|
+
return expr;
|
|
260
|
+
}
|
|
261
|
+
function injectDynamicForTestId(el, attrName, tag, indexChain, testIdPrefix, extra, debug) {
|
|
262
|
+
const expression = buildVForTestIdExpression(tag, indexChain, testIdPrefix, extra);
|
|
263
|
+
el.props.push({
|
|
264
|
+
type: NodeTypes.DIRECTIVE,
|
|
265
|
+
name: "bind",
|
|
266
|
+
arg: {
|
|
267
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
268
|
+
content: attrName,
|
|
269
|
+
isStatic: true,
|
|
270
|
+
constType: 3,
|
|
271
|
+
loc: el.loc
|
|
272
|
+
},
|
|
273
|
+
exp: {
|
|
274
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
275
|
+
content: expression,
|
|
276
|
+
isStatic: false,
|
|
277
|
+
constType: 0,
|
|
278
|
+
loc: el.loc
|
|
279
|
+
},
|
|
280
|
+
modifiers: [],
|
|
281
|
+
loc: el.loc
|
|
282
|
+
});
|
|
283
|
+
if (debug) {
|
|
284
|
+
console.log("[testid] VFOR-DYN", tag, "\u2192", attrName, "=", expression);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function findConditionalAncestor(parentMap, branchStateMap, condStateMap, el) {
|
|
288
|
+
let current = parentMap.get(el);
|
|
289
|
+
while (current) {
|
|
290
|
+
if (current.type === NodeTypes.IF_BRANCH) {
|
|
291
|
+
const state = branchStateMap.get(current);
|
|
292
|
+
if (state) return state;
|
|
293
|
+
}
|
|
294
|
+
if (current.type === NodeTypes.ELEMENT) {
|
|
295
|
+
const state = condStateMap.get(current);
|
|
296
|
+
if (state) return state;
|
|
297
|
+
}
|
|
298
|
+
current = parentMap.get(current);
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function getVForIndexChain(parentMap, el) {
|
|
303
|
+
const chain = [];
|
|
304
|
+
let current = parentMap.get(el);
|
|
305
|
+
while (current) {
|
|
306
|
+
if (current.type === NodeTypes.FOR) {
|
|
307
|
+
const forNode = current;
|
|
308
|
+
const idxNode = forNode.objectIndexAlias;
|
|
309
|
+
if (idxNode && idxNode.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
310
|
+
const idxName = idxNode.content;
|
|
311
|
+
if (idxName) {
|
|
312
|
+
chain.push(idxName);
|
|
313
|
+
} else {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
current = parentMap.get(current);
|
|
321
|
+
}
|
|
322
|
+
return chain.length > 0 ? chain.reverse() : null;
|
|
323
|
+
}
|
|
324
|
+
function isInsideVFor(parentMap, el) {
|
|
325
|
+
let current = parentMap.get(el);
|
|
326
|
+
while (current) {
|
|
327
|
+
if (current.type === NodeTypes.FOR) return true;
|
|
328
|
+
current = parentMap.get(current);
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
195
332
|
function createMultiRootFixTransform(opts = {}) {
|
|
196
333
|
const { antPrefix = "a-" } = opts;
|
|
197
334
|
return (node, _context) => {
|
|
@@ -218,40 +355,178 @@ function createTestIdInjectTransform(opts = {}) {
|
|
|
218
355
|
const {
|
|
219
356
|
attributeName = "data-testid",
|
|
220
357
|
antPrefix = "a-",
|
|
221
|
-
customPrefixes = []
|
|
358
|
+
customPrefixes = [],
|
|
359
|
+
testIdPrefix = "",
|
|
360
|
+
injectForLoops = true,
|
|
361
|
+
injectEventElements = true,
|
|
362
|
+
debug = false
|
|
222
363
|
} = opts;
|
|
223
|
-
|
|
364
|
+
function applyPrefix(id) {
|
|
365
|
+
return testIdPrefix ? testIdPrefix + id : id;
|
|
366
|
+
}
|
|
367
|
+
const parentMap = /* @__PURE__ */ new WeakMap();
|
|
368
|
+
const branchStateMap = /* @__PURE__ */ new WeakMap();
|
|
369
|
+
const condStateMap = /* @__PURE__ */ new WeakMap();
|
|
224
370
|
return (node, context) => {
|
|
225
|
-
|
|
226
|
-
|
|
371
|
+
const children = node.children;
|
|
372
|
+
if (children) {
|
|
373
|
+
for (const child of children) {
|
|
374
|
+
if (typeof child === "object" && child !== null) {
|
|
375
|
+
parentMap.set(child, node);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (node.type === NodeTypes.IF) {
|
|
380
|
+
const ifNode = node;
|
|
381
|
+
for (const branch of ifNode.branches) {
|
|
382
|
+
parentMap.set(branch, node);
|
|
383
|
+
if (branch.children) {
|
|
384
|
+
for (const child of branch.children) {
|
|
385
|
+
if (typeof child === "object" && child !== null) {
|
|
386
|
+
parentMap.set(child, branch);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (node.type === NodeTypes.IF) {
|
|
393
|
+
const ifNode = node;
|
|
394
|
+
let hasTarget = false;
|
|
395
|
+
let firstTag = "";
|
|
396
|
+
for (const branch of ifNode.branches) {
|
|
397
|
+
for (const child of branch.children) {
|
|
398
|
+
if (child.type === NodeTypes.ELEMENT) {
|
|
399
|
+
const el2 = child;
|
|
400
|
+
if (isTargetComponent(el2, antPrefix, customPrefixes) && !hasExistingTestId(el2, attributeName)) {
|
|
401
|
+
hasTarget = true;
|
|
402
|
+
firstTag = el2.tag;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (hasTarget) break;
|
|
408
|
+
}
|
|
409
|
+
if (hasTarget) {
|
|
410
|
+
const testId = generateUniqueId(firstTag);
|
|
411
|
+
const state = {
|
|
412
|
+
counter: {},
|
|
413
|
+
usedIds: {},
|
|
414
|
+
testId
|
|
415
|
+
};
|
|
416
|
+
for (const branch of ifNode.branches) {
|
|
417
|
+
branchStateMap.set(branch, state);
|
|
418
|
+
}
|
|
419
|
+
if (debug) {
|
|
420
|
+
console.log("[testid] IF-BLOCK", firstTag, "\u2192", testId, "(branches:", ifNode.branches.length, ")");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
227
423
|
return;
|
|
228
424
|
}
|
|
229
425
|
if (node.type !== NodeTypes.ELEMENT) return;
|
|
230
426
|
const el = node;
|
|
231
427
|
if (!isTargetComponent(el, antPrefix, customPrefixes)) return;
|
|
232
428
|
if (hasExistingTestId(el, attributeName)) return;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
429
|
+
if (hasVShow(el)) {
|
|
430
|
+
const testId = generateUniqueId(el.tag);
|
|
431
|
+
injectTestId(el, attributeName, testId, applyPrefix, debug);
|
|
432
|
+
condStateMap.set(el, {
|
|
433
|
+
counter: {},
|
|
434
|
+
usedIds: {},
|
|
435
|
+
testId
|
|
436
|
+
});
|
|
437
|
+
if (debug) {
|
|
438
|
+
console.log("[testid] V-SHOW", el.tag, "\u2192", testId);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (injectForLoops) {
|
|
443
|
+
const indexChain = getVForIndexChain(parentMap, el);
|
|
444
|
+
if (indexChain && indexChain.length > 0) {
|
|
445
|
+
if (injectEventElements && hasEventListeners(el)) {
|
|
446
|
+
const eventNames = getEventNames(el);
|
|
447
|
+
injectDynamicForTestId(el, attributeName, el.tag, indexChain, testIdPrefix, "-event-" + eventNames, debug);
|
|
448
|
+
} else {
|
|
449
|
+
injectDynamicForTestId(el, attributeName, el.tag, indexChain, testIdPrefix, "", debug);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const insideFor = injectForLoops && isInsideVFor(parentMap, el);
|
|
455
|
+
const condState = findConditionalAncestor(parentMap, branchStateMap, condStateMap, el);
|
|
456
|
+
if (injectEventElements && hasEventListeners(el)) {
|
|
457
|
+
const eventNames = getEventNames(el);
|
|
458
|
+
const scope = insideFor ? "-in_for" : "";
|
|
459
|
+
if (condState) {
|
|
460
|
+
const condEventId = generateSubUniqueId(
|
|
461
|
+
condState.testId + "__" + el.tag + "-event-" + eventNames + scope,
|
|
462
|
+
condState
|
|
463
|
+
);
|
|
464
|
+
injectTestId(el, attributeName, condEventId, applyPrefix, debug);
|
|
465
|
+
if (debug) console.log("[testid] SUB ", el.tag, "\u2192", condEventId, "(parent:", condState.testId, ")");
|
|
466
|
+
} else {
|
|
467
|
+
const globalEventId = generateUniqueId(el.tag + "-event-" + eventNames + scope);
|
|
468
|
+
injectTestId(el, attributeName, globalEventId, applyPrefix, debug);
|
|
469
|
+
if (debug) console.log("[testid] GLOBAL", el.tag, "\u2192", globalEventId);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const forScope = insideFor ? "-in_for" : "";
|
|
474
|
+
if (condState) {
|
|
475
|
+
const condNormalId = generateSubUniqueId(
|
|
476
|
+
condState.testId + "__" + el.tag + forScope,
|
|
477
|
+
condState
|
|
478
|
+
);
|
|
479
|
+
injectTestId(el, attributeName, condNormalId, applyPrefix, debug);
|
|
480
|
+
if (debug) console.log("[testid] SUB ", el.tag, "\u2192", condNormalId, "(parent:", condState.testId, ")");
|
|
481
|
+
} else {
|
|
482
|
+
const globalNormalId = generateUniqueId(el.tag + forScope);
|
|
483
|
+
injectTestId(el, attributeName, globalNormalId, applyPrefix, debug);
|
|
484
|
+
if (debug) console.log("[testid] GLOBAL", el.tag, "\u2192", globalNormalId);
|
|
485
|
+
}
|
|
239
486
|
};
|
|
240
487
|
}
|
|
241
488
|
function createTestIdTransforms(opts = {}) {
|
|
242
489
|
return [
|
|
243
|
-
// multiRootFix 必须先注册,因为它的 binding 应出现在
|
|
244
|
-
// testId inject 之前(编译顺序无关,但逻辑上先修复再注入更清晰)
|
|
245
490
|
createMultiRootFixTransform(opts),
|
|
246
491
|
createTestIdInjectTransform(opts)
|
|
247
492
|
];
|
|
248
493
|
}
|
|
249
494
|
|
|
495
|
+
// src/plugin.ts
|
|
496
|
+
import { getCurrentInstance } from "vue";
|
|
497
|
+
function createTestIdBridge(opts = {}) {
|
|
498
|
+
const attrName = opts.attributeName ?? "data-testid";
|
|
499
|
+
return {
|
|
500
|
+
install(app) {
|
|
501
|
+
app.mixin({
|
|
502
|
+
mounted() {
|
|
503
|
+
const instance = getCurrentInstance();
|
|
504
|
+
if (!instance) return;
|
|
505
|
+
const testId = instance.vnode.props?.[attrName];
|
|
506
|
+
if (!testId) return;
|
|
507
|
+
const el = instance.vnode.el;
|
|
508
|
+
if (el instanceof HTMLElement && !el.hasAttribute(attrName)) {
|
|
509
|
+
el.setAttribute(attrName, testId);
|
|
510
|
+
if (typeof window !== "undefined" && window.__TESTID_DEBUG) {
|
|
511
|
+
console.debug("[testid-bridge] compile testid \u2192 DOM:", testId, el.tagName, el.className);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
250
520
|
// src/index.ts
|
|
251
521
|
var testIdTransforms = createTestIdTransforms;
|
|
252
|
-
function vueTestIdPlugin(
|
|
522
|
+
function vueTestIdPlugin(opts) {
|
|
253
523
|
return {
|
|
254
|
-
name: "vite-plugin-vue-testid"
|
|
524
|
+
name: "vite-plugin-vue-testid",
|
|
525
|
+
config() {
|
|
526
|
+
if (opts?.debug) {
|
|
527
|
+
console.log("[vite-plugin-vue-testid] plugin initialized with opts:", opts);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
255
530
|
};
|
|
256
531
|
}
|
|
257
532
|
function createVueTestIdTransform() {
|
|
@@ -265,6 +540,7 @@ function createVueTestIdTransform() {
|
|
|
265
540
|
}
|
|
266
541
|
export {
|
|
267
542
|
createMultiRootFixTransform,
|
|
543
|
+
createTestIdBridge,
|
|
268
544
|
createTestIdInjectTransform,
|
|
269
545
|
createTestIdTransforms,
|
|
270
546
|
createVueTestIdTransform,
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue 插件:读取编译器注入的 data-testid 属性,手动写入组件的根 DOM 元素。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:
|
|
5
|
+
* 编译器在模板中为每个 (antd + 自定义) 组件生成稳定的 {tagName}-{index} testid,
|
|
6
|
+
* 但这些 testid 通过 $attrs 传递。部分 ant-design-vue 组件(Modal、Menu 等)
|
|
7
|
+
* 使用了 inheritAttrs: false,或将其 DOM 根通过 teleport 渲染到 body 下,
|
|
8
|
+
* 导致 testid 无法自动到达目标 DOM。
|
|
9
|
+
*
|
|
10
|
+
* 本插件在组件 mounted 时读取 $attrs['data-testid'],手动写入 $el,
|
|
11
|
+
* 绕过 inheritAttrs 限制,确保编译期生成的稳定 testid 最终出现在 DOM 上。
|
|
12
|
+
*
|
|
13
|
+
* 使用方式:
|
|
14
|
+
* ```ts
|
|
15
|
+
* // main.ts
|
|
16
|
+
* import { createTestIdBridge } from 'vite-plugin-vue-testid/plugin'
|
|
17
|
+
*
|
|
18
|
+
* createApp(App).mount('#app')
|
|
19
|
+
* app.use(createTestIdBridge())
|
|
20
|
+
* app.use(createTestIdBridge({ attributeName: 'data-testid' }))
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { type App } from 'vue';
|
|
24
|
+
export interface TestIdBridgeOptions {
|
|
25
|
+
/**
|
|
26
|
+
* testId 属性名
|
|
27
|
+
* @default 'data-testid'
|
|
28
|
+
*/
|
|
29
|
+
attributeName?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function createTestIdBridge(opts?: TestIdBridgeOptions): {
|
|
32
|
+
install(app: App): void;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAsB,KAAK,GAAG,EAAE,MAAM,KAAK,CAAA;AAElD,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,mBAAwB;iBAIhD,GAAG;EAyBnB"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { getCurrentInstance } from "vue";
|
|
3
|
+
function createTestIdBridge(opts = {}) {
|
|
4
|
+
const attrName = opts.attributeName ?? "data-testid";
|
|
5
|
+
return {
|
|
6
|
+
install(app) {
|
|
7
|
+
app.mixin({
|
|
8
|
+
mounted() {
|
|
9
|
+
const instance = getCurrentInstance();
|
|
10
|
+
if (!instance) return;
|
|
11
|
+
const testId = instance.vnode.props?.[attrName];
|
|
12
|
+
if (!testId) return;
|
|
13
|
+
const el = instance.vnode.el;
|
|
14
|
+
if (el instanceof HTMLElement && !el.hasAttribute(attrName)) {
|
|
15
|
+
el.setAttribute(attrName, testId);
|
|
16
|
+
if (typeof window !== "undefined" && window.__TESTID_DEBUG) {
|
|
17
|
+
console.debug("[testid-bridge] compile testid \u2192 DOM:", testId, el.tagName, el.className);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
createTestIdBridge
|
|
27
|
+
};
|
package/dist/runtime.d.ts
CHANGED
|
@@ -61,11 +61,14 @@ export type PanelInjectStrategy = (ctx: PanelContext) => void;
|
|
|
61
61
|
* 启动全运行时 testid 注入。
|
|
62
62
|
*
|
|
63
63
|
* 覆盖范围:
|
|
64
|
-
* - 所有 ant-design-vue 组件根元素(input / select / button / picker / ...)
|
|
65
|
-
* - teleport 弹出层面板(日期选择器 / 级联选择器 /
|
|
64
|
+
* - 所有 ant-design-vue 组件根元素(input / select / button / picker / auto-complete / mentions / transfer ...)
|
|
65
|
+
* - teleport 弹出层面板(日期选择器 / 级联选择器 / 下拉选择器 / 提及)
|
|
66
66
|
* - Menu 子元素(inheritAttrs:false 兜底)
|
|
67
67
|
* - InputNumber 内部输入框及增减按钮
|
|
68
68
|
* - Slider handle、Rate 星星、Upload 内部 file input
|
|
69
|
+
* - AutoComplete 搜索输入框及清除按钮
|
|
70
|
+
* - Mentions textarea 及下拉面板选项
|
|
71
|
+
* - Transfer 左右面板 / 列表项 / 复选框 / 移除按钮 / 操作区
|
|
69
72
|
*
|
|
70
73
|
* @returns 清理函数,调用后停止监听
|
|
71
74
|
*
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAAA;CACzD;AAED,MAAM,WAAW,YAAY;IAC3B,kBAAkB;IAClB,SAAS,EAAE,WAAW,CAAA;IACtB,iBAAiB;IACjB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,oBAAoB;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAAA;CACzD;AAED,MAAM,WAAW,YAAY;IAC3B,kBAAkB;IAClB,SAAS,EAAE,WAAW,CAAA;IACtB,iBAAiB;IACjB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,oBAAoB;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAA;AAs+B7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,IAAI,CA6FrE;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,wBAAkB,CAAA;AAMtD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,cAAmB,GAAG,IAAI,CA+CtE"}
|
package/dist/runtime.js
CHANGED
|
@@ -8,10 +8,14 @@ function buildComponentMatchers(pc) {
|
|
|
8
8
|
{ selector: sel(pc, "input-affix-wrapper"), prefix: "a-input" },
|
|
9
9
|
{ selector: sel(pc, "input-textarea"), prefix: "a-textarea" },
|
|
10
10
|
{ selector: sel(pc, "input-number"), prefix: "a-input-number" },
|
|
11
|
+
// ── AutoComplete(必须在 select 之前,因为同时也有 .ant-select 类)──
|
|
12
|
+
{ selector: sel(pc, "select-auto-complete"), prefix: "a-auto-complete", withId: true },
|
|
11
13
|
// ── 选择器 ──
|
|
12
14
|
{ selector: sel(pc, "select"), prefix: "a-select", withId: true },
|
|
13
15
|
{ selector: sel(pc, "cascader"), prefix: "a-cascader", withId: true },
|
|
14
16
|
{ selector: sel(pc, "tree-select"), prefix: "a-tree-select" },
|
|
17
|
+
// ── Mentions ──
|
|
18
|
+
{ selector: sel(pc, "mentions"), prefix: "a-mentions", withId: true },
|
|
15
19
|
// ── 日期/时间 ──
|
|
16
20
|
{ selector: sel(pc, "picker"), prefix: "a-date-picker", withId: true },
|
|
17
21
|
// ── 表单组件 ──
|
|
@@ -32,6 +36,9 @@ function buildComponentMatchers(pc) {
|
|
|
32
36
|
{ selector: sel(pc, "menu"), prefix: "a-menu" },
|
|
33
37
|
// ── 标签页 ──
|
|
34
38
|
{ selector: sel(pc, "tabs"), prefix: "a-tabs" },
|
|
39
|
+
{ selector: sel(pc, "pagination"), prefix: "a-pagination" },
|
|
40
|
+
// ── Transfer ──
|
|
41
|
+
{ selector: sel(pc, "transfer"), prefix: "a-transfer" },
|
|
35
42
|
// ── 纯 input / textarea(必须排在最后)──
|
|
36
43
|
{ selector: `input.${cls(pc, "input")}`, prefix: "a-input" },
|
|
37
44
|
{ selector: `textarea.${cls(pc, "input")}`, prefix: "a-textarea" }
|
|
@@ -64,6 +71,9 @@ function injectComponentRoots(root, matchers, attrName, counter, skipSelectors)
|
|
|
64
71
|
if (isInnerInput(el, skipSelectors)) continue;
|
|
65
72
|
const testId = `${prefix}-${counter.value++}`;
|
|
66
73
|
el.setAttribute(attrName, testId);
|
|
74
|
+
if (typeof window !== "undefined" && window.__TESTID_DEBUG) {
|
|
75
|
+
console.debug("[testid-runtime] counter inject:", testId, el);
|
|
76
|
+
}
|
|
67
77
|
if (withId && !el.hasAttribute("id")) {
|
|
68
78
|
el.setAttribute("id", testId);
|
|
69
79
|
}
|
|
@@ -272,11 +282,42 @@ function makeInjectSelectPanel(pc) {
|
|
|
272
282
|
}
|
|
273
283
|
};
|
|
274
284
|
}
|
|
285
|
+
function makeInjectMentionsPanel(pc) {
|
|
286
|
+
return (ctx) => {
|
|
287
|
+
const { container, triggerTestId, attrName } = ctx;
|
|
288
|
+
const prefix = `${triggerTestId}-dropdown`;
|
|
289
|
+
container.setAttribute(attrName, prefix);
|
|
290
|
+
const injectAllItems = () => {
|
|
291
|
+
const items = container.querySelectorAll(sel(pc, "mentions-dropdown-menu-item"));
|
|
292
|
+
items.forEach((item) => {
|
|
293
|
+
const el = item;
|
|
294
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
295
|
+
const text = el.textContent?.trim() || "";
|
|
296
|
+
if (text) {
|
|
297
|
+
el.setAttribute(attrName, `${prefix}-option-${text}`);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
injectAllItems();
|
|
302
|
+
if (!_observedContainers.has(container)) {
|
|
303
|
+
_observedContainers.add(container);
|
|
304
|
+
let timer = null;
|
|
305
|
+
const observer = new MutationObserver(() => {
|
|
306
|
+
if (timer !== null) clearTimeout(timer);
|
|
307
|
+
timer = setTimeout(() => {
|
|
308
|
+
injectAllItems();
|
|
309
|
+
}, 50);
|
|
310
|
+
});
|
|
311
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
275
315
|
function buildPanels(pc) {
|
|
276
316
|
return {
|
|
277
317
|
[sel(pc, "picker-dropdown")]: makeInjectPickerPanel(pc),
|
|
278
318
|
[sel(pc, "cascader-dropdown")]: makeInjectCascaderPanel(pc),
|
|
279
|
-
[sel(pc, "select-dropdown")]: makeInjectSelectPanel(pc)
|
|
319
|
+
[sel(pc, "select-dropdown")]: makeInjectSelectPanel(pc),
|
|
320
|
+
[sel(pc, "mentions-dropdown")]: makeInjectMentionsPanel(pc)
|
|
280
321
|
};
|
|
281
322
|
}
|
|
282
323
|
function injectMenuItems(attrName, root = document, pc) {
|
|
@@ -348,6 +389,14 @@ function findActiveTrigger(attrName, pc) {
|
|
|
348
389
|
const found = resolveTriggerInContainer(cascader, attrName);
|
|
349
390
|
if (found) return found;
|
|
350
391
|
}
|
|
392
|
+
const mentionsDropdown = document.querySelector(sel(pc, "mentions-dropdown"));
|
|
393
|
+
if (mentionsDropdown) {
|
|
394
|
+
const mentionsRoot = document.querySelector(sel(pc, "mentions"));
|
|
395
|
+
if (mentionsRoot) {
|
|
396
|
+
const found = resolveTriggerInContainer(mentionsRoot, attrName);
|
|
397
|
+
if (found) return found;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
351
400
|
const expanded = document.querySelector(
|
|
352
401
|
`[id][aria-expanded="true"], [data-testid][aria-expanded="true"], [data-testId][aria-expanded="true"]`
|
|
353
402
|
);
|
|
@@ -480,6 +529,150 @@ function injectTabsSubElements(root, attrName, pc) {
|
|
|
480
529
|
}
|
|
481
530
|
}
|
|
482
531
|
}
|
|
532
|
+
function injectPaginationSubElements(root, attrName, pc) {
|
|
533
|
+
const paginations = root.matches(`${sel(pc, "pagination")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "pagination")}[${attrName}]`));
|
|
534
|
+
for (const pagination of paginations) {
|
|
535
|
+
if (!(pagination instanceof HTMLElement)) continue;
|
|
536
|
+
const testId = getAttrCompat(pagination, attrName) || "";
|
|
537
|
+
if (!testId) continue;
|
|
538
|
+
const prevEl = pagination.querySelector(sel(pc, "pagination-prev"));
|
|
539
|
+
if (prevEl && !hasAttrCompat(prevEl, attrName)) {
|
|
540
|
+
prevEl.setAttribute(attrName, `${testId}-prev`);
|
|
541
|
+
}
|
|
542
|
+
const nextEl = pagination.querySelector(sel(pc, "pagination-next"));
|
|
543
|
+
if (nextEl && !hasAttrCompat(nextEl, attrName)) {
|
|
544
|
+
nextEl.setAttribute(attrName, `${testId}-next`);
|
|
545
|
+
}
|
|
546
|
+
pagination.querySelectorAll(sel(pc, "pagination-item")).forEach((item) => {
|
|
547
|
+
const el = item;
|
|
548
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
549
|
+
const pageNum = el.textContent?.trim() || "";
|
|
550
|
+
if (pageNum) {
|
|
551
|
+
el.setAttribute(attrName, `${testId}-item-${pageNum}`);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
const jumpPrev = pagination.querySelector(sel(pc, "pagination-jump-prev"));
|
|
555
|
+
if (jumpPrev && !hasAttrCompat(jumpPrev, attrName)) {
|
|
556
|
+
jumpPrev.setAttribute(attrName, `${testId}-jump-prev`);
|
|
557
|
+
}
|
|
558
|
+
const jumpNext = pagination.querySelector(sel(pc, "pagination-jump-next"));
|
|
559
|
+
if (jumpNext && !hasAttrCompat(jumpNext, attrName)) {
|
|
560
|
+
jumpNext.setAttribute(attrName, `${testId}-jump-next`);
|
|
561
|
+
}
|
|
562
|
+
const sizeChanger = pagination.querySelector(sel(pc, "pagination-options-size-changer"));
|
|
563
|
+
if (sizeChanger && !hasAttrCompat(sizeChanger, attrName)) {
|
|
564
|
+
sizeChanger.setAttribute(attrName, `${testId}-size-changer`);
|
|
565
|
+
}
|
|
566
|
+
const quickJumper = pagination.querySelector(sel(pc, "pagination-options-quick-jumper"));
|
|
567
|
+
if (quickJumper) {
|
|
568
|
+
const tryQuickJumperInput = () => {
|
|
569
|
+
const input = quickJumper.querySelector("input");
|
|
570
|
+
if (input && !hasAttrCompat(input, attrName)) {
|
|
571
|
+
input.setAttribute(attrName, `${testId}-quick-jumper`);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
};
|
|
576
|
+
if (!tryQuickJumperInput()) {
|
|
577
|
+
const subObs = new MutationObserver(() => {
|
|
578
|
+
if (tryQuickJumperInput()) subObs.disconnect();
|
|
579
|
+
});
|
|
580
|
+
subObs.observe(quickJumper, { childList: true, subtree: true });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const totalText = pagination.querySelector(sel(pc, "pagination-total-text"));
|
|
584
|
+
if (totalText && !hasAttrCompat(totalText, attrName)) {
|
|
585
|
+
totalText.setAttribute(attrName, `${testId}-total`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function injectAutoCompleteSubElements(root, attrName, pc) {
|
|
590
|
+
const wrappers = root.matches(`${sel(pc, "select-auto-complete")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "select-auto-complete")}[${attrName}]`));
|
|
591
|
+
for (const wrapper of wrappers) {
|
|
592
|
+
if (!(wrapper instanceof HTMLElement)) continue;
|
|
593
|
+
const testId = getAttrCompat(wrapper, attrName) || "";
|
|
594
|
+
if (!testId) continue;
|
|
595
|
+
const searchInput = wrapper.querySelector(
|
|
596
|
+
`${sel(pc, "select-selection-search")} input`
|
|
597
|
+
);
|
|
598
|
+
if (searchInput && !hasAttrCompat(searchInput, attrName)) {
|
|
599
|
+
searchInput.setAttribute(attrName, `${testId}-input`);
|
|
600
|
+
}
|
|
601
|
+
const clearBtn = wrapper.querySelector(sel(pc, "select-clear"));
|
|
602
|
+
if (clearBtn && !hasAttrCompat(clearBtn, attrName)) {
|
|
603
|
+
clearBtn.setAttribute(attrName, `${testId}-clear`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function injectMentionsSubElements(root, attrName, pc) {
|
|
608
|
+
const mentions = root.matches(`${sel(pc, "mentions")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "mentions")}[${attrName}]`));
|
|
609
|
+
for (const wrapper of mentions) {
|
|
610
|
+
if (!(wrapper instanceof HTMLElement)) continue;
|
|
611
|
+
const testId = getAttrCompat(wrapper, attrName) || "";
|
|
612
|
+
if (!testId) continue;
|
|
613
|
+
const textarea = wrapper.querySelector(":scope > textarea");
|
|
614
|
+
if (textarea && !hasAttrCompat(textarea, attrName)) {
|
|
615
|
+
textarea.setAttribute(attrName, `${testId}-input`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function injectTransferSubElements(root, attrName, pc) {
|
|
620
|
+
const transfers = root.matches(`${sel(pc, "transfer")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "transfer")}[${attrName}]`));
|
|
621
|
+
for (const transfer of transfers) {
|
|
622
|
+
if (!(transfer instanceof HTMLElement)) continue;
|
|
623
|
+
const testId = getAttrCompat(transfer, attrName) || "";
|
|
624
|
+
if (!testId) continue;
|
|
625
|
+
const lists = transfer.querySelectorAll(`:scope > ${sel(pc, "transfer-list")}`);
|
|
626
|
+
const directions = ["left", "right"];
|
|
627
|
+
for (let i = 0; i < Math.min(lists.length, 2); i++) {
|
|
628
|
+
const list = lists[i];
|
|
629
|
+
const dir = directions[i];
|
|
630
|
+
const listTestId = `${testId}-list-${dir}`;
|
|
631
|
+
if (!hasAttrCompat(list, attrName)) {
|
|
632
|
+
list.setAttribute(attrName, listTestId);
|
|
633
|
+
}
|
|
634
|
+
const headerCheckbox = list.querySelector(
|
|
635
|
+
`${sel(pc, "transfer-list-header")} ${sel(pc, "transfer-list-checkbox")}`
|
|
636
|
+
);
|
|
637
|
+
if (headerCheckbox && !hasAttrCompat(headerCheckbox, attrName)) {
|
|
638
|
+
headerCheckbox.setAttribute(attrName, `${listTestId}-check-all`);
|
|
639
|
+
}
|
|
640
|
+
const searchInput = list.querySelector(
|
|
641
|
+
`${sel(pc, "transfer-list-body-search-wrapper")} input`
|
|
642
|
+
);
|
|
643
|
+
if (searchInput && !hasAttrCompat(searchInput, attrName)) {
|
|
644
|
+
searchInput.setAttribute(attrName, `${listTestId}-search`);
|
|
645
|
+
}
|
|
646
|
+
const items = list.querySelectorAll(sel(pc, "transfer-list-content-item"));
|
|
647
|
+
items.forEach((item) => {
|
|
648
|
+
const el = item;
|
|
649
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
650
|
+
const text = (el.querySelector(sel(pc, "transfer-list-content-item-text"))?.textContent || el.textContent || "").trim().replace(/\s+/g, "-");
|
|
651
|
+
if (text) {
|
|
652
|
+
el.setAttribute(attrName, `${listTestId}-item-${text}`);
|
|
653
|
+
}
|
|
654
|
+
const checkbox = el.querySelector(
|
|
655
|
+
sel(pc, "transfer-list-checkbox")
|
|
656
|
+
);
|
|
657
|
+
if (checkbox && !hasAttrCompat(checkbox, attrName) && text) {
|
|
658
|
+
checkbox.setAttribute(attrName, `${listTestId}-item-check-${text}`);
|
|
659
|
+
}
|
|
660
|
+
const removeBtn = el.querySelector(
|
|
661
|
+
sel(pc, "transfer-list-content-item-remove")
|
|
662
|
+
);
|
|
663
|
+
if (removeBtn && !hasAttrCompat(removeBtn, attrName) && text) {
|
|
664
|
+
removeBtn.setAttribute(attrName, `${listTestId}-item-remove-${text}`);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const operation = transfer.querySelector(
|
|
669
|
+
`:scope > ${sel(pc, "transfer-operation")}`
|
|
670
|
+
);
|
|
671
|
+
if (operation && !hasAttrCompat(operation, attrName)) {
|
|
672
|
+
operation.setAttribute(attrName, `${testId}-operation`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
483
676
|
function setupAntTestIds(opts = {}) {
|
|
484
677
|
const attrName = opts.attributeName ?? "data-testid";
|
|
485
678
|
const pc = opts.prefixCls ?? "ant";
|
|
@@ -507,6 +700,10 @@ function setupAntTestIds(opts = {}) {
|
|
|
507
700
|
}
|
|
508
701
|
injectSubElements(node, attrName, pc);
|
|
509
702
|
injectTabsSubElements(node, attrName, pc);
|
|
703
|
+
injectPaginationSubElements(node, attrName, pc);
|
|
704
|
+
injectAutoCompleteSubElements(node, attrName, pc);
|
|
705
|
+
injectMentionsSubElements(node, attrName, pc);
|
|
706
|
+
injectTransferSubElements(node, attrName, pc);
|
|
510
707
|
}
|
|
511
708
|
let pending = false;
|
|
512
709
|
let pendingNodes = [];
|
|
@@ -529,8 +726,8 @@ function setupAntTestIds(opts = {}) {
|
|
|
529
726
|
requestAnimationFrame(flushPending);
|
|
530
727
|
}
|
|
531
728
|
});
|
|
532
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
533
729
|
processNode(document.body);
|
|
730
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
534
731
|
return () => {
|
|
535
732
|
observer.disconnect();
|
|
536
733
|
pendingNodes = [];
|
|
@@ -558,7 +755,11 @@ function injectCurrentDropdowns(opts = {}) {
|
|
|
558
755
|
}
|
|
559
756
|
injectSubElements(document.body, attrName, pc);
|
|
560
757
|
injectTabsSubElements(document.body, attrName, pc);
|
|
758
|
+
injectPaginationSubElements(document.body, attrName, pc);
|
|
561
759
|
injectMenuItems(attrName, document, pc);
|
|
760
|
+
injectAutoCompleteSubElements(document.body, attrName, pc);
|
|
761
|
+
injectMentionsSubElements(document.body, attrName, pc);
|
|
762
|
+
injectTransferSubElements(document.body, attrName, pc);
|
|
562
763
|
}
|
|
563
764
|
function mergeMatchers(defaults, customs) {
|
|
564
765
|
if (!customs) return defaults;
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 编译期模板 transform
|
|
2
|
+
* 编译期模板 transform — 三层计数器/注入架构。
|
|
3
|
+
*
|
|
4
|
+
* 仿照 vant-testid-webpack-plugin 的设计,为 Vue 3 编译器提供:
|
|
5
|
+
*
|
|
6
|
+
* 1. v-for + index 动态注入(最高优先级)— 利用 index 变量生成每轮迭代唯一 testid
|
|
7
|
+
* 2. 条件块子计数器 — v-if / v-else-if / v-else / v-show 拥有独立子计数器,
|
|
8
|
+
* 子元素以 `{parentTestId}__{tag}-{n}` 格式注入,不受外部元素影响
|
|
9
|
+
* 3. 全局计数器 — 非条件块元素使用跨模板全局计数器,保证 testid 唯一
|
|
3
10
|
*
|
|
4
11
|
* 两个独立的 NodeTransform:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 2. testIdInjectTransform — 在父模板中为每个组件使用点注入唯一的
|
|
8
|
-
* data-testid="{tagName}-{index}",保证同一页面中同类型组件的
|
|
9
|
-
* testid 全局唯一且稳定。
|
|
12
|
+
* - multiRootFixTransform — 多根组件自动补 v-bind="$attrs"
|
|
13
|
+
* - testIdInjectTransform — testid 注入
|
|
10
14
|
*
|
|
11
|
-
*
|
|
15
|
+
* 通过 createTestIdTransforms() 一键获得。
|
|
12
16
|
*/
|
|
13
17
|
import type { NodeTransform } from '@vue/compiler-core';
|
|
14
18
|
export interface TransformOptions {
|
|
@@ -18,29 +22,57 @@ export interface TransformOptions {
|
|
|
18
22
|
antPrefix?: string;
|
|
19
23
|
/** 额外需要注入 testid 的自定义组件前缀 */
|
|
20
24
|
customPrefixes?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* 所有编译期注入的 testid 前添加的公共前缀。
|
|
27
|
+
* 例如设置为 `"static-"`,则 `a-button-0` 变为 `static-a-button-0`。
|
|
28
|
+
* @default ''
|
|
29
|
+
*/
|
|
30
|
+
testIdPrefix?: string;
|
|
31
|
+
/**
|
|
32
|
+
* 是否为 v-for 元素使用特殊格式 `{tag}-in_for-{n}`
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
injectForLoops?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 是否为绑定了事件监听(@click 等)的元素使用特殊格式
|
|
38
|
+
* `{tag}-event-{eventNames}-{n}`
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
injectEventElements?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* 启用调试日志(编译时在终端输出每个注入的 testid)
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
debug?: boolean;
|
|
21
47
|
}
|
|
22
48
|
/**
|
|
23
49
|
* 转换多根子组件的模板,自动给第一个 antd 组件根元素加 v-bind="$attrs"。
|
|
24
|
-
*
|
|
25
|
-
* 为什么需要:多根组件(Fragment)的 inheritAttrs 不生效,父模板编译期
|
|
26
|
-
* 注入的 data-testid 属性会被丢弃。本 transform 确保至少一个 antd 组件根
|
|
27
|
-
* 能收到透传属性。
|
|
28
|
-
*
|
|
29
|
-
* 安全策略:
|
|
30
|
-
* - 先检查用户是否已手动写了任一 v-bind="$attrs",有则不动
|
|
31
|
-
* - 只在模板是多根且至少有一个 antd 组件时才注入
|
|
32
50
|
*/
|
|
33
51
|
export declare function createMultiRootFixTransform(opts?: TransformOptions): NodeTransform;
|
|
34
52
|
/**
|
|
35
53
|
* 在模板编译期为所有(antd + 自定义)组件注入唯一的 data-testid。
|
|
36
54
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
55
|
+
* 三层计数器/注入架构:
|
|
56
|
+
*
|
|
57
|
+
* **v-for 动态注入**(最高优先级)— v-for 有显式 index 变量时,生成运行时
|
|
58
|
+
* 动态表达式(如 `'a-button_in_for-'+i+'_'+j`),每轮迭代自动产生唯一 testid。
|
|
59
|
+
*
|
|
60
|
+
* **条件块子计数器** — v-if / v-else-if / v-else / v-show 元素创建独立子计数器,
|
|
61
|
+
* 其子元素以 `{parentTestId}__{tag}-{n}` 格式注入。
|
|
62
|
+
*
|
|
63
|
+
* **全局计数器** — 非条件块元素使用跨模板全局计数器,格式为 `{tag}-{n}`。
|
|
39
64
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
65
|
+
* 格式一览:
|
|
66
|
+
*
|
|
67
|
+
* | 元素类型 | 格式 |
|
|
68
|
+
* |-----------------------------|----------------------------------------------------|
|
|
69
|
+
* | v-if/v-show 条件块的子元素 | `{parentTestId}__{tag}-event-{names}-{n}` 或 `{parentTestId}__{tag}-{n}` |
|
|
70
|
+
* | v-for + index(动态) | `{tag}_in_for-{i0}[_{i1}]-event-{names}` 或 `{tag}_in_for-{i0}[_{i1}]` |
|
|
71
|
+
* | 顶层事件监听元素 | `{tag}-event-{names}-{n}` |
|
|
72
|
+
* | v-for 内部元素(无 index) | `{tag}-in_for-{n}` |
|
|
73
|
+
* | 普通组件 | `{tag}-{n}` |
|
|
74
|
+
*
|
|
75
|
+
* 所有 testid 前后缀均可通过 `testIdPrefix` 选项配置。
|
|
44
76
|
*/
|
|
45
77
|
export declare function createTestIdInjectTransform(opts?: TransformOptions): NodeTransform;
|
|
46
78
|
/**
|
|
@@ -64,6 +96,17 @@ export declare function createTestIdInjectTransform(opts?: TransformOptions): No
|
|
|
64
96
|
* ]
|
|
65
97
|
* })
|
|
66
98
|
* ```
|
|
99
|
+
*
|
|
100
|
+
* 配置选项:
|
|
101
|
+
* ```ts
|
|
102
|
+
* testIdTransforms({
|
|
103
|
+
* antPrefix: 'a-', // antd 组件前缀
|
|
104
|
+
* testIdPrefix: 'static-', // testid 公共前缀
|
|
105
|
+
* injectForLoops: true, // v-for 标记
|
|
106
|
+
* injectEventElements: true, // 事件监听元素标记
|
|
107
|
+
* debug: false, // 调试日志
|
|
108
|
+
* })
|
|
109
|
+
* ```
|
|
67
110
|
*/
|
|
68
111
|
export declare function createTestIdTransforms(opts?: TransformOptions): NodeTransform[];
|
|
69
112
|
//# sourceMappingURL=transform.d.ts.map
|
package/dist/transform.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACV,aAAa,EAWd,MAAM,oBAAoB,CAAA;AAgD3B,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,6BAA6B;IAC7B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IAEzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IAExB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAE7B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAuVD;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,CA6BtF;AAID;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,CAmMtF;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,EAAE,CAKnF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-vue-testid",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"./runtime": {
|
|
15
15
|
"import": "./dist/runtime.js",
|
|
16
16
|
"types": "./dist/runtime.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./plugin": {
|
|
19
|
+
"import": "./dist/plugin.js",
|
|
20
|
+
"types": "./dist/plugin.d.ts"
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
"files": [
|
|
@@ -49,4 +53,4 @@
|
|
|
49
53
|
"typescript": "~6.0.2",
|
|
50
54
|
"vite": "^8.0.12"
|
|
51
55
|
}
|
|
52
|
-
}
|
|
56
|
+
}
|