vite-plugin-vue-testid 1.0.11 → 1.1.0
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/dist/index.d.ts +69 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +59 -33
- package/dist/transform.d.ts +69 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,23 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vite-plugin-vue-testid
|
|
3
3
|
*
|
|
4
|
-
* 为 ant-design-vue 组件的 DOM 元素自动注入 data-testid
|
|
5
|
-
*
|
|
4
|
+
* 为 Vue (ant-design-vue) 组件的 DOM 元素自动注入 data-testid 属性。
|
|
5
|
+
*
|
|
6
|
+
* 默认提供全运行时方案,可选启用编译期 transform 以获得稳定的、页面内唯一的 testid。
|
|
7
|
+
*
|
|
8
|
+
* === 使用方式 A:纯运行时 ===
|
|
6
9
|
*
|
|
7
|
-
* @example
|
|
8
10
|
* ```ts
|
|
9
|
-
* // vite.config.ts
|
|
11
|
+
* // vite.config.ts
|
|
10
12
|
* import vue from '@vitejs/plugin-vue'
|
|
11
|
-
*
|
|
13
|
+
* import { vueTestIdPlugin } from 'vite-plugin-vue-testid'
|
|
14
|
+
* export default defineConfig({ plugins: [vue(), vueTestIdPlugin()] })
|
|
12
15
|
*
|
|
13
|
-
* // main.ts
|
|
16
|
+
* // main.ts
|
|
14
17
|
* import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
|
|
15
18
|
* createApp(App).mount('#app')
|
|
16
19
|
* setupAntTestIds()
|
|
17
20
|
* ```
|
|
21
|
+
*
|
|
22
|
+
* === 使用方式 B:编译期 + 运行时(推荐,获得唯一稳定 testid) ===
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // vite.config.ts
|
|
26
|
+
* import vue from '@vitejs/plugin-vue'
|
|
27
|
+
* import { vueTestIdPlugin, testIdTransforms } from 'vite-plugin-vue-testid'
|
|
28
|
+
* export default defineConfig({
|
|
29
|
+
* plugins: [
|
|
30
|
+
* vue({
|
|
31
|
+
* template: {
|
|
32
|
+
* compilerOptions: {
|
|
33
|
+
* nodeTransforms: testIdTransforms()
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* }),
|
|
37
|
+
* vueTestIdPlugin(),
|
|
38
|
+
* ]
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // main.ts (与 A 相同)
|
|
42
|
+
* import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
|
|
43
|
+
* setupAntTestIds()
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* === 使用方式 C:仅编译期(无运行时,面板不会注入 testid) ===
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* // vite.config.ts
|
|
50
|
+
* import vue from '@vitejs/plugin-vue'
|
|
51
|
+
* import { vueTestIdPlugin, testIdTransforms } from 'vite-plugin-vue-testid'
|
|
52
|
+
* export default defineConfig({
|
|
53
|
+
* plugins: [
|
|
54
|
+
* vue({
|
|
55
|
+
* template: {
|
|
56
|
+
* compilerOptions: {
|
|
57
|
+
* nodeTransforms: testIdTransforms()
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* }),
|
|
61
|
+
* vueTestIdPlugin(),
|
|
62
|
+
* ]
|
|
63
|
+
* })
|
|
64
|
+
* // main.ts 不需要调用 setupAntTestIds()
|
|
65
|
+
* ```
|
|
18
66
|
*/
|
|
19
67
|
import type { Plugin } from 'vite';
|
|
20
68
|
export type { RuntimeOptions, PanelContext, PanelInjectStrategy } from './runtime';
|
|
69
|
+
export type { TransformOptions } from './transform';
|
|
70
|
+
import { createTestIdTransforms } from './transform';
|
|
71
|
+
/** 等同于 `createTestIdTransforms()`,更短的导出名 */
|
|
72
|
+
export declare const testIdTransforms: typeof createTestIdTransforms;
|
|
73
|
+
export { createTestIdTransforms, createTestIdInjectTransform, createMultiRootFixTransform, } from './transform';
|
|
21
74
|
export interface VueTestIdPluginOptions {
|
|
22
75
|
/**
|
|
23
76
|
* testId 属性名
|
|
@@ -26,32 +79,22 @@ export interface VueTestIdPluginOptions {
|
|
|
26
79
|
attributeName?: string;
|
|
27
80
|
}
|
|
28
81
|
/**
|
|
29
|
-
* Vite
|
|
30
|
-
* 当前仅作为占位,实际 testid 注入完全由运行时 `setupAntTestIds()` 完成。
|
|
82
|
+
* Vite 插件。
|
|
31
83
|
*
|
|
32
|
-
*
|
|
84
|
+
* 当前作为配置占位符存在;实际的 testid 注入由以下组件完成:
|
|
85
|
+
* - 编译期:`testIdTransforms()` → 注册到 `vue()` 的 compilerOptions
|
|
86
|
+
* - 运行时:`setupAntTestIds()` → 在 main.ts 中调用
|
|
33
87
|
*/
|
|
34
88
|
export declare function vueTestIdPlugin(_opts?: VueTestIdPluginOptions): Plugin;
|
|
35
89
|
/**
|
|
36
|
-
* @deprecated
|
|
37
|
-
* 不再需要编译期 AST transform。请从 vite.config.ts 中移除 `nodeTransforms` 配置,
|
|
38
|
-
* 改为在 main.ts 中调用 `setupAntTestIds()`。
|
|
39
|
-
*
|
|
40
|
-
* @example 迁移方式
|
|
41
|
-
* ```diff
|
|
42
|
-
* // vite.config.ts
|
|
43
|
-
* - import { createVueTestIdTransform } from 'vite-plugin-vue-testid'
|
|
44
|
-
* - vue({ template: { compilerOptions: { nodeTransforms: [
|
|
45
|
-
* - createVueTestIdTransform()
|
|
46
|
-
* - ]}}})
|
|
47
|
-
|
|
48
|
-
* + import vue from '@vitejs/plugin-vue'
|
|
49
|
-
* + vue()
|
|
90
|
+
* @deprecated 此函数已废弃。请使用新的编译期 API:
|
|
50
91
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { testIdTransforms } from 'vite-plugin-vue-testid'
|
|
94
|
+
* // 加入 vue() 的 compilerOptions.nodeTransforms
|
|
54
95
|
* ```
|
|
96
|
+
*
|
|
97
|
+
* 或仅使用运行时 `setupAntTestIds()`。
|
|
55
98
|
*/
|
|
56
99
|
export declare function createVueTestIdTransform(): () => void;
|
|
57
100
|
//# sourceMappingURL=index.d.ts.map
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;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;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;AAIpB,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,sBAAsB,GAAG,MAAM,CAItE;AAID;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,IAAI,CASrD"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,254 @@
|
|
|
1
|
+
// src/transform.ts
|
|
2
|
+
var NodeTypes = {
|
|
3
|
+
ROOT: 0,
|
|
4
|
+
ELEMENT: 1,
|
|
5
|
+
TEXT: 2,
|
|
6
|
+
COMMENT: 3,
|
|
7
|
+
SIMPLE_EXPRESSION: 4,
|
|
8
|
+
INTERPOLATION: 5,
|
|
9
|
+
ATTRIBUTE: 6,
|
|
10
|
+
DIRECTIVE: 7,
|
|
11
|
+
COMPOUND_EXPRESSION: 8,
|
|
12
|
+
IF: 9,
|
|
13
|
+
IF_BRANCH: 10,
|
|
14
|
+
FOR: 11,
|
|
15
|
+
TEXT_CALL: 12
|
|
16
|
+
};
|
|
17
|
+
var ElementTypes = {
|
|
18
|
+
ELEMENT: 0,
|
|
19
|
+
COMPONENT: 1,
|
|
20
|
+
SLOT: 2,
|
|
21
|
+
TEMPLATE: 3
|
|
22
|
+
};
|
|
23
|
+
var NATIVE_TAGS = /* @__PURE__ */ new Set([
|
|
24
|
+
"div",
|
|
25
|
+
"span",
|
|
26
|
+
"p",
|
|
27
|
+
"a",
|
|
28
|
+
"br",
|
|
29
|
+
"hr",
|
|
30
|
+
"img",
|
|
31
|
+
"input",
|
|
32
|
+
"button",
|
|
33
|
+
"select",
|
|
34
|
+
"option",
|
|
35
|
+
"textarea",
|
|
36
|
+
"label",
|
|
37
|
+
"form",
|
|
38
|
+
"table",
|
|
39
|
+
"tr",
|
|
40
|
+
"td",
|
|
41
|
+
"th",
|
|
42
|
+
"thead",
|
|
43
|
+
"tbody",
|
|
44
|
+
"tfoot",
|
|
45
|
+
"ul",
|
|
46
|
+
"ol",
|
|
47
|
+
"li",
|
|
48
|
+
"dl",
|
|
49
|
+
"dt",
|
|
50
|
+
"dd",
|
|
51
|
+
"h1",
|
|
52
|
+
"h2",
|
|
53
|
+
"h3",
|
|
54
|
+
"h4",
|
|
55
|
+
"h5",
|
|
56
|
+
"h6",
|
|
57
|
+
"section",
|
|
58
|
+
"article",
|
|
59
|
+
"header",
|
|
60
|
+
"footer",
|
|
61
|
+
"nav",
|
|
62
|
+
"aside",
|
|
63
|
+
"main",
|
|
64
|
+
"figure",
|
|
65
|
+
"figcaption",
|
|
66
|
+
"details",
|
|
67
|
+
"summary",
|
|
68
|
+
"code",
|
|
69
|
+
"pre",
|
|
70
|
+
"strong",
|
|
71
|
+
"em",
|
|
72
|
+
"i",
|
|
73
|
+
"b",
|
|
74
|
+
"u",
|
|
75
|
+
"s",
|
|
76
|
+
"small",
|
|
77
|
+
"sub",
|
|
78
|
+
"sup",
|
|
79
|
+
"blockquote",
|
|
80
|
+
"q",
|
|
81
|
+
"cite",
|
|
82
|
+
"abbr",
|
|
83
|
+
"time",
|
|
84
|
+
"address",
|
|
85
|
+
"fieldset",
|
|
86
|
+
"legend",
|
|
87
|
+
"progress",
|
|
88
|
+
"meter",
|
|
89
|
+
"iframe",
|
|
90
|
+
"svg",
|
|
91
|
+
"path",
|
|
92
|
+
"video",
|
|
93
|
+
"audio",
|
|
94
|
+
"canvas",
|
|
95
|
+
"template",
|
|
96
|
+
"slot",
|
|
97
|
+
"style",
|
|
98
|
+
"script",
|
|
99
|
+
"meta",
|
|
100
|
+
"link",
|
|
101
|
+
"title",
|
|
102
|
+
"head",
|
|
103
|
+
"html",
|
|
104
|
+
"body",
|
|
105
|
+
"noscript",
|
|
106
|
+
"map",
|
|
107
|
+
"area",
|
|
108
|
+
"base",
|
|
109
|
+
"col",
|
|
110
|
+
"colgroup",
|
|
111
|
+
"data",
|
|
112
|
+
"datalist",
|
|
113
|
+
"del",
|
|
114
|
+
"dfn",
|
|
115
|
+
"dialog",
|
|
116
|
+
"embed",
|
|
117
|
+
"ins",
|
|
118
|
+
"kbd",
|
|
119
|
+
"mark",
|
|
120
|
+
"math",
|
|
121
|
+
"menu",
|
|
122
|
+
"object",
|
|
123
|
+
"output",
|
|
124
|
+
"param",
|
|
125
|
+
"picture",
|
|
126
|
+
"pre",
|
|
127
|
+
"rp",
|
|
128
|
+
"rt",
|
|
129
|
+
"ruby",
|
|
130
|
+
"samp",
|
|
131
|
+
"search",
|
|
132
|
+
"source",
|
|
133
|
+
"track",
|
|
134
|
+
"var",
|
|
135
|
+
"wbr"
|
|
136
|
+
]);
|
|
137
|
+
function isAntdComponent(tag, antPrefix) {
|
|
138
|
+
if (!tag.startsWith(antPrefix)) return false;
|
|
139
|
+
if (tag === antPrefix.slice(0, -1)) return false;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
function isTargetComponent(el, antPrefix, customPrefixes) {
|
|
143
|
+
const { tag, tagType } = el;
|
|
144
|
+
if (tagType === ElementTypes.ELEMENT && NATIVE_TAGS.has(tag)) return false;
|
|
145
|
+
if (tagType === ElementTypes.SLOT || tagType === ElementTypes.TEMPLATE) return false;
|
|
146
|
+
if (isAntdComponent(tag, antPrefix)) return false;
|
|
147
|
+
if (tagType === ElementTypes.COMPONENT) return true;
|
|
148
|
+
if (customPrefixes.some((p) => tag.startsWith(p))) return true;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
function hasExistingTestId(el, attrName) {
|
|
152
|
+
return el.props.some((p) => p.type === NodeTypes.ATTRIBUTE && p.name === attrName);
|
|
153
|
+
}
|
|
154
|
+
function hasExistingAttrsBinding(el) {
|
|
155
|
+
return el.props.some(
|
|
156
|
+
(p) => p.type === NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "$attrs"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
function createStaticAttr(name, value, loc) {
|
|
160
|
+
return {
|
|
161
|
+
type: NodeTypes.ATTRIBUTE,
|
|
162
|
+
name,
|
|
163
|
+
value: {
|
|
164
|
+
type: NodeTypes.TEXT,
|
|
165
|
+
content: value,
|
|
166
|
+
loc: loc || {}
|
|
167
|
+
},
|
|
168
|
+
loc: loc || {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function createVBindAttrsDirective(loc) {
|
|
172
|
+
return {
|
|
173
|
+
type: NodeTypes.DIRECTIVE,
|
|
174
|
+
name: "bind",
|
|
175
|
+
arg: {
|
|
176
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
177
|
+
content: "$attrs",
|
|
178
|
+
isStatic: true,
|
|
179
|
+
constType: 3,
|
|
180
|
+
// CAN_STRINGIFY
|
|
181
|
+
loc: loc || {}
|
|
182
|
+
},
|
|
183
|
+
exp: {
|
|
184
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
185
|
+
content: "$attrs",
|
|
186
|
+
isStatic: false,
|
|
187
|
+
constType: 0,
|
|
188
|
+
// NOT_CONSTANT
|
|
189
|
+
loc: loc || {}
|
|
190
|
+
},
|
|
191
|
+
modifiers: [],
|
|
192
|
+
loc: loc || {}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function createMultiRootFixTransform(opts = {}) {
|
|
196
|
+
const { antPrefix = "a-" } = opts;
|
|
197
|
+
return (node, _context) => {
|
|
198
|
+
if (node.type !== NodeTypes.ROOT) return;
|
|
199
|
+
const root = node;
|
|
200
|
+
const elementChildren = root.children.filter(
|
|
201
|
+
(c) => c.type === NodeTypes.ELEMENT
|
|
202
|
+
);
|
|
203
|
+
if (elementChildren.length <= 1) return;
|
|
204
|
+
const hasManualBinding = elementChildren.some(
|
|
205
|
+
(el) => hasExistingAttrsBinding(el)
|
|
206
|
+
);
|
|
207
|
+
if (hasManualBinding) return;
|
|
208
|
+
for (const el of elementChildren) {
|
|
209
|
+
if (isAntdComponent(el.tag, antPrefix)) {
|
|
210
|
+
if (hasExistingAttrsBinding(el)) return;
|
|
211
|
+
el.props.push(createVBindAttrsDirective(el.loc));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function createTestIdInjectTransform(opts = {}) {
|
|
218
|
+
const {
|
|
219
|
+
attributeName = "data-testid",
|
|
220
|
+
antPrefix = "a-",
|
|
221
|
+
customPrefixes = []
|
|
222
|
+
} = opts;
|
|
223
|
+
const templateCounters = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
return (node, context) => {
|
|
225
|
+
if (node.type === NodeTypes.ROOT) {
|
|
226
|
+
templateCounters.set(node, /* @__PURE__ */ new Map());
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (node.type !== NodeTypes.ELEMENT) return;
|
|
230
|
+
const el = node;
|
|
231
|
+
if (!isTargetComponent(el, antPrefix, customPrefixes)) return;
|
|
232
|
+
if (hasExistingTestId(el, attributeName)) return;
|
|
233
|
+
const counters = templateCounters.get(context.root);
|
|
234
|
+
const tagName = el.tag;
|
|
235
|
+
const index = counters.get(tagName) ?? 0;
|
|
236
|
+
counters.set(tagName, index + 1);
|
|
237
|
+
const testId = `${tagName}-${index}`;
|
|
238
|
+
el.props.push(createStaticAttr(attributeName, testId, el.loc));
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function createTestIdTransforms(opts = {}) {
|
|
242
|
+
return [
|
|
243
|
+
// multiRootFix 必须先注册,因为它的 binding 应出现在
|
|
244
|
+
// testId inject 之前(编译顺序无关,但逻辑上先修复再注入更清晰)
|
|
245
|
+
createMultiRootFixTransform(opts),
|
|
246
|
+
createTestIdInjectTransform(opts)
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
1
250
|
// src/index.ts
|
|
251
|
+
var testIdTransforms = createTestIdTransforms;
|
|
2
252
|
function vueTestIdPlugin(_opts) {
|
|
3
253
|
return {
|
|
4
254
|
name: "vite-plugin-vue-testid"
|
|
@@ -7,13 +257,17 @@ function vueTestIdPlugin(_opts) {
|
|
|
7
257
|
function createVueTestIdTransform() {
|
|
8
258
|
if (typeof console !== "undefined") {
|
|
9
259
|
console.warn(
|
|
10
|
-
"[vite-plugin-vue-testid] createVueTestIdTransform() is deprecated.
|
|
260
|
+
"[vite-plugin-vue-testid] createVueTestIdTransform() is deprecated. Use testIdTransforms() in vue() compilerOptions for compile-time injection, or setupAntTestIds() in main.ts for runtime injection."
|
|
11
261
|
);
|
|
12
262
|
}
|
|
13
263
|
return () => {
|
|
14
264
|
};
|
|
15
265
|
}
|
|
16
266
|
export {
|
|
267
|
+
createMultiRootFixTransform,
|
|
268
|
+
createTestIdInjectTransform,
|
|
269
|
+
createTestIdTransforms,
|
|
17
270
|
createVueTestIdTransform,
|
|
271
|
+
testIdTransforms,
|
|
18
272
|
vueTestIdPlugin
|
|
19
273
|
};
|
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;AA6lB7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,IAAI,CA6ErE;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,wBAAkB,CAAA;AAMtD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,cAAmB,GAAG,IAAI,CAgCtE"}
|
package/dist/runtime.js
CHANGED
|
@@ -58,7 +58,7 @@ function injectComponentRoots(root, matchers, attrName, counter, skipSelectors)
|
|
|
58
58
|
const candidates = root.matches(selector) ? [root] : Array.from(root.querySelectorAll(selector));
|
|
59
59
|
for (const el of candidates) {
|
|
60
60
|
if (!(el instanceof HTMLElement)) continue;
|
|
61
|
-
if (el
|
|
61
|
+
if (hasAttrCompat(el, attrName)) continue;
|
|
62
62
|
if (isInnerInput(el, skipSelectors)) continue;
|
|
63
63
|
const testId = `${prefix}-${counter.value++}`;
|
|
64
64
|
el.setAttribute(attrName, testId);
|
|
@@ -94,7 +94,7 @@ function injectHeaderViewButtons(panel, prefix, attrName, suffix, pc) {
|
|
|
94
94
|
const buttons = headerView.querySelectorAll("button");
|
|
95
95
|
buttons.forEach((btn) => {
|
|
96
96
|
const el = btn;
|
|
97
|
-
if (el
|
|
97
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
98
98
|
const text = el.textContent?.trim().toLowerCase() || "";
|
|
99
99
|
let name;
|
|
100
100
|
if (MONTH_NAMES.some((m) => text.startsWith(m))) {
|
|
@@ -116,7 +116,7 @@ function injectPanelCells(panel, prefix, attrName, suffix, pc) {
|
|
|
116
116
|
const cells = panel.querySelectorAll(sel(pc, "picker-cell-inner"));
|
|
117
117
|
cells.forEach((cell) => {
|
|
118
118
|
const el = cell;
|
|
119
|
-
if (el
|
|
119
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
120
120
|
const raw = (el.getAttribute("title") || el.textContent?.trim() || "").replace(/\s+/g, "-");
|
|
121
121
|
switch (mode) {
|
|
122
122
|
case "month":
|
|
@@ -138,7 +138,7 @@ function injectSinglePanel(panel, prefix, attrName, suffix, pc) {
|
|
|
138
138
|
const mode = getPanelMode(panel, pc);
|
|
139
139
|
const setAttr = (selector, name) => {
|
|
140
140
|
const el = panel.querySelector(selector);
|
|
141
|
-
if (el && !el
|
|
141
|
+
if (el && !hasAttrCompat(el, attrName)) {
|
|
142
142
|
el.setAttribute(attrName, `${prefix}-${name}${suffix}`);
|
|
143
143
|
}
|
|
144
144
|
};
|
|
@@ -168,26 +168,26 @@ function makeInjectPickerPanel(pc) {
|
|
|
168
168
|
const presets = container.querySelectorAll(`${sel(pc, "picker-presets")} .${cls(pc, "tag")}`);
|
|
169
169
|
presets.forEach((tag) => {
|
|
170
170
|
const el = tag;
|
|
171
|
-
if (el
|
|
171
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
172
172
|
const text = el.textContent?.trim() || "";
|
|
173
173
|
el.setAttribute(attrName, `${prefix}-preset-${text}`);
|
|
174
174
|
});
|
|
175
175
|
const timeColumns = container.querySelectorAll(sel(pc, "picker-time-panel-column"));
|
|
176
176
|
timeColumns.forEach((col, i) => {
|
|
177
177
|
const el = col;
|
|
178
|
-
if (el
|
|
178
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
179
179
|
el.setAttribute(attrName, `${prefix}-time-column-${i}`);
|
|
180
180
|
const items = col.querySelectorAll("li");
|
|
181
181
|
items.forEach((item) => {
|
|
182
182
|
const li = item;
|
|
183
|
-
if (li
|
|
183
|
+
if (hasAttrCompat(li, attrName)) return;
|
|
184
184
|
const text = li.textContent?.trim() || "";
|
|
185
185
|
li.setAttribute(attrName, `${prefix}-time-${i}-${text}`);
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
const setFooterBtn = (selector, name) => {
|
|
189
189
|
const btn = container.querySelector(selector);
|
|
190
|
-
if (btn && !btn
|
|
190
|
+
if (btn && !hasAttrCompat(btn, attrName)) {
|
|
191
191
|
btn.setAttribute(attrName, `${prefix}-${name}`);
|
|
192
192
|
}
|
|
193
193
|
};
|
|
@@ -236,7 +236,7 @@ function makeInjectSelectPanel(pc) {
|
|
|
236
236
|
const options = container.querySelectorAll(sel(pc, "select-item-option"));
|
|
237
237
|
options.forEach((opt) => {
|
|
238
238
|
const el = opt;
|
|
239
|
-
if (el
|
|
239
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
240
240
|
const title = el.getAttribute("title") || el.textContent?.trim() || "";
|
|
241
241
|
el.setAttribute(attrName, `${prefix}-option-${title}`);
|
|
242
242
|
});
|
|
@@ -245,11 +245,11 @@ function makeInjectSelectPanel(pc) {
|
|
|
245
245
|
const el = node;
|
|
246
246
|
const content = el.querySelector(sel(pc, "select-tree-node-content-wrapper"));
|
|
247
247
|
const title = content?.getAttribute("title") || content?.textContent?.trim() || "";
|
|
248
|
-
if (content && !content
|
|
248
|
+
if (content && !hasAttrCompat(content, attrName)) {
|
|
249
249
|
content.setAttribute(attrName, `${prefix}-tree-${title}`);
|
|
250
250
|
}
|
|
251
251
|
const switcher = el.querySelector(sel(pc, "select-tree-switcher"));
|
|
252
|
-
if (switcher && !switcher
|
|
252
|
+
if (switcher && !hasAttrCompat(switcher, attrName)) {
|
|
253
253
|
const isOpen = switcher.classList.contains(cls(pc, "select-tree-switcher_open"));
|
|
254
254
|
const state = isOpen ? "collapse" : "expand";
|
|
255
255
|
switcher.setAttribute(attrName, `${prefix}-tree-switcher-${state}-${title}`);
|
|
@@ -288,7 +288,7 @@ function injectMenuItems(attrName, root = document, pc) {
|
|
|
288
288
|
const items = root instanceof HTMLElement && root.matches(rule.selector) ? [root, ...descendants] : descendants;
|
|
289
289
|
for (const item of items) {
|
|
290
290
|
const el = item;
|
|
291
|
-
if (el
|
|
291
|
+
if (hasAttrCompat(el, attrName)) return;
|
|
292
292
|
const id = el.getAttribute("id");
|
|
293
293
|
if (id) {
|
|
294
294
|
el.setAttribute(attrName, id);
|
|
@@ -303,56 +303,82 @@ function injectMenuItems(attrName, root = document, pc) {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
var _observedContainers = /* @__PURE__ */ new WeakSet();
|
|
306
|
+
function getAttrCompat(el, name) {
|
|
307
|
+
const value = el.getAttribute(name);
|
|
308
|
+
if (value !== null) return value;
|
|
309
|
+
const target = name.toLowerCase();
|
|
310
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
311
|
+
const attr = el.attributes[i];
|
|
312
|
+
if (attr.name.toLowerCase() === target) return attr.value;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
function hasAttrCompat(el, name) {
|
|
317
|
+
return getAttrCompat(el, name) !== null;
|
|
318
|
+
}
|
|
306
319
|
function getTriggerTestId(trigger, attrName) {
|
|
307
320
|
if (!trigger) return "unknown";
|
|
308
|
-
return trigger
|
|
321
|
+
return getAttrCompat(trigger, attrName) || trigger.getAttribute("id") || "unknown";
|
|
322
|
+
}
|
|
323
|
+
function resolveTriggerInContainer(el, attrName) {
|
|
324
|
+
if (hasAttrCompat(el, attrName)) return el;
|
|
325
|
+
const children = el.querySelectorAll("[id], [data-testid], [data-testId], [data-testID]");
|
|
326
|
+
for (const child of children) {
|
|
327
|
+
if (child instanceof HTMLElement && hasAttrCompat(child, attrName)) return child;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
309
330
|
}
|
|
310
331
|
function findActiveTrigger(attrName, pc) {
|
|
311
332
|
const focused = document.querySelector(sel(pc, "picker-focused"));
|
|
312
333
|
if (focused) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (inner) return inner;
|
|
334
|
+
const found = resolveTriggerInContainer(focused, attrName);
|
|
335
|
+
if (found) return found;
|
|
316
336
|
const input = focused.querySelector("input[id]");
|
|
317
337
|
if (input) return input;
|
|
318
338
|
}
|
|
319
339
|
const selectOpen = document.querySelector(sel(pc, "select-open"));
|
|
320
|
-
if (selectOpen
|
|
340
|
+
if (selectOpen) {
|
|
341
|
+
const found = resolveTriggerInContainer(selectOpen, attrName);
|
|
342
|
+
if (found) return found;
|
|
343
|
+
}
|
|
321
344
|
const cascader = document.querySelector(sel(pc, "cascader-picker-focused"));
|
|
322
|
-
if (cascader
|
|
345
|
+
if (cascader) {
|
|
346
|
+
const found = resolveTriggerInContainer(cascader, attrName);
|
|
347
|
+
if (found) return found;
|
|
348
|
+
}
|
|
323
349
|
const expanded = document.querySelector(
|
|
324
|
-
`[
|
|
350
|
+
`[id][aria-expanded="true"], [data-testid][aria-expanded="true"], [data-testId][aria-expanded="true"]`
|
|
325
351
|
);
|
|
326
|
-
if (expanded) return expanded;
|
|
352
|
+
if (expanded && hasAttrCompat(expanded, attrName)) return expanded;
|
|
327
353
|
return null;
|
|
328
354
|
}
|
|
329
355
|
function injectSubElements(root, attrName, pc) {
|
|
330
356
|
const inputNumbers = root.matches(`${sel(pc, "input-number")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "input-number")}[${attrName}]`));
|
|
331
357
|
for (const wrapper of inputNumbers) {
|
|
332
358
|
if (!(wrapper instanceof HTMLElement)) continue;
|
|
333
|
-
const testId = wrapper
|
|
359
|
+
const testId = getAttrCompat(wrapper, attrName) || "";
|
|
334
360
|
if (!testId) continue;
|
|
335
361
|
const input = wrapper.querySelector(sel(pc, "input-number-input"));
|
|
336
|
-
if (input && !input
|
|
362
|
+
if (input && !hasAttrCompat(input, attrName)) {
|
|
337
363
|
input.setAttribute(attrName, testId);
|
|
338
364
|
}
|
|
339
365
|
const upHandler = wrapper.querySelector(sel(pc, "input-number-handler-up"));
|
|
340
|
-
if (upHandler && !upHandler
|
|
366
|
+
if (upHandler && !hasAttrCompat(upHandler, attrName)) {
|
|
341
367
|
upHandler.setAttribute(attrName, `${testId}-up`);
|
|
342
368
|
}
|
|
343
369
|
const downHandler = wrapper.querySelector(sel(pc, "input-number-handler-down"));
|
|
344
|
-
if (downHandler && !downHandler
|
|
370
|
+
if (downHandler && !hasAttrCompat(downHandler, attrName)) {
|
|
345
371
|
downHandler.setAttribute(attrName, `${testId}-down`);
|
|
346
372
|
}
|
|
347
373
|
}
|
|
348
374
|
const sliders = root.matches(`${sel(pc, "slider")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "slider")}[${attrName}]`));
|
|
349
375
|
for (const slider of sliders) {
|
|
350
376
|
if (!(slider instanceof HTMLElement)) continue;
|
|
351
|
-
const testId = slider
|
|
377
|
+
const testId = getAttrCompat(slider, attrName) || "";
|
|
352
378
|
if (!testId) continue;
|
|
353
379
|
slider.querySelectorAll(sel(pc, "slider-handle")).forEach((h, i) => {
|
|
354
380
|
const el = h;
|
|
355
|
-
if (!el
|
|
381
|
+
if (!hasAttrCompat(el, attrName)) {
|
|
356
382
|
el.setAttribute(attrName, `${testId}-handle-${i}`);
|
|
357
383
|
}
|
|
358
384
|
});
|
|
@@ -360,11 +386,11 @@ function injectSubElements(root, attrName, pc) {
|
|
|
360
386
|
const rates = root.matches(`${sel(pc, "rate")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "rate")}[${attrName}]`));
|
|
361
387
|
for (const rate of rates) {
|
|
362
388
|
if (!(rate instanceof HTMLElement)) continue;
|
|
363
|
-
const testId = rate
|
|
389
|
+
const testId = getAttrCompat(rate, attrName) || "";
|
|
364
390
|
if (!testId) continue;
|
|
365
391
|
rate.querySelectorAll(sel(pc, "rate-star")).forEach((s, i) => {
|
|
366
392
|
const el = s;
|
|
367
|
-
if (!el
|
|
393
|
+
if (!hasAttrCompat(el, attrName)) {
|
|
368
394
|
el.setAttribute(attrName, `${testId}-star-${i}`);
|
|
369
395
|
}
|
|
370
396
|
});
|
|
@@ -373,12 +399,12 @@ function injectSubElements(root, attrName, pc) {
|
|
|
373
399
|
const uploadById = uploadByTestId.length === 0 ? root.matches(`${sel(pc, "upload")}[id]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "upload")}[id]`)) : [];
|
|
374
400
|
for (const upload of [...uploadByTestId, ...uploadById]) {
|
|
375
401
|
if (!(upload instanceof HTMLElement)) continue;
|
|
376
|
-
const testId = upload
|
|
402
|
+
const testId = getAttrCompat(upload, attrName) || upload.getAttribute("id") || "";
|
|
377
403
|
if (!testId) continue;
|
|
378
404
|
const input = upload.querySelector('input[type="file"]');
|
|
379
|
-
if (input && !input
|
|
405
|
+
if (input && !hasAttrCompat(input, attrName)) {
|
|
380
406
|
input.setAttribute(attrName, `${testId}-input`);
|
|
381
|
-
if (!upload
|
|
407
|
+
if (!hasAttrCompat(upload, attrName)) {
|
|
382
408
|
upload.setAttribute(attrName, testId);
|
|
383
409
|
}
|
|
384
410
|
}
|
|
@@ -399,7 +425,7 @@ function setupAntTestIds(opts = {}) {
|
|
|
399
425
|
const containers = node.matches(selector) ? [node] : Array.from(node.querySelectorAll(selector));
|
|
400
426
|
for (const container of containers) {
|
|
401
427
|
if (!(container instanceof HTMLElement)) continue;
|
|
402
|
-
if (container
|
|
428
|
+
if (hasAttrCompat(container, attrName)) continue;
|
|
403
429
|
if (strategy) {
|
|
404
430
|
queueMicrotask(() => {
|
|
405
431
|
const trigger = findActiveTrigger(attrName, pc);
|
|
@@ -452,7 +478,7 @@ function injectCurrentDropdowns(opts = {}) {
|
|
|
452
478
|
const containers = document.querySelectorAll(selector);
|
|
453
479
|
containers.forEach((container) => {
|
|
454
480
|
if (!(container instanceof HTMLElement)) return;
|
|
455
|
-
if (container
|
|
481
|
+
if (hasAttrCompat(container, attrName)) return;
|
|
456
482
|
if (!strategy) return;
|
|
457
483
|
const trigger = findActiveTrigger(attrName, pc);
|
|
458
484
|
const triggerTestId = getTriggerTestId(trigger, attrName);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编译期模板 transform。
|
|
3
|
+
*
|
|
4
|
+
* 两个独立的 NodeTransform:
|
|
5
|
+
* 1. multiRootFixTransform — 多根子组件上自动补 v-bind="$attrs"
|
|
6
|
+
* 确保 data-testid 能穿透 Fragment 落到 antd 组件 DOM 上。
|
|
7
|
+
* 2. testIdInjectTransform — 在父模板中为每个组件使用点注入唯一的
|
|
8
|
+
* data-testid="{tagName}-{index}",保证同一页面中同类型组件的
|
|
9
|
+
* testid 全局唯一且稳定。
|
|
10
|
+
*
|
|
11
|
+
* 两个 transform 均通过 compileTime: true 下的 createTestIdTransforms() 一键获得。
|
|
12
|
+
*/
|
|
13
|
+
import type { NodeTransform } from '@vue/compiler-core';
|
|
14
|
+
export interface TransformOptions {
|
|
15
|
+
/** testid 属性名,默认 'data-testid' */
|
|
16
|
+
attributeName?: string;
|
|
17
|
+
/** ant-design-vue 的 tag 前缀,默认 'a-' */
|
|
18
|
+
antPrefix?: string;
|
|
19
|
+
/** 额外需要注入 testid 的自定义组件前缀 */
|
|
20
|
+
customPrefixes?: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 转换多根子组件的模板,自动给第一个 antd 组件根元素加 v-bind="$attrs"。
|
|
24
|
+
*
|
|
25
|
+
* 为什么需要:多根组件(Fragment)的 inheritAttrs 不生效,父模板编译期
|
|
26
|
+
* 注入的 data-testid 属性会被丢弃。本 transform 确保至少一个 antd 组件根
|
|
27
|
+
* 能收到透传属性。
|
|
28
|
+
*
|
|
29
|
+
* 安全策略:
|
|
30
|
+
* - 先检查用户是否已手动写了任一 v-bind="$attrs",有则不动
|
|
31
|
+
* - 只在模板是多根且至少有一个 antd 组件时才注入
|
|
32
|
+
*/
|
|
33
|
+
export declare function createMultiRootFixTransform(opts?: TransformOptions): NodeTransform;
|
|
34
|
+
/**
|
|
35
|
+
* 在模板编译期为所有(antd + 自定义)组件注入唯一的 data-testid。
|
|
36
|
+
*
|
|
37
|
+
* 格式:`{tagName}-{perTemplateIndex}`
|
|
38
|
+
* 例:`myinput-0`, `myinput-1`, `a-select-0`, `a-button-2`
|
|
39
|
+
*
|
|
40
|
+
* key 设计:
|
|
41
|
+
* - tagName 保持原始大小写,确保一眼认出组件来源
|
|
42
|
+
* - index 是严格按模板 AST 中的出现顺序递增的,不同模板各自重置
|
|
43
|
+
* - 如果用户已经手动写了 data-testid,则跳过(不覆盖)
|
|
44
|
+
*/
|
|
45
|
+
export declare function createTestIdInjectTransform(opts?: TransformOptions): NodeTransform;
|
|
46
|
+
/**
|
|
47
|
+
* 返回编译期所需的全部 NodeTransform。
|
|
48
|
+
*
|
|
49
|
+
* 使用方法:
|
|
50
|
+
* ```ts
|
|
51
|
+
* // vite.config.ts
|
|
52
|
+
* import vue from '@vitejs/plugin-vue'
|
|
53
|
+
* import { testIdTransforms } from 'vite-plugin-vue-testid'
|
|
54
|
+
*
|
|
55
|
+
* export default defineConfig({
|
|
56
|
+
* plugins: [
|
|
57
|
+
* vue({
|
|
58
|
+
* template: {
|
|
59
|
+
* compilerOptions: {
|
|
60
|
+
* nodeTransforms: testIdTransforms()
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* }),
|
|
64
|
+
* ]
|
|
65
|
+
* })
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function createTestIdTransforms(opts?: TransformOptions): NodeTransform[];
|
|
69
|
+
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,aAAa,EAMd,MAAM,oBAAoB,CAAA;AAgD3B,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B;AAyFD;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,CA8BtF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,CAqCtF;AAID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,gBAAqB,GAAG,aAAa,EAAE,CAOnF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-vue-testid",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"vue": ">=3.3.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
+
"@vue/compiler-core": "^3.3.0",
|
|
46
47
|
"tsup": "^8.5.1",
|
|
47
48
|
"typescript": "~6.0.2",
|
|
48
49
|
"vite": "^8.0.12"
|