Skip to content

Commit c438286

Browse files
committed
fix(desktop): expose browser control state to agents
1 parent 8c2f206 commit c438286

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

apps/desktop/src/main/browser-agent/page-functions.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,29 @@ describe('collectSnapshot', () => {
625625
expect(outlineOf(collectSnapshot())).toContain('gridcell "party-parrot"')
626626
})
627627

628+
it('reports native and ARIA control state in the snapshot', () => {
629+
document.body.innerHTML = `
630+
<input type="checkbox" aria-label="Email alerts" />
631+
<input type="radio" aria-label="Weekly" checked />
632+
<button aria-expanded="false" aria-pressed="true">Filters</button>
633+
<div role="switch" aria-label="Dark mode" aria-checked="mixed"></div>
634+
<div role="tab" aria-selected="true">Activity</div>
635+
<textarea aria-label="Notes" readonly required></textarea>
636+
<div role="textbox" aria-label="Summary" aria-readonly="true" aria-required="true"></div>
637+
`
638+
for (const element of document.body.children) visible(element as HTMLElement)
639+
640+
const outline = outlineOf(collectSnapshot())
641+
642+
expect(outline).toMatch(/checkbox "Email alerts" \[ref=\d+\] unchecked/)
643+
expect(outline).toMatch(/radio "Weekly" \[ref=\d+\] checked/)
644+
expect(outline).toMatch(/button "Filters" \[ref=\d+\] aria-expanded=false aria-pressed=true/)
645+
expect(outline).toMatch(/switch "Dark mode" \[ref=\d+\] aria-checked=mixed/)
646+
expect(outline).toMatch(/tab "Activity" \[ref=\d+\] aria-selected=true/)
647+
expect(outline).toMatch(/textbox "Notes" \[ref=\d+\] readonly required/)
648+
expect(outline).toMatch(/textbox "Summary" \[ref=\d+\] aria-readonly aria-required/)
649+
})
650+
628651
it('does not duplicate every descendant of an inherited pointer target', () => {
629652
document.body.innerHTML = `
630653
<div style="cursor: pointer">

apps/desktop/src/main/browser-agent/page-functions.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,18 +448,42 @@ export function collectSnapshot(startingElementId = 0): unknown {
448448
// like any other. Redaction above is realm-safe and runs first, so
449449
// widening this cannot expose a credential field.
450450
const value = (el as HTMLInputElement).value
451-
if (tag === 'INPUT' && (el as HTMLInputElement).type === 'file') {
451+
const inputType = tag === 'INPUT' ? (el as HTMLInputElement).type : ''
452+
if (inputType === 'file') {
452453
parts.push('upload-unsupported')
453-
} else if (value && isSensitiveValueField(el)) parts.push('value-withheld')
454-
else if (value) parts.push(`value=${quote(cut(String(value), 120))}`)
454+
} else if (inputType !== 'checkbox' && inputType !== 'radio') {
455+
if (value && isSensitiveValueField(el)) parts.push('value-withheld')
456+
else if (value) parts.push(`value=${quote(cut(String(value), 120))}`)
457+
}
455458
}
456459
if (tag === 'A') {
457460
const href = el.getAttribute('href')
458461
if (href) parts.push(`href=${quote(cut(href, 200))}`)
459462
}
460463
if ((el as HTMLInputElement).disabled === true) parts.push('disabled')
461464
if (el.getAttribute('aria-disabled') === 'true') parts.push('aria-disabled')
462-
if ((el as HTMLInputElement).checked === true) parts.push('checked')
465+
if (el.getAttribute('aria-readonly') === 'true') parts.push('aria-readonly')
466+
if (el.getAttribute('aria-required') === 'true') parts.push('aria-required')
467+
if (tag === 'INPUT') {
468+
const input = el as HTMLInputElement
469+
if (input.type === 'checkbox' || input.type === 'radio') {
470+
parts.push(input.checked ? 'checked' : 'unchecked')
471+
}
472+
if (input.readOnly) parts.push('readonly')
473+
if (input.required) parts.push('required')
474+
} else if (tag === 'TEXTAREA') {
475+
const textarea = el as HTMLTextAreaElement
476+
if (textarea.readOnly) parts.push('readonly')
477+
if (textarea.required) parts.push('required')
478+
} else if (tag === 'SELECT' && (el as HTMLSelectElement).required) {
479+
parts.push('required')
480+
}
481+
for (const attribute of ['aria-checked', 'aria-expanded', 'aria-pressed', 'aria-selected']) {
482+
const value = el.getAttribute(attribute)
483+
if (value === 'true' || value === 'false' || value === 'mixed') {
484+
parts.push(`${attribute}=${value}`)
485+
}
486+
}
463487
const suffix = parts.length > 0 ? ` ${parts.join(' ')}` : ''
464488
const lineIndex = lines.length
465489
if (push(`${indent}- ${role} ${quote(name)} [ref=${id}]${suffix}`)) {

0 commit comments

Comments
 (0)