@testid/antd-testid-runtime 1.0.1 → 1.0.3
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 +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +101 -15
- package/dist/index.mjs +101 -15
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -30,6 +30,13 @@ interface TestIdMarkConfig {
|
|
|
30
30
|
runtimePagePrefix: string;
|
|
31
31
|
/** Antd 浮层组件专属前缀映射 */
|
|
32
32
|
popupPrefixMap: Record<PopupType, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Ant Design Vue CSS 类名前缀 (对应 ConfigProvider 的 prefixCls)
|
|
35
|
+
*
|
|
36
|
+
* 默认: 'ant' → 匹配 .ant-modal, .ant-picker-dropdown 等
|
|
37
|
+
* 若项目使用 <a-config-provider prefixCls="my-ui"> 则设为 'my-ui'
|
|
38
|
+
*/
|
|
39
|
+
antdClassPrefix: string;
|
|
33
40
|
/** 忽略不打标的 HTML 标签名 */
|
|
34
41
|
ignoreTags: string[];
|
|
35
42
|
/** 包含此 class 的 DOM 跳过打标 */
|
|
@@ -194,6 +201,8 @@ declare function buildPopupTestId(type: PopupType, tag: string, counterId: numbe
|
|
|
194
201
|
*/
|
|
195
202
|
declare class TestIdObserver {
|
|
196
203
|
private state;
|
|
204
|
+
/** 浮层 class 匹配映射 (根据 antdClassPrefix 动态构建) */
|
|
205
|
+
private popupClassMap;
|
|
197
206
|
constructor();
|
|
198
207
|
/**
|
|
199
208
|
* 启动 MutationObserver
|
|
@@ -207,6 +216,10 @@ declare class TestIdObserver {
|
|
|
207
216
|
* 更新当前路由 (路由切换时调用)
|
|
208
217
|
*/
|
|
209
218
|
setRoute(routeName: string): void;
|
|
219
|
+
/**
|
|
220
|
+
* 重置浮层子节点计数器 (路由切换时调用)
|
|
221
|
+
*/
|
|
222
|
+
resetPopupChildCounter(): void;
|
|
210
223
|
/**
|
|
211
224
|
* 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
|
|
212
225
|
*/
|
|
@@ -225,8 +238,9 @@ declare class TestIdObserver {
|
|
|
225
238
|
* 决策优先级:
|
|
226
239
|
* 0. 已有 data-testid → 跳过
|
|
227
240
|
* 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
|
|
228
|
-
* 2. body
|
|
229
|
-
* 3.
|
|
241
|
+
* 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
|
|
242
|
+
* 3. 浮层内部子节点 → 查找浮层祖先 → 浮层独立前缀 + 计数器
|
|
243
|
+
* 4. #app 内普通节点 → dynamic
|
|
230
244
|
*/
|
|
231
245
|
private processSingleNode;
|
|
232
246
|
/**
|
|
@@ -254,18 +268,50 @@ declare class TestIdObserver {
|
|
|
254
268
|
*/
|
|
255
269
|
private handlePopupNode;
|
|
256
270
|
/**
|
|
257
|
-
*
|
|
271
|
+
* 检测节点是否为浮层根节点
|
|
272
|
+
*
|
|
273
|
+
* Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
|
|
274
|
+
* body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
|
|
275
|
+
*
|
|
276
|
+
* 因此不要求 parentElement === document.body,
|
|
277
|
+
* 只要节点 class 匹配浮层类型且不在 #app 内即可。
|
|
258
278
|
*
|
|
259
279
|
* @param node - 待检测节点
|
|
260
|
-
* @returns 浮层类型或 null
|
|
280
|
+
* @returns 浮层类型或 null
|
|
261
281
|
*/
|
|
262
282
|
private detectPopupType;
|
|
283
|
+
/**
|
|
284
|
+
* 向上遍历 DOM,检测节点是否在某个浮层内部
|
|
285
|
+
*
|
|
286
|
+
* 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
|
|
287
|
+
* 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
|
|
288
|
+
* 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
|
|
289
|
+
*
|
|
290
|
+
* @returns 浮层类型或 null (不在任何浮层内)
|
|
291
|
+
*/
|
|
292
|
+
private detectPopupAncestor;
|
|
293
|
+
/**
|
|
294
|
+
* 验证节点的祖先链是否能到达 document.body
|
|
295
|
+
* (经过若干层 wrapper 后依然是 body 下的浮层)
|
|
296
|
+
*/
|
|
297
|
+
private reachesBody;
|
|
298
|
+
/**
|
|
299
|
+
* 检测节点 class 是否匹配某个浮层类型
|
|
300
|
+
*/
|
|
301
|
+
private matchPopupClass;
|
|
263
302
|
/**
|
|
264
303
|
* 处理页面内动态新增节点 (v-for / 异步 v-if 等)
|
|
265
304
|
*
|
|
266
305
|
* ID 格式: ${runtimePagePrefix}${route}_${tag}_${counter}
|
|
267
306
|
*/
|
|
268
307
|
private handleDynamicNode;
|
|
308
|
+
/**
|
|
309
|
+
* 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
|
|
310
|
+
*
|
|
311
|
+
* ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
|
|
312
|
+
* 例: hall_modal_button_0, hall_select_div_2
|
|
313
|
+
*/
|
|
314
|
+
private handlePopupChildNode;
|
|
269
315
|
/**
|
|
270
316
|
* 检测元素是否在 #app 容器内
|
|
271
317
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ interface TestIdMarkConfig {
|
|
|
30
30
|
runtimePagePrefix: string;
|
|
31
31
|
/** Antd 浮层组件专属前缀映射 */
|
|
32
32
|
popupPrefixMap: Record<PopupType, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Ant Design Vue CSS 类名前缀 (对应 ConfigProvider 的 prefixCls)
|
|
35
|
+
*
|
|
36
|
+
* 默认: 'ant' → 匹配 .ant-modal, .ant-picker-dropdown 等
|
|
37
|
+
* 若项目使用 <a-config-provider prefixCls="my-ui"> 则设为 'my-ui'
|
|
38
|
+
*/
|
|
39
|
+
antdClassPrefix: string;
|
|
33
40
|
/** 忽略不打标的 HTML 标签名 */
|
|
34
41
|
ignoreTags: string[];
|
|
35
42
|
/** 包含此 class 的 DOM 跳过打标 */
|
|
@@ -194,6 +201,8 @@ declare function buildPopupTestId(type: PopupType, tag: string, counterId: numbe
|
|
|
194
201
|
*/
|
|
195
202
|
declare class TestIdObserver {
|
|
196
203
|
private state;
|
|
204
|
+
/** 浮层 class 匹配映射 (根据 antdClassPrefix 动态构建) */
|
|
205
|
+
private popupClassMap;
|
|
197
206
|
constructor();
|
|
198
207
|
/**
|
|
199
208
|
* 启动 MutationObserver
|
|
@@ -207,6 +216,10 @@ declare class TestIdObserver {
|
|
|
207
216
|
* 更新当前路由 (路由切换时调用)
|
|
208
217
|
*/
|
|
209
218
|
setRoute(routeName: string): void;
|
|
219
|
+
/**
|
|
220
|
+
* 重置浮层子节点计数器 (路由切换时调用)
|
|
221
|
+
*/
|
|
222
|
+
resetPopupChildCounter(): void;
|
|
210
223
|
/**
|
|
211
224
|
* 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
|
|
212
225
|
*/
|
|
@@ -225,8 +238,9 @@ declare class TestIdObserver {
|
|
|
225
238
|
* 决策优先级:
|
|
226
239
|
* 0. 已有 data-testid → 跳过
|
|
227
240
|
* 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
|
|
228
|
-
* 2. body
|
|
229
|
-
* 3.
|
|
241
|
+
* 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
|
|
242
|
+
* 3. 浮层内部子节点 → 查找浮层祖先 → 浮层独立前缀 + 计数器
|
|
243
|
+
* 4. #app 内普通节点 → dynamic
|
|
230
244
|
*/
|
|
231
245
|
private processSingleNode;
|
|
232
246
|
/**
|
|
@@ -254,18 +268,50 @@ declare class TestIdObserver {
|
|
|
254
268
|
*/
|
|
255
269
|
private handlePopupNode;
|
|
256
270
|
/**
|
|
257
|
-
*
|
|
271
|
+
* 检测节点是否为浮层根节点
|
|
272
|
+
*
|
|
273
|
+
* Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
|
|
274
|
+
* body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
|
|
275
|
+
*
|
|
276
|
+
* 因此不要求 parentElement === document.body,
|
|
277
|
+
* 只要节点 class 匹配浮层类型且不在 #app 内即可。
|
|
258
278
|
*
|
|
259
279
|
* @param node - 待检测节点
|
|
260
|
-
* @returns 浮层类型或 null
|
|
280
|
+
* @returns 浮层类型或 null
|
|
261
281
|
*/
|
|
262
282
|
private detectPopupType;
|
|
283
|
+
/**
|
|
284
|
+
* 向上遍历 DOM,检测节点是否在某个浮层内部
|
|
285
|
+
*
|
|
286
|
+
* 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
|
|
287
|
+
* 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
|
|
288
|
+
* 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
|
|
289
|
+
*
|
|
290
|
+
* @returns 浮层类型或 null (不在任何浮层内)
|
|
291
|
+
*/
|
|
292
|
+
private detectPopupAncestor;
|
|
293
|
+
/**
|
|
294
|
+
* 验证节点的祖先链是否能到达 document.body
|
|
295
|
+
* (经过若干层 wrapper 后依然是 body 下的浮层)
|
|
296
|
+
*/
|
|
297
|
+
private reachesBody;
|
|
298
|
+
/**
|
|
299
|
+
* 检测节点 class 是否匹配某个浮层类型
|
|
300
|
+
*/
|
|
301
|
+
private matchPopupClass;
|
|
263
302
|
/**
|
|
264
303
|
* 处理页面内动态新增节点 (v-for / 异步 v-if 等)
|
|
265
304
|
*
|
|
266
305
|
* ID 格式: ${runtimePagePrefix}${route}_${tag}_${counter}
|
|
267
306
|
*/
|
|
268
307
|
private handleDynamicNode;
|
|
308
|
+
/**
|
|
309
|
+
* 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
|
|
310
|
+
*
|
|
311
|
+
* ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
|
|
312
|
+
* 例: hall_modal_button_0, hall_select_div_2
|
|
313
|
+
*/
|
|
314
|
+
private handlePopupChildNode;
|
|
269
315
|
/**
|
|
270
316
|
* 检测元素是否在 #app 容器内
|
|
271
317
|
*/
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ var defaultConfig = {
|
|
|
60
60
|
globalPrefix: "",
|
|
61
61
|
compilePrefix: "static_",
|
|
62
62
|
runtimePagePrefix: "dynamic_",
|
|
63
|
+
antdClassPrefix: "ant",
|
|
63
64
|
popupPrefixMap: {
|
|
64
65
|
modal: "modal_",
|
|
65
66
|
drawer: "drawer_",
|
|
@@ -181,15 +182,23 @@ function buildPopupTestId(type, tag, counterId) {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
// src/utils/testIdObserver.ts
|
|
184
|
-
var
|
|
185
|
-
modal: ".
|
|
186
|
-
drawer: ".
|
|
187
|
-
select: ".
|
|
188
|
-
datePicker: ".
|
|
189
|
-
popconfirm: ".
|
|
190
|
-
dropdown: ".
|
|
191
|
-
tooltip: ".
|
|
185
|
+
var POPUP_CLASS_SUFFIX_MAP = {
|
|
186
|
+
modal: ".{prefix}-modal",
|
|
187
|
+
drawer: ".{prefix}-drawer",
|
|
188
|
+
select: ".{prefix}-select-dropdown",
|
|
189
|
+
datePicker: ".{prefix}-picker-dropdown",
|
|
190
|
+
popconfirm: ".{prefix}-popover.{prefix}-popconfirm",
|
|
191
|
+
dropdown: ".{prefix}-dropdown",
|
|
192
|
+
tooltip: ".{prefix}-tooltip"
|
|
192
193
|
};
|
|
194
|
+
function buildPopupClassMap(prefix) {
|
|
195
|
+
const result = {};
|
|
196
|
+
const entries = Object.entries(POPUP_CLASS_SUFFIX_MAP);
|
|
197
|
+
for (const [type, template] of entries) {
|
|
198
|
+
result[type] = template.replace(/\{prefix\}/g, prefix);
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
193
202
|
var TestIdObserver = class {
|
|
194
203
|
constructor() {
|
|
195
204
|
// ==========================================================
|
|
@@ -208,11 +217,14 @@ var TestIdObserver = class {
|
|
|
208
217
|
});
|
|
209
218
|
}
|
|
210
219
|
};
|
|
220
|
+
const config = getConfig();
|
|
221
|
+
this.popupClassMap = buildPopupClassMap(config.antdClassPrefix);
|
|
211
222
|
this.state = {
|
|
212
223
|
observer: null,
|
|
213
224
|
isRunning: false,
|
|
214
225
|
currentRoute: "",
|
|
215
|
-
dynamicCounter: /* @__PURE__ */ new Map()
|
|
226
|
+
dynamicCounter: /* @__PURE__ */ new Map(),
|
|
227
|
+
popupChildCounter: /* @__PURE__ */ new Map()
|
|
216
228
|
};
|
|
217
229
|
}
|
|
218
230
|
// ==========================================================
|
|
@@ -249,6 +261,12 @@ var TestIdObserver = class {
|
|
|
249
261
|
setRoute(routeName) {
|
|
250
262
|
this.state.currentRoute = routeName;
|
|
251
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* 重置浮层子节点计数器 (路由切换时调用)
|
|
266
|
+
*/
|
|
267
|
+
resetPopupChildCounter() {
|
|
268
|
+
this.state.popupChildCounter.clear();
|
|
269
|
+
}
|
|
252
270
|
/**
|
|
253
271
|
* 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
|
|
254
272
|
*/
|
|
@@ -286,8 +304,9 @@ var TestIdObserver = class {
|
|
|
286
304
|
* 决策优先级:
|
|
287
305
|
* 0. 已有 data-testid → 跳过
|
|
288
306
|
* 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
|
|
289
|
-
* 2. body
|
|
290
|
-
* 3.
|
|
307
|
+
* 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
|
|
308
|
+
* 3. 浮层内部子节点 → 查找浮层祖先 → 浮层独立前缀 + 计数器
|
|
309
|
+
* 4. #app 内普通节点 → dynamic
|
|
291
310
|
*/
|
|
292
311
|
processSingleNode(node) {
|
|
293
312
|
const config = getConfig();
|
|
@@ -303,6 +322,11 @@ var TestIdObserver = class {
|
|
|
303
322
|
this.handlePopupNode(node, popupType);
|
|
304
323
|
return;
|
|
305
324
|
}
|
|
325
|
+
const popupAncestorType = this.detectPopupAncestor(node);
|
|
326
|
+
if (popupAncestorType) {
|
|
327
|
+
this.handlePopupChildNode(node, popupAncestorType, config);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
306
330
|
if (this.isInsideApp(node)) {
|
|
307
331
|
this.handleDynamicNode(node, config);
|
|
308
332
|
}
|
|
@@ -373,16 +397,62 @@ var TestIdObserver = class {
|
|
|
373
397
|
node.setAttribute("data-testid", testId);
|
|
374
398
|
}
|
|
375
399
|
/**
|
|
376
|
-
*
|
|
400
|
+
* 检测节点是否为浮层根节点
|
|
401
|
+
*
|
|
402
|
+
* Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
|
|
403
|
+
* body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
|
|
404
|
+
*
|
|
405
|
+
* 因此不要求 parentElement === document.body,
|
|
406
|
+
* 只要节点 class 匹配浮层类型且不在 #app 内即可。
|
|
377
407
|
*
|
|
378
408
|
* @param node - 待检测节点
|
|
379
|
-
* @returns 浮层类型或 null
|
|
409
|
+
* @returns 浮层类型或 null
|
|
380
410
|
*/
|
|
381
411
|
detectPopupType(node) {
|
|
382
|
-
if (node
|
|
412
|
+
if (this.isInsideApp(node)) return null;
|
|
413
|
+
return this.matchPopupClass(node);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 向上遍历 DOM,检测节点是否在某个浮层内部
|
|
417
|
+
*
|
|
418
|
+
* 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
|
|
419
|
+
* 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
|
|
420
|
+
* 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
|
|
421
|
+
*
|
|
422
|
+
* @returns 浮层类型或 null (不在任何浮层内)
|
|
423
|
+
*/
|
|
424
|
+
detectPopupAncestor(node) {
|
|
425
|
+
let current = node.parentElement;
|
|
426
|
+
while (current) {
|
|
427
|
+
const type = this.matchPopupClass(current);
|
|
428
|
+
if (type) {
|
|
429
|
+
const reachesBody = this.reachesBody(current);
|
|
430
|
+
if (reachesBody) return type;
|
|
431
|
+
}
|
|
432
|
+
if (current === document.body || current.id === "app") break;
|
|
433
|
+
current = current.parentElement;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 验证节点的祖先链是否能到达 document.body
|
|
439
|
+
* (经过若干层 wrapper 后依然是 body 下的浮层)
|
|
440
|
+
*/
|
|
441
|
+
reachesBody(node) {
|
|
442
|
+
let current = node;
|
|
443
|
+
while (current && current !== document.body) {
|
|
444
|
+
if (current.id === "app") return false;
|
|
445
|
+
current = current.parentElement;
|
|
446
|
+
}
|
|
447
|
+
return current === document.body;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 检测节点 class 是否匹配某个浮层类型
|
|
451
|
+
*/
|
|
452
|
+
matchPopupClass(node) {
|
|
383
453
|
const classStr = node.className || "";
|
|
384
454
|
if (typeof classStr !== "string") return null;
|
|
385
|
-
const entries = Object.entries(
|
|
455
|
+
const entries = Object.entries(this.popupClassMap);
|
|
386
456
|
for (const [type, selector] of entries) {
|
|
387
457
|
const classes = selector.replace(/^\./, "").split(".");
|
|
388
458
|
const allMatch = classes.every(
|
|
@@ -412,6 +482,22 @@ var TestIdObserver = class {
|
|
|
412
482
|
const testId = `${config.runtimePagePrefix}${route}_${tag}_${current}`;
|
|
413
483
|
node.setAttribute("data-testid", testId);
|
|
414
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
|
|
487
|
+
*
|
|
488
|
+
* ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
|
|
489
|
+
* 例: hall_modal_button_0, hall_select_div_2
|
|
490
|
+
*/
|
|
491
|
+
handlePopupChildNode(node, popupType, config) {
|
|
492
|
+
if (config.onlyInteractive && !this.isInteractive(node)) return;
|
|
493
|
+
const tag = this.getSimpleTag(node);
|
|
494
|
+
const key = `${popupType}_${tag}`;
|
|
495
|
+
const current = this.state.popupChildCounter.get(key) ?? 0;
|
|
496
|
+
this.state.popupChildCounter.set(key, current + 1);
|
|
497
|
+
const prefix = config.popupPrefixMap[popupType] || `${popupType}_`;
|
|
498
|
+
const testId = `${prefix}${tag}_${current}`;
|
|
499
|
+
node.setAttribute("data-testid", testId);
|
|
500
|
+
}
|
|
415
501
|
// ==========================================================
|
|
416
502
|
// 辅助方法
|
|
417
503
|
// ==========================================================
|
package/dist/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ var defaultConfig = {
|
|
|
18
18
|
globalPrefix: "",
|
|
19
19
|
compilePrefix: "static_",
|
|
20
20
|
runtimePagePrefix: "dynamic_",
|
|
21
|
+
antdClassPrefix: "ant",
|
|
21
22
|
popupPrefixMap: {
|
|
22
23
|
modal: "modal_",
|
|
23
24
|
drawer: "drawer_",
|
|
@@ -139,15 +140,23 @@ function buildPopupTestId(type, tag, counterId) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// src/utils/testIdObserver.ts
|
|
142
|
-
var
|
|
143
|
-
modal: ".
|
|
144
|
-
drawer: ".
|
|
145
|
-
select: ".
|
|
146
|
-
datePicker: ".
|
|
147
|
-
popconfirm: ".
|
|
148
|
-
dropdown: ".
|
|
149
|
-
tooltip: ".
|
|
143
|
+
var POPUP_CLASS_SUFFIX_MAP = {
|
|
144
|
+
modal: ".{prefix}-modal",
|
|
145
|
+
drawer: ".{prefix}-drawer",
|
|
146
|
+
select: ".{prefix}-select-dropdown",
|
|
147
|
+
datePicker: ".{prefix}-picker-dropdown",
|
|
148
|
+
popconfirm: ".{prefix}-popover.{prefix}-popconfirm",
|
|
149
|
+
dropdown: ".{prefix}-dropdown",
|
|
150
|
+
tooltip: ".{prefix}-tooltip"
|
|
150
151
|
};
|
|
152
|
+
function buildPopupClassMap(prefix) {
|
|
153
|
+
const result = {};
|
|
154
|
+
const entries = Object.entries(POPUP_CLASS_SUFFIX_MAP);
|
|
155
|
+
for (const [type, template] of entries) {
|
|
156
|
+
result[type] = template.replace(/\{prefix\}/g, prefix);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
151
160
|
var TestIdObserver = class {
|
|
152
161
|
constructor() {
|
|
153
162
|
// ==========================================================
|
|
@@ -166,11 +175,14 @@ var TestIdObserver = class {
|
|
|
166
175
|
});
|
|
167
176
|
}
|
|
168
177
|
};
|
|
178
|
+
const config = getConfig();
|
|
179
|
+
this.popupClassMap = buildPopupClassMap(config.antdClassPrefix);
|
|
169
180
|
this.state = {
|
|
170
181
|
observer: null,
|
|
171
182
|
isRunning: false,
|
|
172
183
|
currentRoute: "",
|
|
173
|
-
dynamicCounter: /* @__PURE__ */ new Map()
|
|
184
|
+
dynamicCounter: /* @__PURE__ */ new Map(),
|
|
185
|
+
popupChildCounter: /* @__PURE__ */ new Map()
|
|
174
186
|
};
|
|
175
187
|
}
|
|
176
188
|
// ==========================================================
|
|
@@ -207,6 +219,12 @@ var TestIdObserver = class {
|
|
|
207
219
|
setRoute(routeName) {
|
|
208
220
|
this.state.currentRoute = routeName;
|
|
209
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* 重置浮层子节点计数器 (路由切换时调用)
|
|
224
|
+
*/
|
|
225
|
+
resetPopupChildCounter() {
|
|
226
|
+
this.state.popupChildCounter.clear();
|
|
227
|
+
}
|
|
210
228
|
/**
|
|
211
229
|
* 全量扫描当前 DOM (用于启动时兜底, 处理 Observer 启动前已渲染的节点)
|
|
212
230
|
*/
|
|
@@ -244,8 +262,9 @@ var TestIdObserver = class {
|
|
|
244
262
|
* 决策优先级:
|
|
245
263
|
* 0. 已有 data-testid → 跳过
|
|
246
264
|
* 1. 带 data-test-base-key → 公共组件实例 (锚点定位)
|
|
247
|
-
* 2. body
|
|
248
|
-
* 3.
|
|
265
|
+
* 2. body 直系浮层根节点 → 匹配浮层类型 → 独立前缀
|
|
266
|
+
* 3. 浮层内部子节点 → 查找浮层祖先 → 浮层独立前缀 + 计数器
|
|
267
|
+
* 4. #app 内普通节点 → dynamic
|
|
249
268
|
*/
|
|
250
269
|
processSingleNode(node) {
|
|
251
270
|
const config = getConfig();
|
|
@@ -261,6 +280,11 @@ var TestIdObserver = class {
|
|
|
261
280
|
this.handlePopupNode(node, popupType);
|
|
262
281
|
return;
|
|
263
282
|
}
|
|
283
|
+
const popupAncestorType = this.detectPopupAncestor(node);
|
|
284
|
+
if (popupAncestorType) {
|
|
285
|
+
this.handlePopupChildNode(node, popupAncestorType, config);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
264
288
|
if (this.isInsideApp(node)) {
|
|
265
289
|
this.handleDynamicNode(node, config);
|
|
266
290
|
}
|
|
@@ -331,16 +355,62 @@ var TestIdObserver = class {
|
|
|
331
355
|
node.setAttribute("data-testid", testId);
|
|
332
356
|
}
|
|
333
357
|
/**
|
|
334
|
-
*
|
|
358
|
+
* 检测节点是否为浮层根节点
|
|
359
|
+
*
|
|
360
|
+
* Ant Design Vue 4.x 可能用一层 wrapper DIV 包裹浮层再 append 到 body:
|
|
361
|
+
* body → DIV(wrapper) → DIV.ant-picker-dropdown → panel
|
|
362
|
+
*
|
|
363
|
+
* 因此不要求 parentElement === document.body,
|
|
364
|
+
* 只要节点 class 匹配浮层类型且不在 #app 内即可。
|
|
335
365
|
*
|
|
336
366
|
* @param node - 待检测节点
|
|
337
|
-
* @returns 浮层类型或 null
|
|
367
|
+
* @returns 浮层类型或 null
|
|
338
368
|
*/
|
|
339
369
|
detectPopupType(node) {
|
|
340
|
-
if (node
|
|
370
|
+
if (this.isInsideApp(node)) return null;
|
|
371
|
+
return this.matchPopupClass(node);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 向上遍历 DOM,检测节点是否在某个浮层内部
|
|
375
|
+
*
|
|
376
|
+
* 查找最近的匹配 POPUP_CLASS_MAP 的祖先元素 (不要求它本身是 body 直系,
|
|
377
|
+
* 因为 Ant Design Vue 4 可能在浮层根节点外包一层 wrapper DIV)。
|
|
378
|
+
* 找到浮层根节点后,继续向上验证其祖先链能到达 body (确保不在 #app 内)。
|
|
379
|
+
*
|
|
380
|
+
* @returns 浮层类型或 null (不在任何浮层内)
|
|
381
|
+
*/
|
|
382
|
+
detectPopupAncestor(node) {
|
|
383
|
+
let current = node.parentElement;
|
|
384
|
+
while (current) {
|
|
385
|
+
const type = this.matchPopupClass(current);
|
|
386
|
+
if (type) {
|
|
387
|
+
const reachesBody = this.reachesBody(current);
|
|
388
|
+
if (reachesBody) return type;
|
|
389
|
+
}
|
|
390
|
+
if (current === document.body || current.id === "app") break;
|
|
391
|
+
current = current.parentElement;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 验证节点的祖先链是否能到达 document.body
|
|
397
|
+
* (经过若干层 wrapper 后依然是 body 下的浮层)
|
|
398
|
+
*/
|
|
399
|
+
reachesBody(node) {
|
|
400
|
+
let current = node;
|
|
401
|
+
while (current && current !== document.body) {
|
|
402
|
+
if (current.id === "app") return false;
|
|
403
|
+
current = current.parentElement;
|
|
404
|
+
}
|
|
405
|
+
return current === document.body;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 检测节点 class 是否匹配某个浮层类型
|
|
409
|
+
*/
|
|
410
|
+
matchPopupClass(node) {
|
|
341
411
|
const classStr = node.className || "";
|
|
342
412
|
if (typeof classStr !== "string") return null;
|
|
343
|
-
const entries = Object.entries(
|
|
413
|
+
const entries = Object.entries(this.popupClassMap);
|
|
344
414
|
for (const [type, selector] of entries) {
|
|
345
415
|
const classes = selector.replace(/^\./, "").split(".");
|
|
346
416
|
const allMatch = classes.every(
|
|
@@ -370,6 +440,22 @@ var TestIdObserver = class {
|
|
|
370
440
|
const testId = `${config.runtimePagePrefix}${route}_${tag}_${current}`;
|
|
371
441
|
node.setAttribute("data-testid", testId);
|
|
372
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* 处理浮层内部子节点 (Modal/Drawer/Dropdown 内的按钮、输入框等)
|
|
445
|
+
*
|
|
446
|
+
* ID 格式: ${popupPrefix}${popupType}_${tag}_${counter}
|
|
447
|
+
* 例: hall_modal_button_0, hall_select_div_2
|
|
448
|
+
*/
|
|
449
|
+
handlePopupChildNode(node, popupType, config) {
|
|
450
|
+
if (config.onlyInteractive && !this.isInteractive(node)) return;
|
|
451
|
+
const tag = this.getSimpleTag(node);
|
|
452
|
+
const key = `${popupType}_${tag}`;
|
|
453
|
+
const current = this.state.popupChildCounter.get(key) ?? 0;
|
|
454
|
+
this.state.popupChildCounter.set(key, current + 1);
|
|
455
|
+
const prefix = config.popupPrefixMap[popupType] || `${popupType}_`;
|
|
456
|
+
const testId = `${prefix}${tag}_${current}`;
|
|
457
|
+
node.setAttribute("data-testid", testId);
|
|
458
|
+
}
|
|
373
459
|
// ==========================================================
|
|
374
460
|
// 辅助方法
|
|
375
461
|
// ==========================================================
|