Ran the full 65-test port suite on WebKit + Firefox (UIX_CROSS_BROWSER=1), not just chromium. That caught one real robustness gap: Firefox stores scrollTop as a subpixel value (~59.93 for a 60px scroll), so ScrollState's exact-equality assertion is relaxed to toBeCloseTo — the port faithfully passes the browser's own scrollTop through. New coverage: - tabbable.ts WRAPPER (getTabbableCandidates / getTabbableEdges / getTabbableFrom / getTabbableFromFocusable) — the surface soma's focus-scope actually consumes, previously untested. - resource: AbortController cancellation + onCleanup on rapid source change (aborted request must not clobber state with an AbortError). - FiniteStateMachine: from/to/event/args lifecycle metadata, send-arg forwarding, and no-action-for-event (no transition). All green on chromium + webkit + firefox. (Repo check baseline is ~214 due to the unrelated in-progress Knob component; 0 errors in these files.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>menubar-v4-safe
parent
d4f9474f06
commit
2bd09ac6bb
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Deep tests for the tabbable WRAPPER (`tabbable.ts`) — the surface soma's
|
||||
* focus-scope actually consumes (getTabbableCandidates / getTabbableEdges /
|
||||
* getTabbableFrom). Browser project for real layout + the findVisible filter.
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getTabbableCandidates,
|
||||
getTabbableEdges,
|
||||
getTabbableFrom,
|
||||
getTabbableFromFocusable
|
||||
} from './tabbable';
|
||||
|
||||
let container: HTMLElement;
|
||||
|
||||
function mount(html: string): HTMLElement {
|
||||
container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
afterEach(() => container?.remove());
|
||||
|
||||
const ids = (els: Element[]) => els.map((e) => e.id);
|
||||
|
||||
describe('getTabbableCandidates', () => {
|
||||
it('collects the container tabbables and excludes tabindex -1', () => {
|
||||
mount(`
|
||||
<button id="a">a</button>
|
||||
<input id="b" />
|
||||
<button id="c" tabindex="-1">c</button>
|
||||
`);
|
||||
expect(ids(getTabbableCandidates(container))).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTabbableEdges', () => {
|
||||
it('returns the first and last visible tabbable', () => {
|
||||
mount(`
|
||||
<button id="a">a</button>
|
||||
<button id="b">b</button>
|
||||
<button id="c">c</button>
|
||||
`);
|
||||
const [first, last] = getTabbableEdges(container);
|
||||
expect(first?.id).toBe('a');
|
||||
expect(last?.id).toBe('c');
|
||||
});
|
||||
|
||||
it('skips a hidden edge element', () => {
|
||||
mount(`
|
||||
<button id="hidden" style="display:none">x</button>
|
||||
<button id="a">a</button>
|
||||
<button id="b">b</button>
|
||||
`);
|
||||
const [first] = getTabbableEdges(container);
|
||||
expect(first?.id).toBe('a');
|
||||
});
|
||||
|
||||
it('returns [undefined, undefined] when there are no tabbables', () => {
|
||||
mount(`<span>nothing focusable</span>`);
|
||||
expect(getTabbableEdges(container)).toEqual([undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTabbableFrom', () => {
|
||||
it('walks to the next / previous tabbable in document order', () => {
|
||||
mount(`
|
||||
<button id="a">a</button>
|
||||
<button id="b">b</button>
|
||||
<button id="c">c</button>
|
||||
`);
|
||||
const b = container.querySelector<HTMLElement>('#b')!;
|
||||
expect(getTabbableFrom(b, 'next').id).toBe('c');
|
||||
expect(getTabbableFrom(b, 'prev').id).toBe('a');
|
||||
});
|
||||
|
||||
it('falls back to focusable traversal for a non-tabbable start node', () => {
|
||||
mount(`
|
||||
<button id="a">a</button>
|
||||
<div id="start" tabindex="-1">start</div>
|
||||
<button id="b">b</button>
|
||||
`);
|
||||
const start = container.querySelector<HTMLElement>('#start')!;
|
||||
// start is focusable but not tabbable → next tabbable after it is b.
|
||||
expect(getTabbableFrom(start, 'next').id).toBe('b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTabbableFromFocusable', () => {
|
||||
it('finds the next tabbable among focusable siblings', () => {
|
||||
mount(`
|
||||
<div id="start" tabindex="-1">start</div>
|
||||
<button id="next">next</button>
|
||||
`);
|
||||
const start = container.querySelector<HTMLElement>('#start')!;
|
||||
expect(getTabbableFromFocusable(start, 'next').id).toBe('next');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue