@testid/antd-testid-runtime 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -207,6 +207,10 @@ declare class TestIdObserver {
207
207
  * 更新当前路由 (路由切换时调用)
208
208
  */
209
209
  setRoute(routeName: string): void;
210
+ /**
211
+ * 重置浮层子节点计数器 (路由切换时调用)
212
+ */
213
+ resetPopupChildCounter(): void;
210
214
  /**
211
215
  * 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
212
216
  */
@@ -225,8 +229,9 @@ declare class TestIdObserver {
225
229
  * 决策优先级:
226
230
  * 0. 已有 data-testid → 跳过
227
231
  * 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
228
- * 2. body 直系浮层 → 匹配浮层类型 → 独立前缀
229
- * 3. #app 内普通节点dynamic
232
+ * 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
233
+ * 3. 浮层内部子节点 查找浮层祖先 浮层独立前缀 + 计数器
234
+ * 4. #app 内普通节点 → dynamic
230
235
  */
231
236
  private processSingleNode;
232
237
  /**
@@ -254,18 +259,50 @@ declare class TestIdObserver {
254
259
  */
255
260
  private handlePopupNode;
256
261
  /**
257
- * 通过 CSS class 匹配浮层类型
262
+ * 检测节点是否为浮层根节点
263
+ *
264
+ * Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
265
+ * body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
266
+ *
267
+ * 因此不要求 parentElement === document.body,
268
+ * 只要节点 class 匹配浮层类型且不在 #app 内即可。
258
269
  *
259
270
  * @param node - 待检测节点
260
- * @returns 浮层类型或 null (非 Antd 浮层)
271
+ * @returns 浮层类型或 null
261
272
  */
262
273
  private detectPopupType;
274
+ /**
275
+ * 向上遍历 DOM,检测节点是否在某个浮层内部
276
+ *
277
+ * 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
278
+ * 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
279
+ * 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
280
+ *
281
+ * @returns 浮层类型或 null (不在任何浮层内)
282
+ */
283
+ private detectPopupAncestor;
284
+ /**
285
+ * 验证节点的祖先链是否能到达 document.body
286
+ * (经过若干层 wrapper 后依然是 body 下的浮层)
287
+ */
288
+ private reachesBody;
289
+ /**
290
+ * 检测节点 class 是否匹配某个浮层类型
291
+ */
292
+ private matchPopupClass;
263
293
  /**
264
294
  * 处理页面内动态新增节点 (v-for / 异步 v-if 等)
265
295
  *
266
296
  * ID 格式: ${runtimePagePrefix}${route}_${tag}_${counter}
267
297
  */
268
298
  private handleDynamicNode;
299
+ /**
300
+ * 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
301
+ *
302
+ * ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
303
+ * 例: hall_modal_button_0, hall_select_div_2
304
+ */
305
+ private handlePopupChildNode;
269
306
  /**
270
307
  * 检测元素是否在 #app 容器内
271
308
  */
package/dist/index.d.ts CHANGED
@@ -207,6 +207,10 @@ declare class TestIdObserver {
207
207
  * 更新当前路由 (路由切换时调用)
208
208
  */
209
209
  setRoute(routeName: string): void;
210
+ /**
211
+ * 重置浮层子节点计数器 (路由切换时调用)
212
+ */
213
+ resetPopupChildCounter(): void;
210
214
  /**
211
215
  * 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
212
216
  */
@@ -225,8 +229,9 @@ declare class TestIdObserver {
225
229
  * 决策优先级:
226
230
  * 0. 已有 data-testid → 跳过
227
231
  * 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
228
- * 2. body 直系浮层 → 匹配浮层类型 → 独立前缀
229
- * 3. #app 内普通节点dynamic
232
+ * 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
233
+ * 3. 浮层内部子节点 查找浮层祖先 浮层独立前缀 + 计数器
234
+ * 4. #app 内普通节点 → dynamic
230
235
  */
231
236
  private processSingleNode;
232
237
  /**
@@ -254,18 +259,50 @@ declare class TestIdObserver {
254
259
  */
255
260
  private handlePopupNode;
256
261
  /**
257
- * 通过 CSS class 匹配浮层类型
262
+ * 检测节点是否为浮层根节点
263
+ *
264
+ * Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
265
+ * body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
266
+ *
267
+ * 因此不要求 parentElement === document.body,
268
+ * 只要节点 class 匹配浮层类型且不在 #app 内即可。
258
269
  *
259
270
  * @param node - 待检测节点
260
- * @returns 浮层类型或 null (非 Antd 浮层)
271
+ * @returns 浮层类型或 null
261
272
  */
262
273
  private detectPopupType;
274
+ /**
275
+ * 向上遍历 DOM,检测节点是否在某个浮层内部
276
+ *
277
+ * 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
278
+ * 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
279
+ * 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
280
+ *
281
+ * @returns 浮层类型或 null (不在任何浮层内)
282
+ */
283
+ private detectPopupAncestor;
284
+ /**
285
+ * 验证节点的祖先链是否能到达 document.body
286
+ * (经过若干层 wrapper 后依然是 body 下的浮层)
287
+ */
288
+ private reachesBody;
289
+ /**
290
+ * 检测节点 class 是否匹配某个浮层类型
291
+ */
292
+ private matchPopupClass;
263
293
  /**
264
294
  * 处理页面内动态新增节点 (v-for / 异步 v-if 等)
265
295
  *
266
296
  * ID 格式: ${runtimePagePrefix}${route}_${tag}_${counter}
267
297
  */
268
298
  private handleDynamicNode;
299
+ /**
300
+ * 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
301
+ *
302
+ * ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
303
+ * 例: hall_modal_button_0, hall_select_div_2
304
+ */
305
+ private handlePopupChildNode;
269
306
  /**
270
307
  * 检测元素是否在 #app 容器内
271
308
  */
package/dist/index.js CHANGED
@@ -212,7 +212,8 @@ var TestIdObserver = class {
212
212
  observer: null,
213
213
  isRunning: false,
214
214
  currentRoute: "",
215
- dynamicCounter: /* @__PURE__ */ new Map()
215
+ dynamicCounter: /* @__PURE__ */ new Map(),
216
+ popupChildCounter: /* @__PURE__ */ new Map()
216
217
  };
217
218
  }
218
219
  // ==========================================================
@@ -249,6 +250,12 @@ var TestIdObserver = class {
249
250
  setRoute(routeName) {
250
251
  this.state.currentRoute = routeName;
251
252
  }
253
+ /**
254
+ * 重置浮层子节点计数器 (路由切换时调用)
255
+ */
256
+ resetPopupChildCounter() {
257
+ this.state.popupChildCounter.clear();
258
+ }
252
259
  /**
253
260
  * 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
254
261
  */
@@ -286,8 +293,9 @@ var TestIdObserver = class {
286
293
  * 决策优先级:
287
294
  * 0. 已有 data-testid → 跳过
288
295
  * 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
289
- * 2. body 直系浮层 → 匹配浮层类型 → 独立前缀
290
- * 3. #app 内普通节点dynamic
296
+ * 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
297
+ * 3. 浮层内部子节点 查找浮层祖先 浮层独立前缀 + 计数器
298
+ * 4. #app 内普通节点 → dynamic
291
299
  */
292
300
  processSingleNode(node) {
293
301
  const config = getConfig();
@@ -303,6 +311,11 @@ var TestIdObserver = class {
303
311
  this.handlePopupNode(node, popupType);
304
312
  return;
305
313
  }
314
+ const popupAncestorType = this.detectPopupAncestor(node);
315
+ if (popupAncestorType) {
316
+ this.handlePopupChildNode(node, popupAncestorType, config);
317
+ return;
318
+ }
306
319
  if (this.isInsideApp(node)) {
307
320
  this.handleDynamicNode(node, config);
308
321
  }
@@ -373,13 +386,59 @@ var TestIdObserver = class {
373
386
  node.setAttribute("data-testid", testId);
374
387
  }
375
388
  /**
376
- * 通过 CSS class 匹配浮层类型
389
+ * 检测节点是否为浮层根节点
390
+ *
391
+ * Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
392
+ * body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
393
+ *
394
+ * 因此不要求 parentElement === document.body,
395
+ * 只要节点 class 匹配浮层类型且不在 #app 内即可。
377
396
  *
378
397
  * @param node - 待检测节点
379
- * @returns 浮层类型或 null (非 Antd 浮层)
398
+ * @returns 浮层类型或 null
380
399
  */
381
400
  detectPopupType(node) {
382
- if (node.parentElement !== document.body) return null;
401
+ if (this.isInsideApp(node)) return null;
402
+ return this.matchPopupClass(node);
403
+ }
404
+ /**
405
+ * 向上遍历 DOM,检测节点是否在某个浮层内部
406
+ *
407
+ * 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
408
+ * 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
409
+ * 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
410
+ *
411
+ * @returns 浮层类型或 null (不在任何浮层内)
412
+ */
413
+ detectPopupAncestor(node) {
414
+ let current = node.parentElement;
415
+ while (current) {
416
+ const type = this.matchPopupClass(current);
417
+ if (type) {
418
+ const reachesBody = this.reachesBody(current);
419
+ if (reachesBody) return type;
420
+ }
421
+ if (current === document.body || current.id === "app") break;
422
+ current = current.parentElement;
423
+ }
424
+ return null;
425
+ }
426
+ /**
427
+ * 验证节点的祖先链是否能到达 document.body
428
+ * (经过若干层 wrapper 后依然是 body 下的浮层)
429
+ */
430
+ reachesBody(node) {
431
+ let current = node;
432
+ while (current && current !== document.body) {
433
+ if (current.id === "app") return false;
434
+ current = current.parentElement;
435
+ }
436
+ return current === document.body;
437
+ }
438
+ /**
439
+ * 检测节点 class 是否匹配某个浮层类型
440
+ */
441
+ matchPopupClass(node) {
383
442
  const classStr = node.className || "";
384
443
  if (typeof classStr !== "string") return null;
385
444
  const entries = Object.entries(POPUP_CLASS_MAP);
@@ -412,6 +471,22 @@ var TestIdObserver = class {
412
471
  const testId = `${config.runtimePagePrefix}${route}_${tag}_${current}`;
413
472
  node.setAttribute("data-testid", testId);
414
473
  }
474
+ /**
475
+ * 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
476
+ *
477
+ * ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
478
+ * 例: hall_modal_button_0, hall_select_div_2
479
+ */
480
+ handlePopupChildNode(node, popupType, config) {
481
+ if (config.onlyInteractive && !this.isInteractive(node)) return;
482
+ const tag = this.getSimpleTag(node);
483
+ const key = `${popupType}_${tag}`;
484
+ const current = this.state.popupChildCounter.get(key) ?? 0;
485
+ this.state.popupChildCounter.set(key, current + 1);
486
+ const prefix = config.popupPrefixMap[popupType] || `${popupType}_`;
487
+ const testId = `${prefix}${tag}_${current}`;
488
+ node.setAttribute("data-testid", testId);
489
+ }
415
490
  // ==========================================================
416
491
  // 辅助方法
417
492
  // ==========================================================
package/dist/index.mjs CHANGED
@@ -170,7 +170,8 @@ var TestIdObserver = class {
170
170
  observer: null,
171
171
  isRunning: false,
172
172
  currentRoute: "",
173
- dynamicCounter: /* @__PURE__ */ new Map()
173
+ dynamicCounter: /* @__PURE__ */ new Map(),
174
+ popupChildCounter: /* @__PURE__ */ new Map()
174
175
  };
175
176
  }
176
177
  // ==========================================================
@@ -207,6 +208,12 @@ var TestIdObserver = class {
207
208
  setRoute(routeName) {
208
209
  this.state.currentRoute = routeName;
209
210
  }
211
+ /**
212
+ * 重置浮层子节点计数器 (路由切换时调用)
213
+ */
214
+ resetPopupChildCounter() {
215
+ this.state.popupChildCounter.clear();
216
+ }
210
217
  /**
211
218
  * 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
212
219
  */
@@ -244,8 +251,9 @@ var TestIdObserver = class {
244
251
  * 决策优先级:
245
252
  * 0. 已有 data-testid → 跳过
246
253
  * 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
247
- * 2. body 直系浮层 → 匹配浮层类型 → 独立前缀
248
- * 3. #app 内普通节点dynamic
254
+ * 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
255
+ * 3. 浮层内部子节点 查找浮层祖先 浮层独立前缀 + 计数器
256
+ * 4. #app 内普通节点 → dynamic
249
257
  */
250
258
  processSingleNode(node) {
251
259
  const config = getConfig();
@@ -261,6 +269,11 @@ var TestIdObserver = class {
261
269
  this.handlePopupNode(node, popupType);
262
270
  return;
263
271
  }
272
+ const popupAncestorType = this.detectPopupAncestor(node);
273
+ if (popupAncestorType) {
274
+ this.handlePopupChildNode(node, popupAncestorType, config);
275
+ return;
276
+ }
264
277
  if (this.isInsideApp(node)) {
265
278
  this.handleDynamicNode(node, config);
266
279
  }
@@ -331,13 +344,59 @@ var TestIdObserver = class {
331
344
  node.setAttribute("data-testid", testId);
332
345
  }
333
346
  /**
334
- * 通过 CSS class 匹配浮层类型
347
+ * 检测节点是否为浮层根节点
348
+ *
349
+ * Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
350
+ * body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
351
+ *
352
+ * 因此不要求 parentElement === document.body,
353
+ * 只要节点 class 匹配浮层类型且不在 #app 内即可。
335
354
  *
336
355
  * @param node - 待检测节点
337
- * @returns 浮层类型或 null (非 Antd 浮层)
356
+ * @returns 浮层类型或 null
338
357
  */
339
358
  detectPopupType(node) {
340
- if (node.parentElement !== document.body) return null;
359
+ if (this.isInsideApp(node)) return null;
360
+ return this.matchPopupClass(node);
361
+ }
362
+ /**
363
+ * 向上遍历 DOM,检测节点是否在某个浮层内部
364
+ *
365
+ * 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
366
+ * 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
367
+ * 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
368
+ *
369
+ * @returns 浮层类型或 null (不在任何浮层内)
370
+ */
371
+ detectPopupAncestor(node) {
372
+ let current = node.parentElement;
373
+ while (current) {
374
+ const type = this.matchPopupClass(current);
375
+ if (type) {
376
+ const reachesBody = this.reachesBody(current);
377
+ if (reachesBody) return type;
378
+ }
379
+ if (current === document.body || current.id === "app") break;
380
+ current = current.parentElement;
381
+ }
382
+ return null;
383
+ }
384
+ /**
385
+ * 验证节点的祖先链是否能到达 document.body
386
+ * (经过若干层 wrapper 后依然是 body 下的浮层)
387
+ */
388
+ reachesBody(node) {
389
+ let current = node;
390
+ while (current && current !== document.body) {
391
+ if (current.id === "app") return false;
392
+ current = current.parentElement;
393
+ }
394
+ return current === document.body;
395
+ }
396
+ /**
397
+ * 检测节点 class 是否匹配某个浮层类型
398
+ */
399
+ matchPopupClass(node) {
341
400
  const classStr = node.className || "";
342
401
  if (typeof classStr !== "string") return null;
343
402
  const entries = Object.entries(POPUP_CLASS_MAP);
@@ -370,6 +429,22 @@ var TestIdObserver = class {
370
429
  const testId = `${config.runtimePagePrefix}${route}_${tag}_${current}`;
371
430
  node.setAttribute("data-testid", testId);
372
431
  }
432
+ /**
433
+ * 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
434
+ *
435
+ * ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
436
+ * 例: hall_modal_button_0, hall_select_div_2
437
+ */
438
+ handlePopupChildNode(node, popupType, config) {
439
+ if (config.onlyInteractive && !this.isInteractive(node)) return;
440
+ const tag = this.getSimpleTag(node);
441
+ const key = `${popupType}_${tag}`;
442
+ const current = this.state.popupChildCounter.get(key) ?? 0;
443
+ this.state.popupChildCounter.set(key, current + 1);
444
+ const prefix = config.popupPrefixMap[popupType] || `${popupType}_`;
445
+ const testId = `${prefix}${tag}_${current}`;
446
+ node.setAttribute("data-testid", testId);
447
+ }
373
448
  // ==========================================================
374
449
  // 辅助方法
375
450
  // ==========================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testid/antd-testid-runtime",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "运行时兜底打标模块 — MutationObserver + 锚点计数器 + 浮层计数器 + ID 重复检测",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",