@eko-ai/eko 1.0.9 → 1.0.10

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.
Files changed (280) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +139 -139
  3. package/dist/extension/script/bing.js +0 -0
  4. package/dist/extension/script/build_dom_tree.js +661 -661
  5. package/dist/extension/script/common.js +0 -0
  6. package/dist/extension/script/duckduckgo.js +0 -0
  7. package/dist/extension/script/google.js +0 -0
  8. package/dist/extension/tools/cancel_workflow.d.ts +9 -0
  9. package/dist/extension.cjs.js +75 -75
  10. package/dist/extension.esm.js +75 -75
  11. package/dist/extension_content_script.js +55 -55
  12. package/dist/fellou.cjs.js +18 -18
  13. package/dist/fellou.esm.js +18 -18
  14. package/dist/index.cjs.js +4356 -4255
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.esm.js +4356 -4256
  17. package/dist/jest.config.js +10 -0
  18. package/dist/jest.config.js.map +1 -0
  19. package/dist/nodejs.cjs.js +1627 -1627
  20. package/dist/nodejs.esm.js +1627 -1627
  21. package/dist/rollup.config.js +171 -0
  22. package/dist/rollup.config.js.map +1 -0
  23. package/dist/script.js +10 -0
  24. package/dist/script.js.map +1 -0
  25. package/dist/services/llm/deepseek-provider.d.ts +13 -0
  26. package/dist/services/llm/provider-factory.d.ts +4 -0
  27. package/dist/services/llm/siliconflow-provider.d.ts +13 -0
  28. package/dist/services/workflow/generator.d.ts +1 -0
  29. package/dist/src/core/eko.js +99 -0
  30. package/dist/src/core/eko.js.map +1 -0
  31. package/dist/src/core/tool-registry.js +51 -0
  32. package/dist/src/core/tool-registry.js.map +1 -0
  33. package/dist/src/extension/content/index.js +409 -0
  34. package/dist/src/extension/content/index.js.map +1 -0
  35. package/dist/src/extension/core.js +29 -0
  36. package/dist/src/extension/core.js.map +1 -0
  37. package/dist/src/extension/index.js +12 -0
  38. package/dist/src/extension/index.js.map +1 -0
  39. package/dist/src/extension/script/bing.js +25 -0
  40. package/dist/src/extension/script/bing.js.map +1 -0
  41. package/dist/src/extension/script/build_dom_tree.js +585 -0
  42. package/dist/src/extension/script/build_dom_tree.js.map +1 -0
  43. package/dist/src/extension/script/common.js +203 -0
  44. package/dist/src/extension/script/common.js.map +1 -0
  45. package/dist/src/extension/script/duckduckgo.js +25 -0
  46. package/dist/src/extension/script/duckduckgo.js.map +1 -0
  47. package/dist/src/extension/script/google.js +26 -0
  48. package/dist/src/extension/script/google.js.map +1 -0
  49. package/dist/src/extension/tools/browser.js +174 -0
  50. package/dist/src/extension/tools/browser.js.map +1 -0
  51. package/dist/src/extension/tools/browser_use.js +186 -0
  52. package/dist/src/extension/tools/browser_use.js.map +1 -0
  53. package/dist/src/extension/tools/element_click.js +123 -0
  54. package/dist/src/extension/tools/element_click.js.map +1 -0
  55. package/dist/src/extension/tools/export_file.js +93 -0
  56. package/dist/src/extension/tools/export_file.js.map +1 -0
  57. package/dist/src/extension/tools/extract_content.js +38 -0
  58. package/dist/src/extension/tools/extract_content.js.map +1 -0
  59. package/dist/src/extension/tools/find_element_position.js +125 -0
  60. package/dist/src/extension/tools/find_element_position.js.map +1 -0
  61. package/dist/src/extension/tools/html_script.js +219 -0
  62. package/dist/src/extension/tools/html_script.js.map +1 -0
  63. package/dist/src/extension/tools/index.js +12 -0
  64. package/dist/src/extension/tools/index.js.map +1 -0
  65. package/dist/src/extension/tools/open_url.js +68 -0
  66. package/dist/src/extension/tools/open_url.js.map +1 -0
  67. package/dist/src/extension/tools/request_login.js +87 -0
  68. package/dist/src/extension/tools/request_login.js.map +1 -0
  69. package/dist/src/extension/tools/screenshot.js +26 -0
  70. package/dist/src/extension/tools/screenshot.js.map +1 -0
  71. package/dist/src/extension/tools/tab_management.js +160 -0
  72. package/dist/src/extension/tools/tab_management.js.map +1 -0
  73. package/dist/src/extension/tools/web_search.js +281 -0
  74. package/dist/src/extension/tools/web_search.js.map +1 -0
  75. package/dist/src/extension/utils.js +244 -0
  76. package/dist/src/extension/utils.js.map +1 -0
  77. package/dist/src/fellou/computer.js +104 -0
  78. package/dist/src/fellou/computer.js.map +1 -0
  79. package/dist/src/fellou/index.js +7 -0
  80. package/dist/src/fellou/index.js.map +1 -0
  81. package/dist/src/fellou/tools/computer_use.js +111 -0
  82. package/dist/src/fellou/tools/computer_use.js.map +1 -0
  83. package/dist/src/index.js +9 -0
  84. package/dist/src/index.js.map +1 -0
  85. package/dist/src/models/action.js +364 -0
  86. package/dist/src/models/action.js.map +1 -0
  87. package/dist/src/models/workflow.js +120 -0
  88. package/dist/src/models/workflow.js.map +1 -0
  89. package/dist/src/nodejs/core.js +18 -0
  90. package/dist/src/nodejs/core.js.map +1 -0
  91. package/dist/src/nodejs/index.js +6 -0
  92. package/dist/src/nodejs/index.js.map +1 -0
  93. package/dist/src/nodejs/script/build_dom_tree.js +586 -0
  94. package/dist/src/nodejs/script/build_dom_tree.js.map +1 -0
  95. package/dist/src/nodejs/tools/browser_use.js +458 -0
  96. package/dist/src/nodejs/tools/browser_use.js.map +1 -0
  97. package/dist/src/nodejs/tools/command_execute.js +65 -0
  98. package/dist/src/nodejs/tools/command_execute.js.map +1 -0
  99. package/dist/src/nodejs/tools/file_read.js +45 -0
  100. package/dist/src/nodejs/tools/file_read.js.map +1 -0
  101. package/dist/src/nodejs/tools/file_write.js +95 -0
  102. package/dist/src/nodejs/tools/file_write.js.map +1 -0
  103. package/dist/src/nodejs/tools/index.js +5 -0
  104. package/dist/src/nodejs/tools/index.js.map +1 -0
  105. package/dist/src/schemas/workflow.schema.js +64 -0
  106. package/dist/src/schemas/workflow.schema.js.map +1 -0
  107. package/dist/src/services/llm/claude-provider.js +140 -0
  108. package/dist/src/services/llm/claude-provider.js.map +1 -0
  109. package/dist/src/services/llm/deepseek-provider.js +432 -0
  110. package/dist/src/services/llm/deepseek-provider.js.map +1 -0
  111. package/dist/src/services/llm/glm-provider.js +317 -0
  112. package/dist/src/services/llm/glm-provider.js.map +1 -0
  113. package/dist/src/services/llm/openai-provider copy.js +317 -0
  114. package/dist/src/services/llm/openai-provider copy.js.map +1 -0
  115. package/dist/src/services/llm/openai-provider.js +317 -0
  116. package/dist/src/services/llm/openai-provider.js.map +1 -0
  117. package/dist/src/services/parser/workflow-parser.js +208 -0
  118. package/dist/src/services/parser/workflow-parser.js.map +1 -0
  119. package/dist/src/services/workflow/generator.js +105 -0
  120. package/dist/src/services/workflow/generator.js.map +1 -0
  121. package/dist/src/services/workflow/templates.js +42 -0
  122. package/dist/src/services/workflow/templates.js.map +1 -0
  123. package/dist/src/types/action.types.js +2 -0
  124. package/dist/src/types/action.types.js.map +1 -0
  125. package/dist/src/types/eko.types.js +2 -0
  126. package/dist/src/types/eko.types.js.map +1 -0
  127. package/dist/src/types/index.js +6 -0
  128. package/dist/src/types/index.js.map +1 -0
  129. package/dist/src/types/llm.types.js +2 -0
  130. package/dist/src/types/llm.types.js.map +1 -0
  131. package/dist/src/types/parser.types.js +2 -0
  132. package/dist/src/types/parser.types.js.map +1 -0
  133. package/dist/src/types/tools.types.js +2 -0
  134. package/dist/src/types/tools.types.js.map +1 -0
  135. package/dist/src/types/workflow.types.js +3 -0
  136. package/dist/src/types/workflow.types.js.map +1 -0
  137. package/dist/src/web/core.js +18 -0
  138. package/dist/src/web/core.js.map +1 -0
  139. package/dist/src/web/index.js +9 -0
  140. package/dist/src/web/index.js.map +1 -0
  141. package/dist/src/web/script/build_dom_tree.js +584 -0
  142. package/dist/src/web/script/build_dom_tree.js.map +1 -0
  143. package/dist/src/web/tools/browser.js +249 -0
  144. package/dist/src/web/tools/browser.js.map +1 -0
  145. package/dist/src/web/tools/browser_use.js +176 -0
  146. package/dist/src/web/tools/browser_use.js.map +1 -0
  147. package/dist/src/web/tools/element_click.js +121 -0
  148. package/dist/src/web/tools/element_click.js.map +1 -0
  149. package/dist/src/web/tools/export_file.js +74 -0
  150. package/dist/src/web/tools/export_file.js.map +1 -0
  151. package/dist/src/web/tools/extract_content.js +24 -0
  152. package/dist/src/web/tools/extract_content.js.map +1 -0
  153. package/dist/src/web/tools/find_element_position.js +121 -0
  154. package/dist/src/web/tools/find_element_position.js.map +1 -0
  155. package/dist/src/web/tools/html_script.js +219 -0
  156. package/dist/src/web/tools/html_script.js.map +1 -0
  157. package/dist/src/web/tools/index.js +8 -0
  158. package/dist/src/web/tools/index.js.map +1 -0
  159. package/dist/src/web/tools/screenshot.js +24 -0
  160. package/dist/src/web/tools/screenshot.js.map +1 -0
  161. package/dist/test/integration/claude-provider.test.js +170 -0
  162. package/dist/test/integration/claude-provider.test.js.map +1 -0
  163. package/dist/test/integration/deepseek-provider.test.js +171 -0
  164. package/dist/test/integration/deepseek-provider.test.js.map +1 -0
  165. package/dist/test/integration/glm-provider.test.js +173 -0
  166. package/dist/test/integration/glm-provider.test.js.map +1 -0
  167. package/dist/test/integration/openai-provider.test copy.js +170 -0
  168. package/dist/test/integration/openai-provider.test copy.js.map +1 -0
  169. package/dist/test/integration/openai-provider.test.js +170 -0
  170. package/dist/test/integration/openai-provider.test.js.map +1 -0
  171. package/dist/test/integration/qwen-provider.js +170 -0
  172. package/dist/test/integration/qwen-provider.js.map +1 -0
  173. package/dist/test/integration/qwen-provider.test copy.js +170 -0
  174. package/dist/test/integration/qwen-provider.test copy.js.map +1 -0
  175. package/dist/test/integration/qwen-provider.test.js +170 -0
  176. package/dist/test/integration/qwen-provider.test.js.map +1 -0
  177. package/dist/test/integration/workflow.execution.test.js +152 -0
  178. package/dist/test/integration/workflow.execution.test.js.map +1 -0
  179. package/dist/test/integration/workflow.generation-and-execution.test.js +131 -0
  180. package/dist/test/integration/workflow.generation-and-execution.test.js.map +1 -0
  181. package/dist/test/integration/workflow.generator.test.js +207 -0
  182. package/dist/test/integration/workflow.generator.test.js.map +1 -0
  183. package/dist/test/unit/action.test.js +186 -0
  184. package/dist/test/unit/action.test.js.map +1 -0
  185. package/dist/test/unit/tool-registry.test.js +99 -0
  186. package/dist/test/unit/tool-registry.test.js.map +1 -0
  187. package/dist/test/unit/workflow-parser.test.js +189 -0
  188. package/dist/test/unit/workflow-parser.test.js.map +1 -0
  189. package/dist/test/unit/workflow.test.js +102 -0
  190. package/dist/test/unit/workflow.test.js.map +1 -0
  191. package/dist/types/jest.config.d.ts +10 -0
  192. package/dist/types/rollup.config.d.ts +16 -0
  193. package/dist/types/script.d.ts +1 -0
  194. package/dist/types/src/core/eko.d.ts +20 -0
  195. package/dist/types/src/core/tool-registry.d.ts +13 -0
  196. package/dist/types/src/extension/content/index.d.ts +16 -0
  197. package/dist/types/src/extension/core.d.ts +11 -0
  198. package/dist/types/src/extension/index.d.ts +7 -0
  199. package/dist/types/src/extension/script/bing.d.ts +0 -0
  200. package/dist/types/src/extension/script/build_dom_tree.d.ts +38 -0
  201. package/dist/types/src/extension/script/common.d.ts +0 -0
  202. package/dist/types/src/extension/script/duckduckgo.d.ts +0 -0
  203. package/dist/types/src/extension/script/google.d.ts +0 -0
  204. package/dist/types/src/extension/tools/browser.d.ts +22 -0
  205. package/dist/types/src/extension/tools/browser_use.d.ts +19 -0
  206. package/dist/types/src/extension/tools/element_click.d.ts +12 -0
  207. package/dist/types/src/extension/tools/export_file.d.ts +18 -0
  208. package/dist/types/src/extension/tools/extract_content.d.ts +18 -0
  209. package/dist/types/src/extension/tools/find_element_position.d.ts +12 -0
  210. package/dist/types/src/extension/tools/html_script.d.ts +10 -0
  211. package/dist/types/src/extension/tools/index.d.ts +11 -0
  212. package/dist/types/src/extension/tools/open_url.d.ts +18 -0
  213. package/dist/{extension/tools/form_autofill.d.ts → types/src/extension/tools/request_login.d.ts} +3 -4
  214. package/dist/types/src/extension/tools/screenshot.d.ts +18 -0
  215. package/dist/types/src/extension/tools/tab_management.d.ts +19 -0
  216. package/dist/types/src/extension/tools/web_search.d.ts +18 -0
  217. package/dist/types/src/extension/utils.d.ts +31 -0
  218. package/dist/types/src/fellou/computer.d.ts +20 -0
  219. package/dist/types/src/fellou/index.d.ts +6 -0
  220. package/dist/types/src/fellou/tools/computer_use.d.ts +18 -0
  221. package/dist/types/src/index.d.ts +8 -0
  222. package/dist/types/src/models/action.d.ts +22 -0
  223. package/dist/types/src/models/workflow.d.ts +16 -0
  224. package/dist/types/src/nodejs/core.d.ts +2 -0
  225. package/dist/types/src/nodejs/index.d.ts +3 -0
  226. package/dist/types/src/nodejs/script/build_dom_tree.d.ts +1 -0
  227. package/dist/types/src/nodejs/tools/browser_use.d.ts +28 -0
  228. package/dist/types/src/nodejs/tools/command_execute.d.ts +12 -0
  229. package/dist/types/src/nodejs/tools/file_read.d.ts +11 -0
  230. package/dist/types/src/nodejs/tools/file_write.d.ts +15 -0
  231. package/dist/types/src/nodejs/tools/index.d.ts +4 -0
  232. package/dist/types/src/schemas/workflow.schema.d.ts +88 -0
  233. package/dist/types/src/services/llm/claude-provider.d.ts +11 -0
  234. package/dist/types/src/services/llm/deepseek-provider.d.ts +15 -0
  235. package/dist/types/src/services/llm/glm-provider.d.ts +11 -0
  236. package/dist/types/src/services/llm/openai-provider copy.d.ts +11 -0
  237. package/dist/types/src/services/llm/openai-provider.d.ts +11 -0
  238. package/dist/types/src/services/parser/workflow-parser.d.ts +29 -0
  239. package/dist/types/src/services/workflow/generator.d.ts +13 -0
  240. package/dist/types/src/services/workflow/templates.d.ts +8 -0
  241. package/dist/types/src/types/action.types.d.ts +38 -0
  242. package/dist/types/src/types/eko.types.d.ts +21 -0
  243. package/dist/types/src/types/index.d.ts +5 -0
  244. package/dist/types/src/types/llm.types.d.ts +54 -0
  245. package/dist/types/src/types/parser.types.d.ts +9 -0
  246. package/dist/types/src/types/tools.types.d.ts +88 -0
  247. package/dist/types/src/types/workflow.types.d.ts +39 -0
  248. package/dist/types/src/web/core.d.ts +2 -0
  249. package/dist/types/src/web/index.d.ts +5 -0
  250. package/dist/types/src/web/script/build_dom_tree.d.ts +10 -0
  251. package/dist/types/src/web/tools/browser.d.ts +21 -0
  252. package/dist/types/src/web/tools/browser_use.d.ts +19 -0
  253. package/dist/types/src/web/tools/element_click.d.ts +12 -0
  254. package/dist/types/src/web/tools/export_file.d.ts +18 -0
  255. package/dist/types/src/web/tools/extract_content.d.ts +17 -0
  256. package/dist/types/src/web/tools/find_element_position.d.ts +12 -0
  257. package/dist/types/src/web/tools/html_script.d.ts +10 -0
  258. package/dist/types/src/web/tools/index.d.ts +7 -0
  259. package/dist/types/src/web/tools/screenshot.d.ts +18 -0
  260. package/dist/types/test/integration/claude-provider.test.d.ts +1 -0
  261. package/dist/types/test/integration/deepseek-provider.test.d.ts +1 -0
  262. package/dist/types/test/integration/glm-provider.test.d.ts +1 -0
  263. package/dist/types/test/integration/openai-provider.test copy.d.ts +1 -0
  264. package/dist/types/test/integration/openai-provider.test.d.ts +1 -0
  265. package/dist/types/test/integration/qwen-provider.d.ts +1 -0
  266. package/dist/types/test/integration/qwen-provider.test copy.d.ts +1 -0
  267. package/dist/types/test/integration/qwen-provider.test.d.ts +1 -0
  268. package/dist/types/test/integration/workflow.execution.test.d.ts +1 -0
  269. package/dist/types/test/integration/workflow.generation-and-execution.test.d.ts +1 -0
  270. package/dist/types/test/integration/workflow.generator.test.d.ts +1 -0
  271. package/dist/types/test/unit/action.test.d.ts +1 -0
  272. package/dist/types/test/unit/tool-registry.test.d.ts +1 -0
  273. package/dist/types/test/unit/workflow-parser.test.d.ts +1 -0
  274. package/dist/types/test/unit/workflow.test.d.ts +1 -0
  275. package/dist/universal_tools/human/text.d.ts +9 -0
  276. package/dist/web.cjs.js +117 -117
  277. package/dist/web.esm.js +117 -117
  278. package/package.json +106 -106
  279. package/dist/fellou/tools/index.d.ts +0 -2
  280. package/dist/types/framework.types.d.ts +0 -11
