Short answer: With the new v10.0.125 your code works exactly as you wrote it, so the answer is “update and your code is correct” ![]()
Longer answer:
Thanks for the report! You found a gap: in JS macros, uiv.$() returns a snapshot of the element (its .text, .value, position, etc.), not a live DOM handle — and until now that snapshot did not include attributes, so .getAttribute() didn’t exist on it.
Your code was the natural way to write it (it’s exactly how Selenium/Puppeteer work), so we added it: as of v10.0.125, DOM matches carry all their attributes and getAttribute works directly on the finder result:
const row = uiv.$('xpath=//table[@class="table table-bordered"][1]/tbody/tr[1]');
const cls = row.getAttribute('class'); // your original code — works now
uiv.log(`class = ${cls}`);
Details:
An absent attribute returns null (same as the DOM), it does not throw.
row.attributes gives you the whole attribute map as an object if you want to look at everything at once.
The values are read at find time, together with .text/.value — consistent with the snapshot design. For a live DOM property (e.g. .checked, or .href resolved to an absolute URL), keep using uiv.eval.
If you’re on an older version, either of these works there (but again, these workarounds are not longer needed since V10.0.125):
// page-world eval
const cls = uiv.eval('return document.querySelector("table.table-bordered tbody tr").getAttribute("class")');
// or the classic command bridge
uiv.run('storeAttribute', 'xpath=//table[@class="table table-bordered"][1]/tbody/tr[1]@class', 'v');
const cls = uiv.getVar('v');
The DemoExtract (JS) demo macro was also updated to show the new form.