@midscene/shared 0.17.2 → 0.17.3-beta-20250526042111.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.
@@ -17,20 +17,6 @@ const getElementIndex = (element: Element): number => {
17
17
  return index;
18
18
  };
19
19
 
20
- // Find the first ancestor with an ID
21
- const findFirstAncestorWithId = (element: Element): Element | null => {
22
- let current = element;
23
-
24
- while (current?.parentElement) {
25
- if (current.id) {
26
- return current;
27
- }
28
- current = current.parentElement;
29
- }
30
-
31
- return null;
32
- };
33
-
34
20
  // Get the index of a text node among its siblings of the same type
35
21
  const getTextNodeIndex = (textNode: Node): number => {
36
22
  let index = 1;
@@ -54,6 +40,12 @@ const getElementXPath = (element: Node): string => {
54
40
  if (parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
55
41
  const parentXPath = getElementXPath(parentNode);
56
42
  const textIndex = getTextNodeIndex(element);
43
+ const textContent = element.textContent?.trim();
44
+
45
+ // If we have text content, include it in the xpath for better matching
46
+ if (textContent) {
47
+ return `${parentXPath}/text()[${textIndex}][normalize-space()="${textContent}"]`;
48
+ }
57
49
  return `${parentXPath}/text()[${textIndex}]`;
58
50
  }
59
51
  return '';
@@ -72,36 +64,7 @@ const getElementXPath = (element: Node): string => {
72
64
  return '/html/body';
73
65
  }
74
66
 
75
- // If this element has an ID, return an XPath with the ID
76
- if (el.id) {
77
- return `//*[@id="${el.id}"]`;
78
- }
79
-
80
- const ancestorWithId = findFirstAncestorWithId(el);
81
-
82
- if (ancestorWithId) {
83
- // Start from the ancestor with ID
84
- const ancestorPath = `//*[@id="${ancestorWithId.id}"]`;
85
-
86
- // Build the path from the ancestor to this element
87
- let current: Element | null = el;
88
- const pathParts: string[] = [];
89
-
90
- while (current && current !== ancestorWithId) {
91
- const index = getElementIndex(current);
92
- const tagName = current.nodeName.toLowerCase();
93
- pathParts.unshift(`${tagName}[${index}]`);
94
- current = current.parentElement;
95
- }
96
-
97
- // Combine the ancestor path with the rest of the path
98
- return pathParts.length > 0
99
- ? `${ancestorPath}/${pathParts.join('/')}`
100
- : ancestorPath;
101
- }
102
-
103
- // If no parent node, or we need a full path and haven't returned yet,
104
- // start building the full path
67
+ // If no parent node, return just the tag name
105
68
  if (!el.parentNode) {
106
69
  return `/${el.nodeName.toLowerCase()}`;
107
70
  }
@@ -120,9 +83,9 @@ const getElementXPath = (element: Node): string => {
120
83
  function generateXPaths(node: Node | null): string[] {
121
84
  if (!node) return [];
122
85
 
123
- const xPath = getElementXPath(node);
86
+ const fullXPath = getElementXPath(node);
124
87
 
125
- return [xPath];
88
+ return [fullXPath];
126
89
  }
127
90
 
128
91
  export function getXpathsById(id: string): string[] | null {
@@ -6,6 +6,7 @@ import {
6
6
  import type { WebElementInfo } from '../types';
7
7
  import type { Point } from '../types';
8
8
  import {
9
+ isAElement,
9
10
  isButtonElement,
10
11
  isContainerElement,
11
12
  isFormElement,
@@ -126,6 +127,16 @@ export function collectElementInfo(
126
127
  }
127
128
 
128
129
  if (isButtonElement(node)) {
130
+ const rect = mergeElementAndChildrenRects(
131
+ node,
132
+ currentWindow,
133
+ currentDocument,
134
+ baseZoom,
135
+ visibleOnly,
136
+ );
137
+ if (!rect) {
138
+ return null;
139
+ }
129
140
  const attributes = getNodeAttributes(node, currentWindow);
130
141
  const pseudo = getPseudoElementContent(node, currentWindow);
131
142
  const content = node.innerText || pseudo.before || pseudo.after || '';
@@ -218,6 +229,34 @@ export function collectElementInfo(
218
229
  return elementInfo;
219
230
  }
220
231
 
232
+ if (isAElement(node)) {
233
+ const attributes = getNodeAttributes(node, currentWindow);
234
+ const pseudo = getPseudoElementContent(node, currentWindow);
235
+ const content = node.innerText || pseudo.before || pseudo.after || '';
236
+ const nodeHashId = midsceneGenerateHash(node, content, rect);
237
+ const selector = setDataForNode(node, nodeHashId, false, currentWindow);
238
+ const elementInfo: WebElementInfo = {
239
+ id: nodeHashId,
240
+ indexId: indexId++,
241
+ nodeHashId,
242
+ nodeType: NodeType.A,
243
+ locator: selector,
244
+ attributes: {
245
+ ...attributes,
246
+ htmlTagName: tagNameOfNode(node),
247
+ nodeType: NodeType.A,
248
+ },
249
+ content,
250
+ rect,
251
+ center: [
252
+ Math.round(rect.left + rect.width / 2),
253
+ Math.round(rect.top + rect.height / 2),
254
+ ],
255
+ zoom: rect.zoom,
256
+ };
257
+ return elementInfo;
258
+ }
259
+
221
260
  // else, consider as a container
222
261
  if (isContainerElement(node)) {
223
262
  const attributes = getNodeAttributes(node, currentWindow);
@@ -329,11 +368,12 @@ export function extractTreeNode(
329
368
  node: elementInfo,
330
369
  children: [],
331
370
  };
332
- // stop collecting if the node is a Button/Image/Text/FormItem/Container
371
+ // stop collecting if the node is a Button/Image/Text/A/FormItem/Container
333
372
  if (
334
373
  elementInfo?.nodeType === NodeType.BUTTON ||
335
374
  elementInfo?.nodeType === NodeType.IMG ||
336
375
  elementInfo?.nodeType === NodeType.TEXT ||
376
+ elementInfo?.nodeType === NodeType.A ||
337
377
  elementInfo?.nodeType === NodeType.FORM_ITEM ||
338
378
  elementInfo?.nodeType === NodeType.CONTAINER
339
379
  ) {
@@ -397,3 +437,56 @@ export function extractTreeNode(
397
437
  children: topChildren,
398
438
  };
399
439
  }
440
+
441
+ export function mergeElementAndChildrenRects(
442
+ node: Node,
443
+ currentWindow: typeof window,
444
+ currentDocument: typeof document,
445
+ baseZoom = 1,
446
+ visibleOnly = true,
447
+ ) {
448
+ const selfRect = elementRect(
449
+ node,
450
+ currentWindow,
451
+ currentDocument,
452
+ baseZoom,
453
+ visibleOnly,
454
+ );
455
+ if (!selfRect) return null;
456
+
457
+ let minLeft = selfRect.left;
458
+ let minTop = selfRect.top;
459
+ let maxRight = selfRect.left + selfRect.width;
460
+ let maxBottom = selfRect.top + selfRect.height;
461
+
462
+ function traverse(child: Node) {
463
+ for (let i = 0; i < child.childNodes.length; i++) {
464
+ const sub = child.childNodes[i];
465
+ if (sub.nodeType === 1) {
466
+ const rect = elementRect(
467
+ sub,
468
+ currentWindow,
469
+ currentDocument,
470
+ baseZoom,
471
+ visibleOnly,
472
+ );
473
+ if (rect) {
474
+ minLeft = Math.min(minLeft, rect.left);
475
+ minTop = Math.min(minTop, rect.top);
476
+ maxRight = Math.max(maxRight, rect.left + rect.width);
477
+ maxBottom = Math.max(maxBottom, rect.top + rect.height);
478
+ }
479
+ traverse(sub);
480
+ }
481
+ }
482
+ }
483
+ traverse(node);
484
+
485
+ return {
486
+ ...selfRect,
487
+ left: minLeft,
488
+ top: minTop,
489
+ width: maxRight - minLeft,
490
+ height: maxBottom - minTop,
491
+ };
492
+ }
package/src/logger.ts CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import util from 'node:util';
4
4
  import debug from 'debug';
5
- import { isNodeEnv, logDir } from './common';
5
+ import { getMidsceneRunSubDir, isNodeEnv } from './common';
6
6
 
7
7
  const topicPrefix = 'midscene';
8
8
  // Map to store file streams
@@ -12,7 +12,10 @@ const logStreams = new Map<string, fs.WriteStream>();
12
12
  function getLogStream(topic: string): fs.WriteStream {
13
13
  const topicFileName = topic.replace(/:/g, '-');
14
14
  if (!logStreams.has(topicFileName)) {
15
- const logFile = path.join(logDir, `${topicFileName}.log`);
15
+ const logFile = path.join(
16
+ getMidsceneRunSubDir('log'),
17
+ `${topicFileName}.log`,
18
+ );
16
19
  const stream = fs.createWriteStream(logFile, { flags: 'a' });
17
20
  logStreams.set(topicFileName, stream);
18
21
  }