* fix(app): move keyboard focus to target pane on keyboard navigation (#137) Add a pane focus registry so each panel type can expose its own focus callback. When keyboard shortcuts navigate between panes, the workspace screen calls the target panel's registered focus function, moving DOM focus to the correct input element. - Agent panels register a callback that focuses the message input - Terminal panels register a callback that triggers terminal focus - Panel-internal focus logic stays inside each panel type - No DOM selectors or panel knowledge in shared infrastructure * fix(app): simplify workspace pane focus handoff
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { focusWithRetries } from "./web-focus";
|
|
|
|
describe("focusWithRetries", () => {
|
|
let frameQueue: FrameRequestCallback[] = [];
|
|
let originalRequestAnimationFrame: typeof requestAnimationFrame | undefined;
|
|
|
|
beforeEach(() => {
|
|
frameQueue = [];
|
|
originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
|
|
frameQueue.push(callback);
|
|
return frameQueue.length;
|
|
}) as typeof requestAnimationFrame;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalRequestAnimationFrame) {
|
|
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
|
return;
|
|
}
|
|
|
|
delete (globalThis as { requestAnimationFrame?: typeof requestAnimationFrame })
|
|
.requestAnimationFrame;
|
|
});
|
|
|
|
function flushAnimationFrames(count: number): void {
|
|
for (let index = 0; index < count; index += 1) {
|
|
const callbacks = frameQueue;
|
|
frameQueue = [];
|
|
for (const callback of callbacks) {
|
|
callback(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
it("tries to focus immediately before waiting for animation frames", () => {
|
|
let focused = false;
|
|
const focus = vi.fn(() => {
|
|
focused = true;
|
|
});
|
|
const onSuccess = vi.fn();
|
|
|
|
focusWithRetries({
|
|
focus,
|
|
isFocused: () => focused,
|
|
onSuccess,
|
|
});
|
|
|
|
expect(focus).toHaveBeenCalledTimes(1);
|
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
expect(frameQueue).toHaveLength(0);
|
|
});
|
|
|
|
it("keeps retrying on later animation frames until focus succeeds", () => {
|
|
let focused = false;
|
|
let attempts = 0;
|
|
const focus = vi.fn(() => {
|
|
attempts += 1;
|
|
if (attempts >= 3) {
|
|
focused = true;
|
|
}
|
|
});
|
|
const onSuccess = vi.fn();
|
|
|
|
focusWithRetries({
|
|
focus,
|
|
isFocused: () => focused,
|
|
onSuccess,
|
|
});
|
|
|
|
expect(focus).toHaveBeenCalledTimes(1);
|
|
expect(onSuccess).not.toHaveBeenCalled();
|
|
|
|
flushAnimationFrames(2);
|
|
expect(focus).toHaveBeenCalledTimes(2);
|
|
expect(onSuccess).not.toHaveBeenCalled();
|
|
|
|
flushAnimationFrames(2);
|
|
expect(focus).toHaveBeenCalledTimes(3);
|
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("stops retrying after cancellation", () => {
|
|
const focus = vi.fn();
|
|
|
|
const cancel = focusWithRetries({
|
|
focus,
|
|
isFocused: () => false,
|
|
});
|
|
|
|
expect(focus).toHaveBeenCalledTimes(1);
|
|
|
|
cancel();
|
|
flushAnimationFrames(4);
|
|
|
|
expect(focus).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|