|
1 | 1 | // Package cdp provides a browser automation driver using Rod (go-rod/rod) + CDP. |
2 | 2 | package cdp |
3 | 3 |
|
| 4 | +import _ "embed" |
| 5 | + |
4 | 6 | // jsHelperCode is injected via page.EvalOnNewDocument() to persist across navigations. |
5 | 7 | // This is the last-resort fallback for finding elements when both the AX tree |
6 | 8 | // and page.Search() miss (rare, ~5% of cases). |
7 | | -const jsHelperCode = ` |
8 | | -window.__maestro = { |
9 | | - findByText: function(text) { |
10 | | - var lower = text.toLowerCase(); |
11 | | - var all = document.querySelectorAll('*'); |
12 | | - var best = null, bestDepth = -1; |
13 | | - for (var i = 0; i < all.length; i++) { |
14 | | - var el = all[i]; |
15 | | - var t = (el.textContent || '').trim().toLowerCase(); |
16 | | - var label = (el.getAttribute('aria-label') || '').toLowerCase(); |
17 | | - var ph = (el.getAttribute('placeholder') || '').toLowerCase(); |
18 | | - if (t.indexOf(lower) !== -1 || label.indexOf(lower) !== -1 || ph.indexOf(lower) !== -1) { |
19 | | - var d = 0, n = el; |
20 | | - while (n.parentElement) { d++; n = n.parentElement; } |
21 | | - if (d > bestDepth) { best = el; bestDepth = d; } |
22 | | - } |
23 | | - } |
24 | | - if (!best) throw new Error('not found: ' + text); |
25 | | - var p = best; |
26 | | - while (p && p !== document.body) { |
27 | | - var tag = p.tagName.toLowerCase(); |
28 | | - if (['a','button','input','select','textarea'].indexOf(tag) !== -1 || |
29 | | - p.getAttribute('role') === 'button' || p.getAttribute('tabindex') !== null) return p; |
30 | | - p = p.parentElement; |
31 | | - } |
32 | | - return best; |
33 | | - }, |
34 | | -
|
35 | | - // Visibility check: returns true if element is visible in the page. |
36 | | - _isElementVisible: function(el) { |
37 | | - if (!el || !el.isConnected) return false; |
38 | | - // Check offsetParent (null means display:none, except for body/html/fixed) |
39 | | - if (el.offsetParent === null) { |
40 | | - var style = window.getComputedStyle(el); |
41 | | - if (style.display === 'none') return false; |
42 | | - if (style.visibility === 'hidden') return false; |
43 | | - // Fixed/sticky elements have null offsetParent but can be visible |
44 | | - if (style.position !== 'fixed' && style.position !== 'sticky') { |
45 | | - // Check if it's body/html |
46 | | - var tag = el.tagName.toLowerCase(); |
47 | | - if (tag !== 'body' && tag !== 'html') return false; |
48 | | - } |
49 | | - } |
50 | | - var rect = el.getBoundingClientRect(); |
51 | | - if (rect.width === 0 && rect.height === 0) return false; |
52 | | - var style = window.getComputedStyle(el); |
53 | | - if (style.visibility === 'hidden' || style.opacity === '0') return false; |
54 | | - return true; |
55 | | - }, |
56 | | -
|
57 | | - // Find elements matching selector config and return true if any are visible. |
58 | | - // selectorType: "text", "id", "css", "textContains", "textRegex", or attribute types |
59 | | - _isAnyVisible: function(selectorType, selectorValue) { |
60 | | - var self = this; |
61 | | - var elements = self._findMatchingElements(selectorType, selectorValue); |
62 | | - for (var i = 0; i < elements.length; i++) { |
63 | | - if (self._isElementVisible(elements[i])) return true; |
64 | | - } |
65 | | - return false; |
66 | | - }, |
67 | | -
|
68 | | - // Find all elements matching a selector. |
69 | | - _findMatchingElements: function(selectorType, selectorValue) { |
70 | | - var results = []; |
71 | | - switch (selectorType) { |
72 | | - case 'css': |
73 | | - try { results = Array.from(document.querySelectorAll(selectorValue)); } catch(e) {} |
74 | | - break; |
75 | | - case 'id': |
76 | | - var el = document.getElementById(selectorValue); |
77 | | - if (el) results = [el]; |
78 | | - break; |
79 | | - case 'testId': |
80 | | - results = Array.from(document.querySelectorAll('[data-testid="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
81 | | - break; |
82 | | - case 'placeholder': |
83 | | - results = Array.from(document.querySelectorAll('[placeholder="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
84 | | - break; |
85 | | - case 'name': |
86 | | - results = Array.from(document.querySelectorAll('[name="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
87 | | - break; |
88 | | - case 'href': |
89 | | - results = Array.from(document.querySelectorAll('a[href*="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
90 | | - break; |
91 | | - case 'alt': |
92 | | - results = Array.from(document.querySelectorAll('[alt="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
93 | | - break; |
94 | | - case 'title': |
95 | | - results = Array.from(document.querySelectorAll('[title="' + selectorValue.replace(/"/g, '\\"') + '"]')); |
96 | | - break; |
97 | | - case 'text': { |
98 | | - var lower = selectorValue.toLowerCase(); |
99 | | - var all = document.querySelectorAll('*'); |
100 | | - for (var i = 0; i < all.length; i++) { |
101 | | - var el = all[i]; |
102 | | - var t = (el.textContent || '').trim().toLowerCase(); |
103 | | - var label = (el.getAttribute('aria-label') || '').toLowerCase(); |
104 | | - var ph = (el.getAttribute('placeholder') || '').toLowerCase(); |
105 | | - if (t === lower || label === lower || ph === lower || |
106 | | - t.indexOf(lower) !== -1 || label.indexOf(lower) !== -1 || ph.indexOf(lower) !== -1) { |
107 | | - results.push(el); |
108 | | - } |
109 | | - } |
110 | | - break; |
111 | | - } |
112 | | - case 'textContains': { |
113 | | - var lower = selectorValue.toLowerCase(); |
114 | | - var all = document.querySelectorAll('*'); |
115 | | - for (var i = 0; i < all.length; i++) { |
116 | | - var t = (all[i].textContent || '').trim().toLowerCase(); |
117 | | - if (t.indexOf(lower) !== -1) results.push(all[i]); |
118 | | - } |
119 | | - break; |
120 | | - } |
121 | | - case 'textRegex': { |
122 | | - try { |
123 | | - var re = new RegExp(selectorValue, 'i'); |
124 | | - var all = document.querySelectorAll('*'); |
125 | | - for (var i = 0; i < all.length; i++) { |
126 | | - var t = (all[i].textContent || '').trim(); |
127 | | - var label = all[i].getAttribute('aria-label') || ''; |
128 | | - if (re.test(t) || re.test(label)) results.push(all[i]); |
129 | | - } |
130 | | - } catch(e) {} |
131 | | - break; |
132 | | - } |
133 | | - case 'role': { |
134 | | - var roleSelector = '[role="' + selectorValue.replace(/"/g, '\\"') + '"]'; |
135 | | - results = Array.from(document.querySelectorAll(roleSelector)); |
136 | | - break; |
137 | | - } |
138 | | - } |
139 | | - return results; |
140 | | - }, |
141 | | -
|
142 | | - // RAF-based polling: waits until no matching element is visible or timeout. |
143 | | - // Returns a promise that resolves to true (element gone) or false (still visible at timeout). |
144 | | - waitForNotVisible: function(selectorType, selectorValue, timeoutMs) { |
145 | | - var self = this; |
146 | | - return new Promise(function(resolve) { |
147 | | - var deadline = Date.now() + timeoutMs; |
148 | | -
|
149 | | - // Quick check: already not visible? |
150 | | - if (!self._isAnyVisible(selectorType, selectorValue)) { |
151 | | - resolve(true); |
152 | | - return; |
153 | | - } |
154 | | -
|
155 | | - // RAF polling loop |
156 | | - function check() { |
157 | | - if (!self._isAnyVisible(selectorType, selectorValue)) { |
158 | | - resolve(true); |
159 | | - return; |
160 | | - } |
161 | | - if (Date.now() >= deadline) { |
162 | | - resolve(false); |
163 | | - return; |
164 | | - } |
165 | | - requestAnimationFrame(check); |
166 | | - } |
167 | | - requestAnimationFrame(check); |
168 | | - }); |
169 | | - }, |
170 | | -
|
171 | | - // RAF-based polling: waits until a matching element is visible or timeout. |
172 | | - // Returns a promise that resolves to true (element visible) or false (not found at timeout). |
173 | | - waitForVisible: function(selectorType, selectorValue, timeoutMs) { |
174 | | - var self = this; |
175 | | - return new Promise(function(resolve) { |
176 | | - var deadline = Date.now() + timeoutMs; |
177 | | -
|
178 | | - // Quick check: already visible? |
179 | | - if (self._isAnyVisible(selectorType, selectorValue)) { |
180 | | - resolve(true); |
181 | | - return; |
182 | | - } |
183 | | -
|
184 | | - // RAF polling loop |
185 | | - function check() { |
186 | | - if (self._isAnyVisible(selectorType, selectorValue)) { |
187 | | - resolve(true); |
188 | | - return; |
189 | | - } |
190 | | - if (Date.now() >= deadline) { |
191 | | - resolve(false); |
192 | | - return; |
193 | | - } |
194 | | - requestAnimationFrame(check); |
195 | | - } |
196 | | - requestAnimationFrame(check); |
197 | | - }); |
198 | | - } |
199 | | -}; |
200 | | -` |
| 9 | +// |
| 10 | +//go:embed jshelper.js |
| 11 | +var jsHelperCode string |
0 commit comments