fix: snapshot -i auto-detects dropdown/popover interactive elements (#845)

* fix: snapshot -i auto-detects dropdown/popover interactive elements

- Auto-enable cursor-interactive scan (-C) when -i flag is used
- Add floating container detection (portals, popovers, dropdowns)
  - Detects position:fixed/absolute with high z-index
  - Recognizes data-floating-ui-portal, data-radix-* attributes
  - Recognizes role=listbox, role=menu containers
- Elements inside floating containers bypass the hasRole skip
  - Catches dropdown items missed by the accessibility tree
- Role=option/menuitem elements in floating containers captured
  even without cursor:pointer/onclick
- Tag floating container items with 'popover-child' reason
- Include role name in @c ref reasons when present
- Add dropdown.html test fixture
- Add dropdown/popover detection test suite (6 tests)
- Add test: -i alone includes cursor-interactive elements

Fixes: Bookface autocomplete, Radix UI combobox, React portals,
and similar dynamic dropdown patterns where ariaSnapshot() misses
the floating content.

* chore: bump version and changelog (v0.15.12.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update snapshot -i/-C flag descriptions to mention auto-enable behavior

* test: strengthen clickability test guard assertions

The @c ref clickability test previously used if-guards that would
silently pass when no Alice line was found in the snapshot output.
Both Claude and Codex adversarial review flagged this as a test that
could regress without CI noticing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: regenerate top-level SKILL.md with updated flag descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: root <root@localhost>
Co-authored-by: gstack <ship@gstack.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-05 22:57:45 -07:00
committed by GitHub
co-authored by Claude Opus 4.6 root gstack
parent 237ae2abbe
commit a94a64f821
8 changed files with 285 additions and 11 deletions
+69
View File
@@ -386,6 +386,75 @@ describe('Cursor-interactive', () => {
// And cursor-interactive section
expect(result).toContain('cursor-interactive');
});
test('snapshot -i alone also includes cursor-interactive elements', async () => {
await handleWriteCommand('goto', [baseUrl + '/cursor-interactive.html'], bm);
const result = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
// -i now auto-enables -C
expect(result).toContain('[button]');
expect(result).toContain('[link]');
expect(result).toContain('cursor-interactive');
expect(result).toContain('@c');
});
});
// ─── Dropdown/Popover Detection ─────────────────────────────────
describe('Dropdown/popover detection', () => {
test('snapshot -i auto-enables cursor scan and finds dropdown items', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const result = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
// Should find standard interactive elements
expect(result).toContain('[button]');
expect(result).toContain('[link]');
expect(result).toContain('[textbox]');
// Should also find cursor-interactive dropdown items
expect(result).toContain('cursor-interactive');
expect(result).toContain('@c');
expect(result).toContain('Alice Johnson');
expect(result).toContain('Bob Smith');
});
test('dropdown items in floating container are tagged as popover-child', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const result = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
expect(result).toContain('popover-child');
});
test('dropdown items with role="option" in portal are captured', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const result = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
// Dave Wilson has role="option" — should be captured even though it has a role
expect(result).toContain('Dave Wilson');
});
test('static text in dropdown without interactivity is NOT captured', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const result = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
// "No results? Try a different search." has no cursor:pointer, no onclick, no tabindex
expect(result).not.toContain('No results');
});
test('@c ref from dropdown is clickable', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const snap = await handleMetaCommand('snapshot', ['-i'], bm, shutdown);
// Find a @c ref for Alice
const aliceLine = snap.split('\n').find(l => l.includes('@c') && l.includes('Alice'));
expect(aliceLine).toBeTruthy();
const refMatch = aliceLine!.match(/@(c\d+)/);
expect(refMatch).toBeTruthy();
const result = await handleWriteCommand('click', [`@${refMatch![1]}`], bm);
expect(result).toContain('Clicked');
});
test('snapshot -C still works standalone without -i', async () => {
await handleWriteCommand('goto', [baseUrl + '/dropdown.html'], bm);
const result = await handleMetaCommand('snapshot', ['-C'], bm, shutdown);
expect(result).toContain('cursor-interactive');
expect(result).toContain('Alice Johnson');
// Without -i, should include non-interactive ARIA elements too
expect(result).toContain('[heading]');
});
});
// ─── Snapshot Error Paths ───────────────────────────────────────