vite-plugin-vue-testid 1.0.12 → 1.1.1

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;AAukB7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,IAAI,CAwFrE;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;AAqpB7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,IAAI,CAgFrE;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,wBAAkB,CAAA;AAMtD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,cAAmB,GAAG,IAAI,CAmCtE"}
package/dist/runtime.js CHANGED
@@ -30,6 +30,8 @@ function buildComponentMatchers(pc) {
30
30
  { selector: sel(pc, "modal-wrap"), prefix: "a-modal", withId: true },
31
31
  // ── 菜单 ──
32
32
  { selector: sel(pc, "menu"), prefix: "a-menu" },
33
+ // ── 标签页 ──
34
+ { selector: sel(pc, "tabs"), prefix: "a-tabs" },
33
35
  // ── 纯 input / textarea(必须排在最后)──
34
36
  { selector: `input.${cls(pc, "input")}`, prefix: "a-input" },
35
37
  { selector: `textarea.${cls(pc, "input")}`, prefix: "a-textarea" }
@@ -58,7 +60,7 @@ function injectComponentRoots(root, matchers, attrName, counter, skipSelectors)
58
60
  const candidates = root.matches(selector) ? [root] : Array.from(root.querySelectorAll(selector));
59
61
  for (const el of candidates) {
60
62
  if (!(el instanceof HTMLElement)) continue;
61
- if (el.hasAttribute(attrName)) continue;
63
+ if (hasAttrCompat(el, attrName)) continue;
62
64
  if (isInnerInput(el, skipSelectors)) continue;
63
65
  const testId = `${prefix}-${counter.value++}`;
64
66
  el.setAttribute(attrName, testId);
@@ -94,7 +96,7 @@ function injectHeaderViewButtons(panel, prefix, attrName, suffix, pc) {
94
96
  const buttons = headerView.querySelectorAll("button");
95
97
  buttons.forEach((btn) => {
96
98
  const el = btn;
97
- if (el.hasAttribute(attrName)) return;
99
+ if (hasAttrCompat(el, attrName)) return;
98
100
  const text = el.textContent?.trim().toLowerCase() || "";
99
101
  let name;
100
102
  if (MONTH_NAMES.some((m) => text.startsWith(m))) {
@@ -116,7 +118,7 @@ function injectPanelCells(panel, prefix, attrName, suffix, pc) {
116
118
  const cells = panel.querySelectorAll(sel(pc, "picker-cell-inner"));
117
119
  cells.forEach((cell) => {
118
120
  const el = cell;
119
- if (el.hasAttribute(attrName)) return;
121
+ if (hasAttrCompat(el, attrName)) return;
120
122
  const raw = (el.getAttribute("title") || el.textContent?.trim() || "").replace(/\s+/g, "-");
121
123
  switch (mode) {
122
124
  case "month":
@@ -138,7 +140,7 @@ function injectSinglePanel(panel, prefix, attrName, suffix, pc) {
138
140
  const mode = getPanelMode(panel, pc);
139
141
  const setAttr = (selector, name) => {
140
142
  const el = panel.querySelector(selector);
141
- if (el && !el.hasAttribute(attrName)) {
143
+ if (el && !hasAttrCompat(el, attrName)) {
142
144
  el.setAttribute(attrName, `${prefix}-${name}${suffix}`);
143
145
  }
144
146
  };
@@ -168,26 +170,26 @@ function makeInjectPickerPanel(pc) {
168
170
  const presets = container.querySelectorAll(`${sel(pc, "picker-presets")} .${cls(pc, "tag")}`);
169
171
  presets.forEach((tag) => {
170
172
  const el = tag;
171
- if (el.hasAttribute(attrName)) return;
173
+ if (hasAttrCompat(el, attrName)) return;
172
174
  const text = el.textContent?.trim() || "";
173
175
  el.setAttribute(attrName, `${prefix}-preset-${text}`);
174
176
  });
175
177
  const timeColumns = container.querySelectorAll(sel(pc, "picker-time-panel-column"));
176
178
  timeColumns.forEach((col, i) => {
177
179
  const el = col;
178
- if (el.hasAttribute(attrName)) return;
180
+ if (hasAttrCompat(el, attrName)) return;
179
181
  el.setAttribute(attrName, `${prefix}-time-column-${i}`);
180
182
  const items = col.querySelectorAll("li");
181
183
  items.forEach((item) => {
182
184
  const li = item;
183
- if (li.hasAttribute(attrName)) return;
185
+ if (hasAttrCompat(li, attrName)) return;
184
186
  const text = li.textContent?.trim() || "";
185
187
  li.setAttribute(attrName, `${prefix}-time-${i}-${text}`);
186
188
  });
187
189
  });
188
190
  const setFooterBtn = (selector, name) => {
189
191
  const btn = container.querySelector(selector);
190
- if (btn && !btn.hasAttribute(attrName)) {
192
+ if (btn && !hasAttrCompat(btn, attrName)) {
191
193
  btn.setAttribute(attrName, `${prefix}-${name}`);
192
194
  }
193
195
  };
@@ -236,7 +238,7 @@ function makeInjectSelectPanel(pc) {
236
238
  const options = container.querySelectorAll(sel(pc, "select-item-option"));
237
239
  options.forEach((opt) => {
238
240
  const el = opt;
239
- if (el.hasAttribute(attrName)) return;
241
+ if (hasAttrCompat(el, attrName)) return;
240
242
  const title = el.getAttribute("title") || el.textContent?.trim() || "";
241
243
  el.setAttribute(attrName, `${prefix}-option-${title}`);
242
244
  });
@@ -245,11 +247,11 @@ function makeInjectSelectPanel(pc) {
245
247
  const el = node;
246
248
  const content = el.querySelector(sel(pc, "select-tree-node-content-wrapper"));
247
249
  const title = content?.getAttribute("title") || content?.textContent?.trim() || "";
248
- if (content && !content.hasAttribute(attrName)) {
250
+ if (content && !hasAttrCompat(content, attrName)) {
249
251
  content.setAttribute(attrName, `${prefix}-tree-${title}`);
250
252
  }
251
253
  const switcher = el.querySelector(sel(pc, "select-tree-switcher"));
252
- if (switcher && !switcher.hasAttribute(attrName)) {
254
+ if (switcher && !hasAttrCompat(switcher, attrName)) {
253
255
  const isOpen = switcher.classList.contains(cls(pc, "select-tree-switcher_open"));
254
256
  const state = isOpen ? "collapse" : "expand";
255
257
  switcher.setAttribute(attrName, `${prefix}-tree-switcher-${state}-${title}`);
@@ -288,7 +290,7 @@ function injectMenuItems(attrName, root = document, pc) {
288
290
  const items = root instanceof HTMLElement && root.matches(rule.selector) ? [root, ...descendants] : descendants;
289
291
  for (const item of items) {
290
292
  const el = item;
291
- if (el.hasAttribute(attrName)) return;
293
+ if (hasAttrCompat(el, attrName)) return;
292
294
  const id = el.getAttribute("id");
293
295
  if (id) {
294
296
  el.setAttribute(attrName, id);
@@ -303,67 +305,82 @@ function injectMenuItems(attrName, root = document, pc) {
303
305
  }
304
306
  }
305
307
  var _observedContainers = /* @__PURE__ */ new WeakSet();
308
+ function getAttrCompat(el, name) {
309
+ const value = el.getAttribute(name);
310
+ if (value !== null) return value;
311
+ const target = name.toLowerCase();
312
+ for (let i = 0; i < el.attributes.length; i++) {
313
+ const attr = el.attributes[i];
314
+ if (attr.name.toLowerCase() === target) return attr.value;
315
+ }
316
+ return null;
317
+ }
318
+ function hasAttrCompat(el, name) {
319
+ return getAttrCompat(el, name) !== null;
320
+ }
306
321
  function getTriggerTestId(trigger, attrName) {
307
322
  if (!trigger) return "unknown";
308
- return trigger.getAttribute(attrName) || trigger.getAttribute("id") || "unknown";
323
+ return getAttrCompat(trigger, attrName) || trigger.getAttribute("id") || "unknown";
324
+ }
325
+ function resolveTriggerInContainer(el, attrName) {
326
+ if (hasAttrCompat(el, attrName)) return el;
327
+ const children = el.querySelectorAll("[id], [data-testid], [data-testId], [data-testID]");
328
+ for (const child of children) {
329
+ if (child instanceof HTMLElement && hasAttrCompat(child, attrName)) return child;
330
+ }
331
+ return null;
309
332
  }
310
333
  function findActiveTrigger(attrName, pc) {
311
- const resolveInContainer = (el) => {
312
- if (el.hasAttribute(attrName)) return el;
313
- const inner = el.querySelector(`[${attrName}]`);
314
- if (inner) return inner;
315
- return null;
316
- };
317
334
  const focused = document.querySelector(sel(pc, "picker-focused"));
318
335
  if (focused) {
319
- const found = resolveInContainer(focused);
336
+ const found = resolveTriggerInContainer(focused, attrName);
320
337
  if (found) return found;
321
338
  const input = focused.querySelector("input[id]");
322
339
  if (input) return input;
323
340
  }
324
341
  const selectOpen = document.querySelector(sel(pc, "select-open"));
325
342
  if (selectOpen) {
326
- const found = resolveInContainer(selectOpen);
343
+ const found = resolveTriggerInContainer(selectOpen, attrName);
327
344
  if (found) return found;
328
345
  }
329
346
  const cascader = document.querySelector(sel(pc, "cascader-picker-focused"));
330
347
  if (cascader) {
331
- const found = resolveInContainer(cascader);
348
+ const found = resolveTriggerInContainer(cascader, attrName);
332
349
  if (found) return found;
333
350
  }
334
351
  const expanded = document.querySelector(
335
- `[${attrName}][aria-expanded="true"]`
352
+ `[id][aria-expanded="true"], [data-testid][aria-expanded="true"], [data-testId][aria-expanded="true"]`
336
353
  );
337
- if (expanded) return expanded;
354
+ if (expanded && hasAttrCompat(expanded, attrName)) return expanded;
338
355
  return null;
339
356
  }
340
357
  function injectSubElements(root, attrName, pc) {
341
358
  const inputNumbers = root.matches(`${sel(pc, "input-number")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "input-number")}[${attrName}]`));
342
359
  for (const wrapper of inputNumbers) {
343
360
  if (!(wrapper instanceof HTMLElement)) continue;
344
- const testId = wrapper.getAttribute(attrName) || "";
361
+ const testId = getAttrCompat(wrapper, attrName) || "";
345
362
  if (!testId) continue;
346
363
  const input = wrapper.querySelector(sel(pc, "input-number-input"));
347
- if (input && !input.hasAttribute(attrName)) {
348
- input.setAttribute(attrName, testId);
364
+ if (input && !hasAttrCompat(input, attrName)) {
365
+ input.setAttribute(attrName, `${testId}-input`);
349
366
  }
350
367
  const upHandler = wrapper.querySelector(sel(pc, "input-number-handler-up"));
351
- if (upHandler && !upHandler.hasAttribute(attrName)) {
368
+ if (upHandler && !hasAttrCompat(upHandler, attrName)) {
352
369
  upHandler.setAttribute(attrName, `${testId}-up`);
353
370
  }
354
371
  const downHandler = wrapper.querySelector(sel(pc, "input-number-handler-down"));
355
- if (downHandler && !downHandler.hasAttribute(attrName)) {
372
+ if (downHandler && !hasAttrCompat(downHandler, attrName)) {
356
373
  downHandler.setAttribute(attrName, `${testId}-down`);
357
374
  }
358
375
  }
359
376
  const sliders = root.matches(`${sel(pc, "slider")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "slider")}[${attrName}]`));
360
377
  for (const slider of sliders) {
361
378
  if (!(slider instanceof HTMLElement)) continue;
362
- const testId = slider.getAttribute(attrName) || "";
379
+ const testId = getAttrCompat(slider, attrName) || "";
363
380
  if (!testId) continue;
364
381
  slider.querySelectorAll(sel(pc, "slider-handle")).forEach((h, i) => {
365
382
  const el = h;
366
- if (!el.hasAttribute(attrName)) {
383
+ if (!hasAttrCompat(el, attrName)) {
367
384
  el.setAttribute(attrName, `${testId}-handle-${i}`);
368
385
  }
369
386
  });
@@ -371,11 +388,11 @@ function injectSubElements(root, attrName, pc) {
371
388
  const rates = root.matches(`${sel(pc, "rate")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "rate")}[${attrName}]`));
372
389
  for (const rate of rates) {
373
390
  if (!(rate instanceof HTMLElement)) continue;
374
- const testId = rate.getAttribute(attrName) || "";
391
+ const testId = getAttrCompat(rate, attrName) || "";
375
392
  if (!testId) continue;
376
393
  rate.querySelectorAll(sel(pc, "rate-star")).forEach((s, i) => {
377
394
  const el = s;
378
- if (!el.hasAttribute(attrName)) {
395
+ if (!hasAttrCompat(el, attrName)) {
379
396
  el.setAttribute(attrName, `${testId}-star-${i}`);
380
397
  }
381
398
  });
@@ -384,17 +401,52 @@ function injectSubElements(root, attrName, pc) {
384
401
  const uploadById = uploadByTestId.length === 0 ? root.matches(`${sel(pc, "upload")}[id]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "upload")}[id]`)) : [];
385
402
  for (const upload of [...uploadByTestId, ...uploadById]) {
386
403
  if (!(upload instanceof HTMLElement)) continue;
387
- const testId = upload.getAttribute(attrName) || upload.getAttribute("id") || "";
404
+ const testId = getAttrCompat(upload, attrName) || upload.getAttribute("id") || "";
388
405
  if (!testId) continue;
389
406
  const input = upload.querySelector('input[type="file"]');
390
- if (input && !input.hasAttribute(attrName)) {
407
+ if (input && !hasAttrCompat(input, attrName)) {
391
408
  input.setAttribute(attrName, `${testId}-input`);
392
- if (!upload.hasAttribute(attrName)) {
409
+ if (!hasAttrCompat(upload, attrName)) {
393
410
  upload.setAttribute(attrName, testId);
394
411
  }
395
412
  }
396
413
  }
397
414
  }
415
+ function injectTabsSubElements(root, attrName, pc) {
416
+ const tabsContainers = root.matches(`${sel(pc, "tabs")}[${attrName}]`) ? [root] : Array.from(root.querySelectorAll(`${sel(pc, "tabs")}[${attrName}]`));
417
+ for (const tabs of tabsContainers) {
418
+ if (!(tabs instanceof HTMLElement)) continue;
419
+ const testId = getAttrCompat(tabs, attrName) || "";
420
+ if (!testId) continue;
421
+ const tabItems = tabs.querySelectorAll(sel(pc, "tabs-tab"));
422
+ tabItems.forEach((tab, i) => {
423
+ const el = tab;
424
+ if (hasAttrCompat(el, attrName)) return;
425
+ const btn = el.querySelector(sel(pc, "tabs-tab-btn"));
426
+ const text = (btn?.textContent || el.textContent || "").trim().replace(/\s+/g, "-");
427
+ el.setAttribute(attrName, `${testId}-tab-${i}-${text}`);
428
+ });
429
+ const panes = tabs.querySelectorAll(sel(pc, "tabs-tabpane"));
430
+ panes.forEach((pane, i) => {
431
+ const el = pane;
432
+ if (hasAttrCompat(el, attrName)) return;
433
+ const tabBtn = tabItems[i]?.querySelector(sel(pc, "tabs-tab-btn"));
434
+ const text = (tabBtn?.textContent || "").trim().replace(/\s+/g, "-");
435
+ el.setAttribute(attrName, `${testId}-pane-${i}-${text}`);
436
+ });
437
+ const navOps = tabs.querySelector(sel(pc, "tabs-nav-operations"));
438
+ if (navOps instanceof HTMLElement) {
439
+ const addBtn = navOps.querySelector(sel(pc, "tabs-nav-add"));
440
+ if (addBtn instanceof HTMLElement && !hasAttrCompat(addBtn, attrName)) {
441
+ addBtn.setAttribute(attrName, `${testId}-add`);
442
+ }
443
+ const moreBtn = navOps.querySelector(sel(pc, "tabs-nav-more"));
444
+ if (moreBtn instanceof HTMLElement && !hasAttrCompat(moreBtn, attrName)) {
445
+ moreBtn.setAttribute(attrName, `${testId}-more`);
446
+ }
447
+ }
448
+ }
449
+ }
398
450
  function setupAntTestIds(opts = {}) {
399
451
  const attrName = opts.attributeName ?? "data-testid";
400
452
  const pc = opts.prefixCls ?? "ant";
@@ -410,24 +462,18 @@ function setupAntTestIds(opts = {}) {
410
462
  const containers = node.matches(selector) ? [node] : Array.from(node.querySelectorAll(selector));
411
463
  for (const container of containers) {
412
464
  if (!(container instanceof HTMLElement)) continue;
413
- if (container.hasAttribute(attrName)) continue;
465
+ if (hasAttrCompat(container, attrName)) continue;
414
466
  if (strategy) {
415
467
  queueMicrotask(() => {
416
- const runPanelInjection = (retriesLeft) => {
417
- const trigger = findActiveTrigger(attrName, pc);
418
- const triggerTestId = getTriggerTestId(trigger, attrName);
419
- if (triggerTestId === "unknown" && retriesLeft > 0) {
420
- requestAnimationFrame(() => runPanelInjection(retriesLeft - 1));
421
- return;
422
- }
423
- strategy({ container, trigger, triggerTestId, attrName });
424
- };
425
- runPanelInjection(3);
468
+ const trigger = findActiveTrigger(attrName, pc);
469
+ const triggerTestId = getTriggerTestId(trigger, attrName);
470
+ strategy({ container, trigger, triggerTestId, attrName });
426
471
  });
427
472
  }
428
473
  }
429
474
  }
430
475
  injectSubElements(node, attrName, pc);
476
+ injectTabsSubElements(node, attrName, pc);
431
477
  }
432
478
  let pending = false;
433
479
  let pendingNodes = [];
@@ -470,7 +516,7 @@ function injectCurrentDropdowns(opts = {}) {
470
516
  const containers = document.querySelectorAll(selector);
471
517
  containers.forEach((container) => {
472
518
  if (!(container instanceof HTMLElement)) return;
473
- if (container.hasAttribute(attrName)) return;
519
+ if (hasAttrCompat(container, attrName)) return;
474
520
  if (!strategy) return;
475
521
  const trigger = findActiveTrigger(attrName, pc);
476
522
  const triggerTestId = getTriggerTestId(trigger, attrName);
@@ -478,6 +524,7 @@ function injectCurrentDropdowns(opts = {}) {
478
524
  });
479
525
  }
480
526
  injectSubElements(document.body, attrName, pc);
527
+ injectTabsSubElements(document.body, attrName, pc);
481
528
  injectMenuItems(attrName, document, pc);
482
529
  }
483
530
  function mergeMatchers(defaults, customs) {
@@ -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.12",
3
+ "version": "1.1.1",
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"