feat: add cursor-interactive element detection in snapshots (#374)

* fix: only warn about ignored flags when explicitly passed via CLI

The warning about launch-time options being ignored (when daemon is
already running) was incorrectly shown when options were set via
environment variables like AGENT_BROWSER_EXECUTABLE_PATH, even when
no CLI flag was passed.

Now the warning only appears when flags are explicitly passed on the
command line, not when values come solely from environment variables.

Fixes #372

* feat: add cursor-interactive element detection in snapshots

Add -C/--cursor flag to snapshot command that detects clickable elements
that don't have proper ARIA roles but are interactive based on:
- cursor: pointer CSS style
- onclick attribute/handler
- tabindex attribute

This helps with modern web apps that use custom divs/spans as buttons.

Fixes #366

* fix: add cursor option to getSnapshot type signature
This commit is contained in:
Chris Tate
2026-02-04 23:44:58 -06:00
committed by GitHub
parent d34ce8c2d0
commit 74be667c80
9 changed files with 291 additions and 0 deletions
+80
View File
@@ -304,6 +304,86 @@ describe('BrowserManager', () => {
// Compact should be equal or shorter
expect(compactSnapshot.length).toBeLessThanOrEqual(fullSnapshot.length);
});
it('should not capture cursor-interactive elements without cursor flag', async () => {
const page = browser.getPage();
await page.setContent(`
<html>
<body>
<button id="standard-btn">Standard Button</button>
<div id="clickable-div" style="cursor: pointer;" onclick="void(0)">Clickable Div</div>
</body>
</html>
`);
const { tree, refs } = await browser.getSnapshot({ interactive: true });
// Standard button should be captured via ARIA
expect(tree).toContain('button "Standard Button"');
// Cursor-interactive elements should NOT be captured without cursor flag
expect(tree).not.toContain('Cursor-interactive elements');
expect(tree).not.toContain('clickable "Clickable Div"');
// Should only have refs for ARIA interactive elements
const refValues = Object.values(refs);
expect(refValues.some((r) => r.role === 'button')).toBe(true);
expect(refValues.some((r) => r.role === 'clickable')).toBe(false);
});
it('should capture cursor-interactive elements with cursor flag', async () => {
const page = browser.getPage();
await page.setContent(`
<html>
<body>
<button id="standard-btn">Standard Button</button>
<div id="clickable-div" style="cursor: pointer;" onclick="void(0)">Clickable Div</div>
<span onclick="void(0)">Onclick Span</span>
</body>
</html>
`);
const { tree, refs } = await browser.getSnapshot({ interactive: true, cursor: true });
// Standard button should be captured via ARIA
expect(tree).toContain('button "Standard Button"');
// Cursor-interactive elements should be captured with cursor flag
expect(tree).toContain('Cursor-interactive elements');
expect(tree).toContain('clickable "Clickable Div"');
expect(tree).toContain('clickable "Onclick Span"');
// Should have refs for all interactive elements
const refValues = Object.values(refs);
expect(refValues.some((r) => r.role === 'button')).toBe(true);
expect(refValues.some((r) => r.role === 'clickable')).toBe(true);
});
it('should click cursor-interactive elements via refs', async () => {
const page = browser.getPage();
await page.setContent(`
<html>
<body>
<div id="clickable" style="cursor: pointer;" onclick="document.getElementById('result').textContent = 'clicked'">Click Me</div>
<div id="result">not clicked</div>
</body>
</html>
`);
const { refs } = await browser.getSnapshot({ cursor: true });
// Find the ref for the clickable element
const clickableRef = Object.keys(refs).find((k) => refs[k].name === 'Click Me');
expect(clickableRef).toBeDefined();
// Click using the ref
const locator = browser.getLocator(`@${clickableRef}`);
await locator.click();
// Verify click worked
const result = await page.locator('#result').textContent();
expect(result).toBe('clicked');
});
});
describe('locator resolution', () => {