Skip to content

Commit 68d56d2

Browse files
tmaheshmahesh-e27
andauthored
refactor: embed JavaScript helper code from a dedicated .js file rather than a Go string literal (#37)
Co-authored-by: Mahesh <mahesh@e27.ai>
1 parent 96b548e commit 68d56d2

2 files changed

Lines changed: 197 additions & 194 deletions

File tree

pkg/driver/browser/cdp/jshelper.go

Lines changed: 5 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,11 @@
11
// Package cdp provides a browser automation driver using Rod (go-rod/rod) + CDP.
22
package cdp
33

4+
import _ "embed"
5+
46
// jsHelperCode is injected via page.EvalOnNewDocument() to persist across navigations.
57
// This is the last-resort fallback for finding elements when both the AX tree
68
// 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

pkg/driver/browser/cdp/jshelper.js

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

0 commit comments

Comments
 (0)