@@ -21,13 +21,34 @@ func (b *Browser) GetURL() (string, error) {
2121}
2222
2323// GetText returns the inner text of the element with the given display ID.
24+ // For form elements (input, textarea, select), it returns the value or placeholder.
2425func (b * Browser ) GetText (id int ) (string , error ) {
25- return b .evaluateOnElement (id , `function() { return this.innerText || this.textContent || ''; }` )
26+ return b .evaluateOnElement (id , `function() {
27+ var tag = (this.tagName || '').toUpperCase();
28+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
29+ if (typeof this.value === 'string' && this.value !== '') return this.value;
30+ if (tag === 'SELECT' && this.selectedOptions && this.selectedOptions.length > 0) {
31+ return Array.from(this.selectedOptions).map(function(o) { return o.textContent; }).join(', ');
32+ }
33+ return this.placeholder || '';
34+ }
35+ return this.innerText || this.textContent || '';
36+ }` )
2637}
2738
2839// GetHTML returns the inner HTML of the element with the given display ID.
40+ // For void elements (input, img, br, etc.) where innerHTML is always empty,
41+ // it returns outerHTML instead.
2942func (b * Browser ) GetHTML (id int ) (string , error ) {
30- return b .evaluateOnElement (id , `function() { return this.innerHTML || ''; }` )
43+ return b .evaluateOnElement (id , `function() {
44+ var html = this.innerHTML;
45+ if (html === '' || html === undefined) {
46+ var tag = (this.tagName || '').toUpperCase();
47+ var voidTags = {'INPUT':1,'IMG':1,'BR':1,'HR':1,'META':1,'LINK':1,'AREA':1,'BASE':1,'COL':1,'EMBED':1,'SOURCE':1,'TRACK':1,'WBR':1};
48+ if (voidTags[tag]) return this.outerHTML || '';
49+ }
50+ return html || '';
51+ }` )
3152}
3253
3354// GetValue returns the value of a form element with the given display ID.
@@ -160,11 +181,22 @@ func (b *Browser) evaluateString(expression string) (string, error) {
160181
161182// evaluateOnElement resolves a display ID to a remote object and calls a JS function on it.
162183// Returns the string result.
184+ // If the backend node ID is stale (DOM has changed since last snapshot), it will
185+ // refresh the snapshot and retry once before falling back to interactive ordinal.
163186func (b * Browser ) evaluateOnElement (id int , function string , args ... any ) (string , error ) {
164187 ctx , cancel := b .operationContext ()
165188 defer cancel ()
166189
167190 remoteObj , _ , err := b .resolveRemoteObject (ctx , id )
191+ if err != nil {
192+ // Backend node ID may be stale; refresh snapshot and retry once
193+ if _ , snapErr := b .Snapshot (); snapErr == nil {
194+ if retryObj , _ , retryErr := b .resolveRemoteObject (ctx , id ); retryErr == nil {
195+ remoteObj = retryObj
196+ err = nil
197+ }
198+ }
199+ }
168200 if err != nil {
169201 // Fallback: use interactiveOrdinal if available
170202 return b .evaluateOnElementFallback (id , function , args ... )
@@ -183,10 +215,18 @@ func (b *Browser) evaluateOnElement(id int, function string, args ...any) (strin
183215 }
184216 return nil
185217 }))
218+ // If callFunctionOn failed (e.g. node was collected), try fallback
219+ if err != nil {
220+ if fallbackResult , fallbackErr := b .evaluateOnElementFallback (id , function , args ... ); fallbackErr == nil {
221+ return fallbackResult , nil
222+ }
223+ }
186224 return result , err
187225}
188226
189227// evaluateOnElementFallback uses querySelectorAll to find the element.
228+ // It first tries via interactive ordinal for interactive elements, then falls back
229+ // to a general tree-walker approach for any element.
190230func (b * Browser ) evaluateOnElementFallback (id int , function string , args ... any ) (string , error ) {
191231 // Build args JSON array for JS
192232 argsJSON := "[]"
@@ -199,18 +239,40 @@ func (b *Browser) evaluateOnElementFallback(id int, function string, args ...any
199239 }
200240
201241 ordinal , err := b .interactiveOrdinal (id )
202- if err != nil {
203- // Not interactive, try all elements via tree walk
242+ if err == nil {
243+ // Interactive element: use querySelectorAll
244+ expression := `(() => {
245+ const elements = Array.from(document.querySelectorAll(` + mustJSON (interactiveQuery ) + `));
246+ const el = elements[` + mustJSON (ordinal ) + `];
247+ if (!el) throw new Error('element not found');
248+ const fn = ` + function + `;
249+ const args = ` + argsJSON + `;
250+ return fn.apply(el, args);
251+ })()`
252+
253+ return b .evaluateString (expression )
254+ }
255+
256+ // Non-interactive element: use TreeWalker to find by position
257+ // Walk all element/text nodes and match by ordinal position in the AX tree
258+ allOrdinal := b .allElementOrdinal (id )
259+ if allOrdinal < 0 {
204260 return "" , fmt .Errorf ("element %d not found for query" , id )
205261 }
206262
207263 expression := `(() => {
208- const elements = Array.from(document.querySelectorAll(` + mustJSON (interactiveQuery ) + `));
209- const el = elements[` + mustJSON (ordinal ) + `];
210- if (!el) throw new Error('element not found');
211- const fn = ` + function + `;
212- const args = ` + argsJSON + `;
213- return fn.apply(el, args);
264+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
265+ let idx = -1;
266+ let node;
267+ while ((node = walker.nextNode())) {
268+ idx++;
269+ if (idx === ` + mustJSON (allOrdinal ) + `) {
270+ const fn = ` + function + `;
271+ const args = ` + argsJSON + `;
272+ return fn.apply(node, args);
273+ }
274+ }
275+ throw new Error('element not found at ordinal ' + ` + mustJSON (allOrdinal ) + `);
214276 })()`
215277
216278 return b .evaluateString (expression )
0 commit comments