@@ -1,661 +1,661 @@
1
- /**
2
- * Get clickable elements on the page
3
- *
4
- * @param {*} doHighlightElements Is highlighted
5
- * @param {*} includeAttributes [attr_names...]
6
- * @returns { element_str, selector_map }
7
- */
8
- function get_clickable_elements(doHighlightElements = true, includeAttributes) {
9
- window.clickable_elements = {};
10
- let page_tree = build_dom_tree(doHighlightElements);
11
- let element_tree = parse_node(page_tree);
12
- let selector_map = create_selector_map(element_tree);
13
- let element_str = clickable_elements_to_string(element_tree, includeAttributes);
14
- return { element_str, selector_map };
15
- }
16
-
17
- function get_highlight_element(highlightIndex) {
18
- return window.clickable_elements[highlightIndex];
19
- }
20
-
21
- function remove_highlight() {
22
- let highlight = document.getElementById('playwright-highlight-container');
23
- if (highlight) {
24
- highlight.remove();
25
- }
26
- }
27
-
28
- function clickable_elements_to_string(element_tree, includeAttributes) {
29
- if (!includeAttributes) {
30
- includeAttributes = [
31
- 'id',
32
- 'title',
33
- 'type',
34
- 'name',
35
- 'role',
36
- // 'href',
37
- 'tabindex',
38
- 'aria-label',
39
- 'placeholder',
40
- 'value',
41
- 'alt',
42
- 'aria-expanded',
43
- ];
44
- }
45
-
46
- function get_all_text_till_next_clickable_element(element_node) {
47
- let text_parts = [];
48
- function collect_text(node) {
49
- if (node.tagName && node != element_node && node.highlightIndex != null) {
50
- return;
51
- }
52
- if (!node.tagName && node.text) {
53
- text_parts.push(node.text);
54
- } else if (node.tagName) {
55
- for (let i = 0; i < node.children.length; i++) {
56
- collect_text(node.children[i]);
57
- }
58
- }
59
- }
60
- collect_text(element_node);
61
- return text_parts.join('\n').trim().replace(/\n+/g, ' ');
62
- }
63
-
64
- function has_parent_with_highlight_index(node) {
65
- let current = node.parent;
66
- while (current) {
67
- if (current.highlightIndex != null) {
68
- return true;
69
- }
70
- current = current.parent;
71
- }
72
- return false;
73
- }
74
-
75
- let formatted_text = [];
76
- function process_node(node, depth) {
77
- if (node.text == null) {
78
- if (node.highlightIndex != null) {
79
- let attributes_str = '';
80
- if (includeAttributes) {
81
- for (let i = 0; i < includeAttributes.length; i++) {
82
- let key = includeAttributes[i];
83
- let value = node.attributes[key];
84
- if (key && value) {
85
- attributes_str += ` ${key}="${value}"`;
86
- }
87
- }
88
- attributes_str = attributes_str.replace(/\n+/g, ' ');
89
- }
90
- let text = get_all_text_till_next_clickable_element(node);
91
- formatted_text.push(
92
- `[${node.highlightIndex}]:<${node.tagName}${attributes_str}>${text}</${node.tagName}>`
93
- );
94
- }
95
- for (let i = 0; i < node.children.length; i++) {
96
- let child = node.children[i];
97
- process_node(child, depth + 1);
98
- }
99
- } else if (!has_parent_with_highlight_index(node)) {
100
- formatted_text.push(`[]:${node.text}`);
101
- }
102
- }
103
- process_node(element_tree, 0);
104
- return formatted_text.join('\n');
105
- }
106
-
107
- function create_selector_map(element_tree) {
108
- let selector_map = {};
109
- function process_node(node) {
110
- if (node.tagName) {
111
- if (node.highlightIndex != null) {
112
- selector_map[node.highlightIndex] = node;
113
- }
114
- for (let i = 0; i < node.children.length; i++) {
115
- process_node(node.children[i]);
116
- }
117
- }
118
- }
119
- process_node(element_tree);
120
- return selector_map;
121
- }
122
-
123
- function parse_node(node_data, parent) {
124
- if (!node_data) {
125
- return;
126
- }
127
- if (node_data.type == 'TEXT_NODE') {
128
- return {
129
- text: node_data.text || '',
130
- isVisible: node_data.isVisible || false,
131
- parent: parent,
132
- };
133
- }
134
- let element_node = {
135
- tagName: node_data.tagName,
136
- xpath: node_data.xpath,
137
- highlightIndex: node_data.highlightIndex,
138
- attributes: node_data.attributes || {},
139
- isVisible: node_data.isVisible || false,
140
- isInteractive: node_data.isInteractive || false,
141
- isTopElement: node_data.isTopElement || false,
142
- shadowRoot: node_data.shadowRoot || false,
143
- children: [],
144
- parent: parent,
145
- };
146
- if (node_data.children) {
147
- let children = [];
148
- for (let i = 0; i < node_data.children.length; i++) {
149
- let child = node_data.children[i];
150
- if (child) {
151
- let child_node = parse_node(child, element_node);
152
- if (child_node) {
153
- children.push(child_node);
154
- }
155
- }
156
- }
157
- element_node.children = children;
158
- }
159
- return element_node;
160
- }
161
-
162
- function build_dom_tree(doHighlightElements) {
163
- let highlightIndex = 0; // Reset highlight index
164
-
165
- function highlightElement(element, index, parentIframe = null) {
166
- // Create or get highlight container
167
- let container = document.getElementById('playwright-highlight-container');
168
- if (!container) {
169
- container = document.createElement('div');
170
- container.id = 'playwright-highlight-container';
171
- container.style.position = 'fixed';
172
- container.style.pointerEvents = 'none';
173
- container.style.top = '0';
174
- container.style.left = '0';
175
- container.style.width = '100%';
176
- container.style.height = '100%';
177
- container.style.zIndex = '2147483647'; // Maximum z-index value
178
- document.documentElement.appendChild(container);
179
- }
180
-
181
- // Generate a color based on the index
182
- const colors = [
183
- '#FF0000',
184
- '#00FF00',
185
- '#0000FF',
186
- '#FFA500',
187
- '#800080',
188
- '#008080',
189
- '#FF69B4',
190
- '#4B0082',
191
- '#FF4500',
192
- '#2E8B57',
193
- '#DC143C',
194
- '#4682B4',
195
- ];
196
- const colorIndex = index % colors.length;
197
- const baseColor = colors[colorIndex];
198
- const backgroundColor = `${baseColor}1A`; // 10% opacity version of the color
199
-
200
- // Create highlight overlay
201
- const overlay = document.createElement('div');
202
- overlay.style.position = 'absolute';
203
- overlay.style.border = `2px solid ${baseColor}`;
204
- overlay.style.pointerEvents = 'none';
205
- overlay.style.boxSizing = 'border-box';
206
-
207
- // Position overlay based on element
208
- const rect = element.getBoundingClientRect();
209
- let top = rect.top;
210
- let left = rect.left;
211
-
212
- if (rect.width < window.innerWidth / 2 || rect.height < window.innerHeight / 2) {
213
- overlay.style.backgroundColor = backgroundColor;
214
- }
215
-
216
- // Adjust position if element is inside an iframe
217
- if (parentIframe) {
218
- const iframeRect = parentIframe.getBoundingClientRect();
219
- top += iframeRect.top;
220
- left += iframeRect.left;
221
- }
222
-
223
- overlay.style.top = `${top}px`;
224
- overlay.style.left = `${left}px`;
225
- overlay.style.width = `${rect.width}px`;
226
- overlay.style.height = `${rect.height}px`;
227
-
228
- // Create label
229
- const label = document.createElement('div');
230
- label.className = 'playwright-highlight-label';
231
- label.style.position = 'absolute';
232
- label.style.background = baseColor;
233
- label.style.color = 'white';
234
- label.style.padding = '1px 4px';
235
- label.style.borderRadius = '4px';
236
- label.style.fontSize = `${Math.min(12, Math.max(8, rect.height / 2))}px`; // Responsive font size
237
- label.textContent = index;
238
-
239
- // Calculate label position
240
- const labelWidth = 20; // Approximate width
241
- const labelHeight = 16; // Approximate height
242
-
243
- // Default position (top-right corner inside the box)
244
- let labelTop = top + 2;
245
- let labelLeft = left + rect.width - labelWidth - 2;
246
-
247
- // Adjust if box is too small
248
- if (rect.width < labelWidth + 4 || rect.height < labelHeight + 4) {
249
- // Position outside the box if it's too small
250
- labelTop = top - labelHeight - 2;
251
- labelLeft = left + rect.width - labelWidth;
252
- }
253
-
254
- // Ensure label stays within viewport
255
- if (labelTop < 0) labelTop = top + 2;
256
- if (labelLeft < 0) labelLeft = left + 2;
257
- if (labelLeft + labelWidth > window.innerWidth) {
258
- labelLeft = left + rect.width - labelWidth - 2;
259
- }
260
-
261
- label.style.top = `${labelTop}px`;
262
- label.style.left = `${labelLeft}px`;
263
-
264
- // Add to container
265
- container.appendChild(overlay);
266
- container.appendChild(label);
267
-
268
- // Store reference for cleanup
269
- element.setAttribute('browser-user-highlight-id', `playwright-highlight-${index}`);
270
-
271
- return index + 1;
272
- }
273
-
274
- // Helper function to generate XPath as a tree
275
- function getXPathTree(element, stopAtBoundary = true) {
276
- const segments = [];
277
- let currentElement = element;
278
-
279
- while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
280
- // Stop if we hit a shadow root or iframe
281
- if (
282
- stopAtBoundary &&
283
- (currentElement.parentNode instanceof ShadowRoot ||
284
- currentElement.parentNode instanceof HTMLIFrameElement)
285
- ) {
286
- break;
287
- }
288
-
289
- let index = 0;
290
- let sibling = currentElement.previousSibling;
291
- while (sibling) {
292
- if (
293
- sibling.nodeType === Node.ELEMENT_NODE &&
294
- sibling.nodeName === currentElement.nodeName
295
- ) {
296
- index++;
297
- }
298
- sibling = sibling.previousSibling;
299
- }
300
-
301
- const tagName = currentElement.nodeName.toLowerCase();
302
- const xpathIndex = index > 0 ? `[${index + 1}]` : '';
303
- segments.unshift(`${tagName}${xpathIndex}`);
304
-
305
- currentElement = currentElement.parentNode;
306
- }
307
-
308
- return segments.join('/');
309
- }
310
-
311
- // Helper function to check if element is accepted
312
- function isElementAccepted(element) {
313
- const leafElementDenyList = new Set(['svg', 'script', 'style', 'link', 'meta']);
314
- return !leafElementDenyList.has(element.tagName.toLowerCase());
315
- }
316
-
317
- // Helper function to check if element is interactive
318
- function isInteractiveElement(element) {
319
- // Base interactive elements and roles
320
- const interactiveElements = new Set([
321
- 'a',
322
- 'button',
323
- 'details',
324
- 'embed',
325
- 'input',
326
- 'label',
327
- 'menu',
328
- 'menuitem',
329
- 'object',
330
- 'select',
331
- 'textarea',
332
- 'summary',
333
- ]);
334
-
335
- const interactiveRoles = new Set([
336
- 'button',
337
- 'menu',
338
- 'menuitem',
339
- 'link',
340
- 'checkbox',
341
- 'radio',
342
- 'slider',
343
- 'tab',
344
- 'tabpanel',
345
- 'textbox',
346
- 'combobox',
347
- 'grid',
348
- 'listbox',
349
- 'option',
350
- 'progressbar',
351
- 'scrollbar',
352
- 'searchbox',
353
- 'switch',
354
- 'tree',
355
- 'treeitem',
356
- 'spinbutton',
357
- 'tooltip',
358
- 'a-button-inner',
359
- 'a-dropdown-button',
360
- 'click',
361
- 'menuitemcheckbox',
362
- 'menuitemradio',
363
- 'a-button-text',
364
- 'button-text',
365
- 'button-icon',
366
- 'button-icon-only',
367
- 'button-text-icon-only',
368
- 'dropdown',
369
- 'combobox',
370
- ]);
371
-
372
- const tagName = element.tagName.toLowerCase();
373
- const role = element.getAttribute('role');
374
- const ariaRole = element.getAttribute('aria-role');
375
- const tabIndex = element.getAttribute('tabindex');
376
-
377
- // Basic role/attribute checks
378
- const hasInteractiveRole =
379
- interactiveElements.has(tagName) ||
380
- interactiveRoles.has(role) ||
381
- interactiveRoles.has(ariaRole) ||
382
- (tabIndex !== null && tabIndex !== '-1') ||
383
- element.getAttribute('data-action') === 'a-dropdown-select' ||
384
- element.getAttribute('data-action') === 'a-dropdown-button';
385
-
386
- if (hasInteractiveRole) return true;
387
-
388
- // Get computed style
389
- const style = window.getComputedStyle(element);
390
-
391
- // Check if element has click-like styling
392
- // const hasClickStyling = style.cursor === 'pointer' ||
393
- // element.style.cursor === 'pointer' ||
394
- // style.pointerEvents !== 'none';
395
-
396
- // Check for event listeners
397
- const hasClickHandler =
398
- element.onclick !== null ||
399
- element.getAttribute('onclick') !== null ||
400
- element.hasAttribute('ng-click') ||
401
- element.hasAttribute('@click') ||
402
- element.hasAttribute('v-on:click');
403
-
404
- // Helper function to safely get event listeners
405
- function getEventListeners(el) {
406
- try {
407
- // Try to get listeners using Chrome DevTools API
408
- return window.getEventListeners?.(el) || {};
409
- } catch (e) {
410
- // Fallback: check for common event properties
411
- const listeners = {};
412
-
413
- // List of common event types to check
414
- const eventTypes = [
415
- 'click',
416
- 'mousedown',
417
- 'mouseup',
418
- 'touchstart',
419
- 'touchend',
420
- 'keydown',
421
- 'keyup',
422
- 'focus',
423
- 'blur',
424
- ];
425
-
426
- for (const type of eventTypes) {
427
- const handler = el[`on${type}`];
428
- if (handler) {
429
- listeners[type] = [
430
- {
431
- listener: handler,
432
- useCapture: false,
433
- },
434
- ];
435
- }
436
- }
437
-
438
- return listeners;
439
- }
440
- }
441
-
442
- // Check for click-related events on the element itself
443
- const listeners = getEventListeners(element);
444
- const hasClickListeners =
445
- listeners &&
446
- (listeners.click?.length > 0 ||
447
- listeners.mousedown?.length > 0 ||
448
- listeners.mouseup?.length > 0 ||
449
- listeners.touchstart?.length > 0 ||
450
- listeners.touchend?.length > 0);
451
-
452
- // Check for ARIA properties that suggest interactivity
453
- const hasAriaProps =
454
- element.hasAttribute('aria-expanded') ||
455
- element.hasAttribute('aria-pressed') ||
456
- element.hasAttribute('aria-selected') ||
457
- element.hasAttribute('aria-checked');
458
-
459
- // Check for form-related functionality
460
- const isFormRelated =
461
- element.form !== undefined ||
462
- element.hasAttribute('contenteditable') ||
463
- style.userSelect !== 'none';
464
-
465
- // Check if element is draggable
466
- const isDraggable = element.draggable || element.getAttribute('draggable') === 'true';
467
-
468
- return (
469
- hasAriaProps ||
470
- // hasClickStyling ||
471
- hasClickHandler ||
472
- hasClickListeners ||
473
- // isFormRelated ||
474
- isDraggable
475
- );
476
- }
477
-
478
- // Helper function to check if element is visible
479
- function isElementVisible(element) {
480
- const style = window.getComputedStyle(element);
481
- return (
482
- element.offsetWidth > 0 &&
483
- element.offsetHeight > 0 &&
484
- style.visibility !== 'hidden' &&
485
- style.display !== 'none'
486
- );
487
- }
488
-
489
- // Helper function to check if element is the top element at its position
490
- function isTopElement(element) {
491
- // Find the correct document context and root element
492
- let doc = element.ownerDocument;
493
-
494
- // If we're in an iframe, elements are considered top by default
495
- if (doc !== window.document) {
496
- return true;
497
- }
498
-
499
- // For shadow DOM, we need to check within its own root context
500
- const shadowRoot = element.getRootNode();
501
- if (shadowRoot instanceof ShadowRoot) {
502
- const rect = element.getBoundingClientRect();
503
- const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
504
-
505
- try {
506
- // Use shadow root's elementFromPoint to check within shadow DOM context
507
- const topEl = shadowRoot.elementFromPoint(point.x, point.y);
508
- if (!topEl) return false;
509
-
510
- // Check if the element or any of its parents match our target element
511
- let current = topEl;
512
- while (current && current !== shadowRoot) {
513
- if (current === element) return true;
514
- current = current.parentElement;
515
- }
516
- return false;
517
- } catch (e) {
518
- return true; // If we can't determine, consider it visible
519
- }
520
- }
521
-
522
- // Regular DOM elements
523
- const rect = element.getBoundingClientRect();
524
- const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
525
-
526
- try {
527
- const topEl = document.elementFromPoint(point.x, point.y);
528
- if (!topEl) return false;
529
-
530
- let current = topEl;
531
- while (current && current !== document.documentElement) {
532
- if (current === element) return true;
533
- current = current.parentElement;
534
- }
535
- return false;
536
- } catch (e) {
537
- return true;
538
- }
539
- }
540
-
541
- // Helper function to check if text node is visible
542
- function isTextNodeVisible(textNode) {
543
- const range = document.createRange();
544
- range.selectNodeContents(textNode);
545
- const rect = range.getBoundingClientRect();
546
-
547
- return (
548
- rect.width !== 0 &&
549
- rect.height !== 0 &&
550
- rect.top >= 0 &&
551
- rect.top <= window.innerHeight &&
552
- textNode.parentElement?.checkVisibility({
553
- checkOpacity: true,
554
- checkVisibilityCSS: true,
555
- })
556
- );
557
- }
558
-
559
- // Function to traverse the DOM and create nested JSON
560
- function buildDomTree(node, parentIframe = null) {
561
- if (!node) return null;
562
-
563
- // Special case for text nodes
564
- if (node.nodeType === Node.TEXT_NODE) {
565
- const textContent = node.textContent.trim();
566
- if (textContent && isTextNodeVisible(node)) {
567
- return {
568
- type: 'TEXT_NODE',
569
- text: textContent,
570
- isVisible: true,
571
- };
572
- }
573
- return null;
574
- }
575
-
576
- // Check if element is accepted
577
- if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
578
- return null;
579
- }
580
-
581
- const nodeData = {
582
- tagName: node.tagName ? node.tagName.toLowerCase() : null,
583
- attributes: {},
584
- xpath: node.nodeType === Node.ELEMENT_NODE ? getXPathTree(node, true) : null,
585
- children: [],
586
- };
587
-
588
- // Copy all attributes if the node is an element
589
- if (node.nodeType === Node.ELEMENT_NODE && node.attributes) {
590
- // Use getAttributeNames() instead of directly iterating attributes
591
- const attributeNames = node.getAttributeNames?.() || [];
592
- for (const name of attributeNames) {
593
- nodeData.attributes[name] = node.getAttribute(name);
594
- }
595
- }
596
-
597
- if (node.nodeType === Node.ELEMENT_NODE) {
598
- const isInteractive = isInteractiveElement(node);
599
- const isVisible = isElementVisible(node);
600
- const isTop = isTopElement(node);
601
-
602
- nodeData.isInteractive = isInteractive;
603
- nodeData.isVisible = isVisible;
604
- nodeData.isTopElement = isTop;
605
-
606
- // Highlight if element meets all criteria and highlighting is enabled
607
- if (isInteractive && isVisible && isTop) {
608
- nodeData.highlightIndex = highlightIndex++;
609
- window.clickable_elements[nodeData.highlightIndex] = node;
610
- if (doHighlightElements) {
611
- highlightElement(node, nodeData.highlightIndex, parentIframe);
612
- }
613
- }
614
- }
615
-
616
- // Only add iframeContext if we're inside an iframe
617
- // if (parentIframe) {
618
- // nodeData.iframeContext = `iframe[src="${parentIframe.src || ''}"]`;
619
- // }
620
-
621
- // Only add shadowRoot field if it exists
622
- if (node.shadowRoot) {
623
- nodeData.shadowRoot = true;
624
- }
625
-
626
- // Handle shadow DOM
627
- if (node.shadowRoot) {
628
- const shadowChildren = Array.from(node.shadowRoot.childNodes).map((child) =>
629
- buildDomTree(child, parentIframe)
630
- );
631
- nodeData.children.push(...shadowChildren);
632
- }
633
-
634
- // Handle iframes
635
- if (node.tagName === 'IFRAME') {
636
- try {
637
- const iframeDoc = node.contentDocument || node.contentWindow.document;
638
- if (iframeDoc) {
639
- const iframeChildren = Array.from(iframeDoc.body.childNodes).map((child) =>
640
- buildDomTree(child, node)
641
- );
642
- nodeData.children.push(...iframeChildren);
643
- }
644
- } catch (e) {
645
- console.warn('Unable to access iframe:', node);
646
- }
647
- } else {
648
- const children = Array.from(node.childNodes).map((child) =>
649
- buildDomTree(child, parentIframe)
650
- );
651
- nodeData.children.push(...children);
652
- }
653
-
654
- return nodeData;
655
- }
656
- return buildDomTree(document.body);
657
- }
658
-
659
- window.get_clickable_elements = get_clickable_elements;
660
- window.get_highlight_element = get_highlight_element;
661
- window.remove_highlight = remove_highlight;
1
+ /**
2
+ * Get clickable elements on the page
3
+ *
4
+ * @param {*} doHighlightElements Is highlighted
5
+ * @param {*} includeAttributes [attr_names...]
6
+ * @returns { element_str, selector_map }
7
+ */
8
+ function get_clickable_elements(doHighlightElements = true, includeAttributes) {
9
+ window.clickable_elements = {};
10
+ let page_tree = build_dom_tree(doHighlightElements);
11
+ let element_tree = parse_node(page_tree);
12
+ let selector_map = create_selector_map(element_tree);
13
+ let element_str = clickable_elements_to_string(element_tree, includeAttributes);
14
+ return { element_str, selector_map };
15
+ }
16
+
17
+ function get_highlight_element(highlightIndex) {
18
+ return window.clickable_elements[highlightIndex];
19
+ }
20
+
21
+ function remove_highlight() {
22
+ let highlight = document.getElementById('playwright-highlight-container');
23
+ if (highlight) {
24
+ highlight.remove();
25
+ }
26
+ }
27
+
28
+ function clickable_elements_to_string(element_tree, includeAttributes) {
29
+ if (!includeAttributes) {
30
+ includeAttributes = [
31
+ 'id',
32
+ 'title',
33
+ 'type',
34
+ 'name',
35
+ 'role',
36
+ // 'href',
37
+ 'tabindex',
38
+ 'aria-label',
39
+ 'placeholder',
40
+ 'value',
41
+ 'alt',
42
+ 'aria-expanded',
43
+ ];
44
+ }
45
+
46
+ function get_all_text_till_next_clickable_element(element_node) {
47
+ let text_parts = [];
48
+ function collect_text(node) {
49
+ if (node.tagName && node != element_node && node.highlightIndex != null) {
50
+ return;
51
+ }
52
+ if (!node.tagName && node.text) {
53
+ text_parts.push(node.text);
54
+ } else if (node.tagName) {
55
+ for (let i = 0; i < node.children.length; i++) {
56
+ collect_text(node.children[i]);
57
+ }
58
+ }
59
+ }
60
+ collect_text(element_node);
61
+ return text_parts.join('\n').trim().replace(/\n+/g, ' ');
62
+ }
63
+
64
+ function has_parent_with_highlight_index(node) {
65
+ let current = node.parent;
66
+ while (current) {
67
+ if (current.highlightIndex != null) {
68
+ return true;
69
+ }
70
+ current = current.parent;
71
+ }
72
+ return false;
73
+ }
74
+
75
+ let formatted_text = [];
76
+ function process_node(node, depth) {
77
+ if (node.text == null) {
78
+ if (node.highlightIndex != null) {
79
+ let attributes_str = '';
80
+ if (includeAttributes) {
81
+ for (let i = 0; i < includeAttributes.length; i++) {
82
+ let key = includeAttributes[i];
83
+ let value = node.attributes[key];
84
+ if (key && value) {
85
+ attributes_str += ` ${key}="${value}"`;
86
+ }
87
+ }
88
+ attributes_str = attributes_str.replace(/\n+/g, ' ');
89
+ }
90
+ let text = get_all_text_till_next_clickable_element(node);
91
+ formatted_text.push(
92
+ `[${node.highlightIndex}]:<${node.tagName}${attributes_str}>${text}</${node.tagName}>`
93
+ );
94
+ }
95
+ for (let i = 0; i < node.children.length; i++) {
96
+ let child = node.children[i];
97
+ process_node(child, depth + 1);
98
+ }
99
+ } else if (!has_parent_with_highlight_index(node)) {
100
+ formatted_text.push(`[]:${node.text}`);
101
+ }
102
+ }
103
+ process_node(element_tree, 0);
104
+ return formatted_text.join('\n');
105
+ }
106
+
107
+ function create_selector_map(element_tree) {
108
+ let selector_map = {};
109
+ function process_node(node) {
110
+ if (node.tagName) {
111
+ if (node.highlightIndex != null) {
112
+ selector_map[node.highlightIndex] = node;
113
+ }
114
+ for (let i = 0; i < node.children.length; i++) {
115
+ process_node(node.children[i]);
116
+ }
117
+ }
118
+ }
119
+ process_node(element_tree);
120
+ return selector_map;
121
+ }
122
+
123
+ function parse_node(node_data, parent) {
124
+ if (!node_data) {
125
+ return;
126
+ }
127
+ if (node_data.type == 'TEXT_NODE') {
128
+ return {
129
+ text: node_data.text || '',
130
+ isVisible: node_data.isVisible || false,
131
+ parent: parent,
132
+ };
133
+ }
134
+ let element_node = {
135
+ tagName: node_data.tagName,
136
+ xpath: node_data.xpath,
137
+ highlightIndex: node_data.highlightIndex,
138
+ attributes: node_data.attributes || {},
139
+ isVisible: node_data.isVisible || false,
140
+ isInteractive: node_data.isInteractive || false,
141
+ isTopElement: node_data.isTopElement || false,
142
+ shadowRoot: node_data.shadowRoot || false,
143
+ children: [],
144
+ parent: parent,
145
+ };
146
+ if (node_data.children) {
147
+ let children = [];
148
+ for (let i = 0; i < node_data.children.length; i++) {
149
+ let child = node_data.children[i];
150
+ if (child) {
151
+ let child_node = parse_node(child, element_node);
152
+ if (child_node) {
153
+ children.push(child_node);
154
+ }
155
+ }
156
+ }
157
+ element_node.children = children;
158
+ }
159
+ return element_node;
160
+ }
161
+
162
+ function build_dom_tree(doHighlightElements) {
163
+ let highlightIndex = 0; // Reset highlight index
164
+
165
+ function highlightElement(element, index, parentIframe = null) {
166
+ // Create or get highlight container
167
+ let container = document.getElementById('playwright-highlight-container');
168
+ if (!container) {
169
+ container = document.createElement('div');
170
+ container.id = 'playwright-highlight-container';
171
+ container.style.position = 'fixed';
172
+ container.style.pointerEvents = 'none';
173
+ container.style.top = '0';
174
+ container.style.left = '0';
175
+ container.style.width = '100%';
176
+ container.style.height = '100%';
177
+ container.style.zIndex = '2147483647'; // Maximum z-index value
178
+ document.documentElement.appendChild(container);
179
+ }
180
+
181
+ // Generate a color based on the index
182
+ const colors = [
183
+ '#FF0000',
184
+ '#00FF00',
185
+ '#0000FF',
186
+ '#FFA500',
187
+ '#800080',
188
+ '#008080',
189
+ '#FF69B4',
190
+ '#4B0082',
191
+ '#FF4500',
192
+ '#2E8B57',
193
+ '#DC143C',
194
+ '#4682B4',
195
+ ];
196
+ const colorIndex = index % colors.length;
197
+ const baseColor = colors[colorIndex];
198
+ const backgroundColor = `${baseColor}1A`; // 10% opacity version of the color
199
+
200
+ // Create highlight overlay
201
+ const overlay = document.createElement('div');
202
+ overlay.style.position = 'absolute';
203
+ overlay.style.border = `2px solid ${baseColor}`;
204
+ overlay.style.pointerEvents = 'none';
205
+ overlay.style.boxSizing = 'border-box';
206
+
207
+ // Position overlay based on element
208
+ const rect = element.getBoundingClientRect();
209
+ let top = rect.top;
210
+ let left = rect.left;
211
+
212
+ if (rect.width < window.innerWidth / 2 || rect.height < window.innerHeight / 2) {
213
+ overlay.style.backgroundColor = backgroundColor;
214
+ }
215
+
216
+ // Adjust position if element is inside an iframe
217
+ if (parentIframe) {
218
+ const iframeRect = parentIframe.getBoundingClientRect();
219
+ top += iframeRect.top;
220
+ left += iframeRect.left;
221
+ }
222
+
223
+ overlay.style.top = `${top}px`;
224
+ overlay.style.left = `${left}px`;
225
+ overlay.style.width = `${rect.width}px`;
226
+ overlay.style.height = `${rect.height}px`;
227
+
228
+ // Create label
229
+ const label = document.createElement('div');
230
+ label.className = 'playwright-highlight-label';
231
+ label.style.position = 'absolute';
232
+ label.style.background = baseColor;
233
+ label.style.color = 'white';
234
+ label.style.padding = '1px 4px';
235
+ label.style.borderRadius = '4px';
236
+ label.style.fontSize = `${Math.min(12, Math.max(8, rect.height / 2))}px`; // Responsive font size
237
+ label.textContent = index;
238
+
239
+ // Calculate label position
240
+ const labelWidth = 20; // Approximate width
241
+ const labelHeight = 16; // Approximate height
242
+
243
+ // Default position (top-right corner inside the box)
244
+ let labelTop = top + 2;
245
+ let labelLeft = left + rect.width - labelWidth - 2;
246
+
247
+ // Adjust if box is too small
248
+ if (rect.width < labelWidth + 4 || rect.height < labelHeight + 4) {
249
+ // Position outside the box if it's too small
250
+ labelTop = top - labelHeight - 2;
251
+ labelLeft = left + rect.width - labelWidth;
252
+ }
253
+
254
+ // Ensure label stays within viewport
255
+ if (labelTop < 0) labelTop = top + 2;
256
+ if (labelLeft < 0) labelLeft = left + 2;
257
+ if (labelLeft + labelWidth > window.innerWidth) {
258
+ labelLeft = left + rect.width - labelWidth - 2;
259
+ }
260
+
261
+ label.style.top = `${labelTop}px`;
262
+ label.style.left = `${labelLeft}px`;
263
+
264
+ // Add to container
265
+ container.appendChild(overlay);
266
+ container.appendChild(label);
267
+
268
+ // Store reference for cleanup
269
+ element.setAttribute('browser-user-highlight-id', `playwright-highlight-${index}`);
270
+
271
+ return index + 1;
272
+ }
273
+
274
+ // Helper function to generate XPath as a tree
275
+ function getXPathTree(element, stopAtBoundary = true) {
276
+ const segments = [];
277
+ let currentElement = element;
278
+
279
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
280
+ // Stop if we hit a shadow root or iframe
281
+ if (
282
+ stopAtBoundary &&
283
+ (currentElement.parentNode instanceof ShadowRoot ||
284
+ currentElement.parentNode instanceof HTMLIFrameElement)
285
+ ) {
286
+ break;
287
+ }
288
+
289
+ let index = 0;
290
+ let sibling = currentElement.previousSibling;
291
+ while (sibling) {
292
+ if (
293
+ sibling.nodeType === Node.ELEMENT_NODE &&
294
+ sibling.nodeName === currentElement.nodeName
295
+ ) {
296
+ index++;
297
+ }
298
+ sibling = sibling.previousSibling;
299
+ }
300
+
301
+ const tagName = currentElement.nodeName.toLowerCase();
302
+ const xpathIndex = index > 0 ? `[${index + 1}]` : '';
303
+ segments.unshift(`${tagName}${xpathIndex}`);
304
+
305
+ currentElement = currentElement.parentNode;
306
+ }
307
+
308
+ return segments.join('/');
309
+ }
310
+
311
+ // Helper function to check if element is accepted
312
+ function isElementAccepted(element) {
313
+ const leafElementDenyList = new Set(['svg', 'script', 'style', 'link', 'meta']);
314
+ return !leafElementDenyList.has(element.tagName.toLowerCase());
315
+ }
316
+
317
+ // Helper function to check if element is interactive
318
+ function isInteractiveElement(element) {
319
+ // Base interactive elements and roles
320
+ const interactiveElements = new Set([
321
+ 'a',
322
+ 'button',
323
+ 'details',
324
+ 'embed',
325
+ 'input',
326
+ 'label',
327
+ 'menu',
328
+ 'menuitem',
329
+ 'object',
330
+ 'select',
331
+ 'textarea',
332
+ 'summary',
333
+ ]);
334
+
335
+ const interactiveRoles = new Set([
336
+ 'button',
337
+ 'menu',
338
+ 'menuitem',
339
+ 'link',
340
+ 'checkbox',
341
+ 'radio',
342
+ 'slider',
343
+ 'tab',
344
+ 'tabpanel',
345
+ 'textbox',
346
+ 'combobox',
347
+ 'grid',
348
+ 'listbox',
349
+ 'option',
350
+ 'progressbar',
351
+ 'scrollbar',
352
+ 'searchbox',
353
+ 'switch',
354
+ 'tree',
355
+ 'treeitem',
356
+ 'spinbutton',
357
+ 'tooltip',
358
+ 'a-button-inner',
359
+ 'a-dropdown-button',
360
+ 'click',
361
+ 'menuitemcheckbox',
362
+ 'menuitemradio',
363
+ 'a-button-text',
364
+ 'button-text',
365
+ 'button-icon',
366
+ 'button-icon-only',
367
+ 'button-text-icon-only',
368
+ 'dropdown',
369
+ 'combobox',
370
+ ]);
371
+
372
+ const tagName = element.tagName.toLowerCase();
373
+ const role = element.getAttribute('role');
374
+ const ariaRole = element.getAttribute('aria-role');
375
+ const tabIndex = element.getAttribute('tabindex');
376
+
377
+ // Basic role/attribute checks
378
+ const hasInteractiveRole =
379
+ interactiveElements.has(tagName) ||
380
+ interactiveRoles.has(role) ||
381
+ interactiveRoles.has(ariaRole) ||
382
+ (tabIndex !== null && tabIndex !== '-1') ||
383
+ element.getAttribute('data-action') === 'a-dropdown-select' ||
384
+ element.getAttribute('data-action') === 'a-dropdown-button';
385
+
386
+ if (hasInteractiveRole) return true;
387
+
388
+ // Get computed style
389
+ const style = window.getComputedStyle(element);
390
+
391
+ // Check if element has click-like styling
392
+ // const hasClickStyling = style.cursor === 'pointer' ||
393
+ // element.style.cursor === 'pointer' ||
394
+ // style.pointerEvents !== 'none';
395
+
396
+ // Check for event listeners
397
+ const hasClickHandler =
398
+ element.onclick !== null ||
399
+ element.getAttribute('onclick') !== null ||
400
+ element.hasAttribute('ng-click') ||
401
+ element.hasAttribute('@click') ||
402
+ element.hasAttribute('v-on:click');
403
+
404
+ // Helper function to safely get event listeners
405
+ function getEventListeners(el) {
406
+ try {
407
+ // Try to get listeners using Chrome DevTools API
408
+ return window.getEventListeners?.(el) || {};
409
+ } catch (e) {
410
+ // Fallback: check for common event properties
411
+ const listeners = {};
412
+
413
+ // List of common event types to check
414
+ const eventTypes = [
415
+ 'click',
416
+ 'mousedown',
417
+ 'mouseup',
418
+ 'touchstart',
419
+ 'touchend',
420
+ 'keydown',
421
+ 'keyup',
422
+ 'focus',
423
+ 'blur',
424
+ ];
425
+
426
+ for (const type of eventTypes) {
427
+ const handler = el[`on${type}`];
428
+ if (handler) {
429
+ listeners[type] = [
430
+ {
431
+ listener: handler,
432
+ useCapture: false,
433
+ },
434
+ ];
435
+ }
436
+ }
437
+
438
+ return listeners;
439
+ }
440
+ }
441
+
442
+ // Check for click-related events on the element itself
443
+ const listeners = getEventListeners(element);
444
+ const hasClickListeners =
445
+ listeners &&
446
+ (listeners.click?.length > 0 ||
447
+ listeners.mousedown?.length > 0 ||
448
+ listeners.mouseup?.length > 0 ||
449
+ listeners.touchstart?.length > 0 ||
450
+ listeners.touchend?.length > 0);
451
+
452
+ // Check for ARIA properties that suggest interactivity
453
+ const hasAriaProps =
454
+ element.hasAttribute('aria-expanded') ||
455
+ element.hasAttribute('aria-pressed') ||
456
+ element.hasAttribute('aria-selected') ||
457
+ element.hasAttribute('aria-checked');
458
+
459
+ // Check for form-related functionality
460
+ const isFormRelated =
461
+ element.form !== undefined ||
462
+ element.hasAttribute('contenteditable') ||
463
+ style.userSelect !== 'none';
464
+
465
+ // Check if element is draggable
466
+ const isDraggable = element.draggable || element.getAttribute('draggable') === 'true';
467
+
468
+ return (
469
+ hasAriaProps ||
470
+ // hasClickStyling ||
471
+ hasClickHandler ||
472
+ hasClickListeners ||
473
+ // isFormRelated ||
474
+ isDraggable
475
+ );
476
+ }
477
+
478
+ // Helper function to check if element is visible
479
+ function isElementVisible(element) {
480
+ const style = window.getComputedStyle(element);
481
+ return (
482
+ element.offsetWidth > 0 &&
483
+ element.offsetHeight > 0 &&
484
+ style.visibility !== 'hidden' &&
485
+ style.display !== 'none'
486
+ );
487
+ }
488
+
489
+ // Helper function to check if element is the top element at its position
490
+ function isTopElement(element) {
491
+ // Find the correct document context and root element
492
+ let doc = element.ownerDocument;
493
+
494
+ // If we're in an iframe, elements are considered top by default
495
+ if (doc !== window.document) {
496
+ return true;
497
+ }
498
+
499
+ // For shadow DOM, we need to check within its own root context
500
+ const shadowRoot = element.getRootNode();
501
+ if (shadowRoot instanceof ShadowRoot) {
502
+ const rect = element.getBoundingClientRect();
503
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
504
+
505
+ try {
506
+ // Use shadow root's elementFromPoint to check within shadow DOM context
507
+ const topEl = shadowRoot.elementFromPoint(point.x, point.y);
508
+ if (!topEl) return false;
509
+
510
+ // Check if the element or any of its parents match our target element
511
+ let current = topEl;
512
+ while (current && current !== shadowRoot) {
513
+ if (current === element) return true;
514
+ current = current.parentElement;
515
+ }
516
+ return false;
517
+ } catch (e) {
518
+ return true; // If we can't determine, consider it visible
519
+ }
520
+ }
521
+
522
+ // Regular DOM elements
523
+ const rect = element.getBoundingClientRect();
524
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
525
+
526
+ try {
527
+ const topEl = document.elementFromPoint(point.x, point.y);
528
+ if (!topEl) return false;
529
+
530
+ let current = topEl;
531
+ while (current && current !== document.documentElement) {
532
+ if (current === element) return true;
533
+ current = current.parentElement;
534
+ }
535
+ return false;
536
+ } catch (e) {
537
+ return true;
538
+ }
539
+ }
540
+
541
+ // Helper function to check if text node is visible
542
+ function isTextNodeVisible(textNode) {
543
+ const range = document.createRange();
544
+ range.selectNodeContents(textNode);
545
+ const rect = range.getBoundingClientRect();
546
+
547
+ return (
548
+ rect.width !== 0 &&
549
+ rect.height !== 0 &&
550
+ rect.top >= 0 &&
551
+ rect.top <= window.innerHeight &&
552
+ textNode.parentElement?.checkVisibility({
553
+ checkOpacity: true,
554
+ checkVisibilityCSS: true,
555
+ })
556
+ );
557
+ }
558
+
559
+ // Function to traverse the DOM and create nested JSON
560
+ function buildDomTree(node, parentIframe = null) {
561
+ if (!node) return null;
562
+
563
+ // Special case for text nodes
564
+ if (node.nodeType === Node.TEXT_NODE) {
565
+ const textContent = node.textContent.trim();
566
+ if (textContent && isTextNodeVisible(node)) {
567
+ return {
568
+ type: 'TEXT_NODE',
569
+ text: textContent,
570
+ isVisible: true,
571
+ };
572
+ }
573
+ return null;
574
+ }
575
+
576
+ // Check if element is accepted
577
+ if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
578
+ return null;
579
+ }
580
+
581
+ const nodeData = {
582
+ tagName: node.tagName ? node.tagName.toLowerCase() : null,
583
+ attributes: {},
584
+ xpath: node.nodeType === Node.ELEMENT_NODE ? getXPathTree(node, true) : null,
585
+ children: [],
586
+ };
587
+
588
+ // Copy all attributes if the node is an element
589
+ if (node.nodeType === Node.ELEMENT_NODE && node.attributes) {
590
+ // Use getAttributeNames() instead of directly iterating attributes
591
+ const attributeNames = node.getAttributeNames?.() || [];
592
+ for (const name of attributeNames) {
593
+ nodeData.attributes[name] = node.getAttribute(name);
594
+ }
595
+ }
596
+
597
+ if (node.nodeType === Node.ELEMENT_NODE) {
598
+ const isInteractive = isInteractiveElement(node);
599
+ const isVisible = isElementVisible(node);
600
+ const isTop = isTopElement(node);
601
+
602
+ nodeData.isInteractive = isInteractive;
603
+ nodeData.isVisible = isVisible;
604
+ nodeData.isTopElement = isTop;
605
+
606
+ // Highlight if element meets all criteria and highlighting is enabled
607
+ if (isInteractive && isVisible && isTop) {
608
+ nodeData.highlightIndex = highlightIndex++;
609
+ window.clickable_elements[nodeData.highlightIndex] = node;
610
+ if (doHighlightElements) {
611
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
612
+ }
613
+ }
614
+ }
615
+
616
+ // Only add iframeContext if we're inside an iframe
617
+ // if (parentIframe) {
618
+ // nodeData.iframeContext = `iframe[src="${parentIframe.src || ''}"]`;
619
+ // }
620
+
621
+ // Only add shadowRoot field if it exists
622
+ if (node.shadowRoot) {
623
+ nodeData.shadowRoot = true;
624
+ }
625
+
626
+ // Handle shadow DOM
627
+ if (node.shadowRoot) {
628
+ const shadowChildren = Array.from(node.shadowRoot.childNodes).map((child) =>
629
+ buildDomTree(child, parentIframe)
630
+ );
631
+ nodeData.children.push(...shadowChildren);
632
+ }
633
+
634
+ // Handle iframes
635
+ if (node.tagName === 'IFRAME') {
636
+ try {
637
+ const iframeDoc = node.contentDocument || node.contentWindow.document;
638
+ if (iframeDoc) {
639
+ const iframeChildren = Array.from(iframeDoc.body.childNodes).map((child) =>
640
+ buildDomTree(child, node)
641
+ );
642
+ nodeData.children.push(...iframeChildren);
643
+ }
644
+ } catch (e) {
645
+ console.warn('Unable to access iframe:', node);
646
+ }
647
+ } else {
648
+ const children = Array.from(node.childNodes).map((child) =>
649
+ buildDomTree(child, parentIframe)
650
+ );
651
+ nodeData.children.push(...children);
652
+ }
653
+
654
+ return nodeData;
655
+ }
656
+ return buildDomTree(document.body);
657
+ }
658
+
659
+ window.get_clickable_elements = get_clickable_elements;
660
+ window.get_highlight_element = get_highlight_element;
661
+ window.remove_highlight = remove_highlight;