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 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
- * export default defineConfig({ plugins: [vue()] })
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 此函数已废弃。testid 注入现在完全在运行时进行,
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
- * // main.ts
52
- * + import { setupAntTestIds } from 'vite-plugin-vue-testid/runtime'
53
- * + setupAntTestIds()
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAIlC,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAIlF,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,sBAAsB,GAAG,MAAM,CAItE;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,IAAI,CASrD"}
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. Remove nodeTransforms from vite.config.ts and call setupAntTestIds() in main.ts instead. See https://github.com/... for migration guide."
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
  };
@@ -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;AAsjB7D;;;;;;;;;;;;;;;;;;;;;;;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"}
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.hasAttribute(attrName)) continue;
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) return;
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) return;
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.getAttribute(attrName) || trigger.getAttribute("id") || "unknown";
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
- if (focused.hasAttribute(attrName)) return focused;
314
- const inner = focused.querySelector(`[${attrName}]`);
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?.hasAttribute(attrName)) return 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?.hasAttribute(attrName)) return cascader;
345
+ if (cascader) {
346
+ const found = resolveTriggerInContainer(cascader, attrName);
347
+ if (found) return found;
348
+ }
323
349
  const expanded = document.querySelector(
324
- `[${attrName}][aria-expanded="true"]`
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.getAttribute(attrName) || "";
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) {
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.hasAttribute(attrName)) {
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.getAttribute(attrName) || "";
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.hasAttribute(attrName)) {
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.getAttribute(attrName) || "";
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.hasAttribute(attrName)) {
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.getAttribute(attrName) || upload.getAttribute("id") || "";
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.hasAttribute(attrName)) {
405
+ if (input && !hasAttrCompat(input, attrName)) {
380
406
  input.setAttribute(attrName, `${testId}-input`);
381
- if (!upload.hasAttribute(attrName)) {
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.hasAttribute(attrName)) continue;
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.hasAttribute(attrName)) return;
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.11",
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"