diff --git a/packages/platform-web/src/__tests__/agent-browser-snapshot-refless.test.ts b/packages/platform-web/src/__tests__/agent-browser-snapshot-refless.test.ts new file mode 100644 index 0000000000..3f427124cd --- /dev/null +++ b/packages/platform-web/src/__tests__/agent-browser-snapshot-refless.test.ts @@ -0,0 +1,308 @@ +import { describe, expect, test } from 'vitest'; + +import { normalizeAgentBrowserSnapshot } from '../agent-browser-snapshot.ts'; + +/** + * Payloads captured from agent-browser 0.27.1 against a real page (`snapshot` with no `--compact`, + * which is what the capture now asks for), so the projection is tested against the wording the + * backend actually produces rather than a hand-written approximation of it. + */ +const FORM_AFTER_SUBMIT = { + snapshot: [ + '- heading "Bench form" [level=1, ref=e1]', + '- form', + ' - LabelText', + ' - StaticText "Name"', + ' - textbox "Name" [ref=e2]: Zed', + ' - StaticText "Zed"', + ' - LabelText', + ' - StaticText "Email"', + ' - textbox "Email" [ref=e3]', + ' - checkbox "I agree" [checked=false, ref=e4]', + ' - combobox "Color" [expanded=false, ref=e5]: Red', + ' - MenuListPopup', + ' - option "Red" [selected, ref=e7]', + ' - option "Blue" [ref=e8]', + ' - button "Submit" [ref=e6]', + '- status', + ' - StaticText "Hello Zed"', + '- list', + ].join('\n'), + refs: { + e1: { name: 'Bench form', role: 'heading' }, + e2: { name: 'Name', role: 'textbox' }, + e3: { name: 'Email', role: 'textbox' }, + e4: { name: 'I agree', role: 'checkbox' }, + e5: { name: 'Color', role: 'combobox' }, + e6: { name: 'Submit', role: 'button' }, + e7: { name: 'Red', role: 'option' }, + e8: { name: 'Blue', role: 'option' }, + }, +}; + +/** The same page before the submit announced anything: the `status` region is there and silent. */ +const FORM_BEFORE_SUBMIT = { + snapshot: [ + '- heading "Bench form" [level=1, ref=e1]', + '- form', + ' - LabelText', + ' - StaticText "Name"', + ' - textbox "Name" [ref=e2]', + ' - button "Submit" [ref=e6]', + '- status', + '- list', + ].join('\n'), + refs: { + e1: { name: 'Bench form', role: 'heading' }, + e2: { name: 'Name', role: 'textbox' }, + e6: { name: 'Submit', role: 'button' }, + }, +}; + +const ROLE_VOCABULARY = { + snapshot: [ + '- heading "Roles probe" [level=1, ref=e1]', + '- paragraph', + ' - StaticText "Paragraph text"', + '- StaticText "Span text"', + '- status', + ' - StaticText "Status text"', + '- alert', + ' - StaticText "Alert text"', + '- note', + ' - StaticText "Note text"', + '- LabelText', + ' - StaticText "Label text"', + '- textbox "Label text" [ref=e2]', + '- button "Go" [ref=e3]', + ].join('\n'), + refs: { + e1: { name: 'Roles probe', role: 'heading' }, + e2: { name: 'Label text', role: 'textbox' }, + e3: { name: 'Go', role: 'button' }, + }, +}; + +/** One list item per row, each carrying the bullet the renderer draws. */ +const LIST_WITH_MARKERS = { + snapshot: [ + '- heading "Items" [level=1, ref=e1]', + '- list', + ' - listitem', + ' - ListMarker "• "', + ' - StaticText "First"', + ' - listitem', + ' - ListMarker "• "', + ' - StaticText "Second"', + ].join('\n'), + refs: { e1: { name: 'Items', role: 'heading' } }, +}; + +function refLessNodes(result: Awaited>) { + return result.nodes.filter((node) => node.ref === undefined); +} + +describe('normalizeAgentBrowserSnapshot ref-less content', () => { + test('publishes the text of a region the backend issued no ref for', async () => { + const result = await normalizeAgentBrowserSnapshot(FORM_AFTER_SUBMIT); + const announcement = refLessNodes(result).filter((node) => node.value === 'Hello Zed'); + + // The announcement is the reason the capture is taken, and the backend has no ref for it: before, + // it reached no reader at all, because the backend's `--compact` removed the region with it. + expect(announcement.length).toBe(1); + expect(announcement[0]?.type).toBe('StaticText'); + expect(announcement[0]?.ref).toBeUndefined(); + expect(announcement[0]?.label).toBeUndefined(); + }); + + test('leaves the ref each control carries untouched', async () => { + const result = await normalizeAgentBrowserSnapshot(FORM_AFTER_SUBMIT); + + expect( + result.nodes + .filter((node) => node.ref !== undefined) + .map((node) => `${node.type} ${node.ref} ${node.label}`), + ).toEqual([ + 'heading e1 Bench form', + 'textbox e2 Name', + 'textbox e3 Email', + 'checkbox e4 I agree', + 'combobox e5 Color', + 'option e7 Red', + 'option e8 Blue', + 'button e6 Submit', + ]); + }); + + test('subsumes a label element text under the control that carries it as its name', async () => { + // The backend prints the same visible string twice: as the `