fix(maestro): mobile drawer access, focus preservation, stale rAF focus (#140)
Three more fixes from acceptance review: - structured.ts's header omitted the .sessions-btn attach.ts's terminal header carries. The sidebar is off-canvas on mobile, so a structured session had no way to reach it — add the same button and requestToggleDrawer wiring. - ConnectionPool.reconcileViews always remounted a repaired connection with focus:false. If the mistyped pane was the one actually focused, disposing it dropped DOM focus while `focusedId` still named it. Pass `focus: this.focusedId === id` instead — `focusedId` only ever names a real focus (multiview.ts skips setFocus while previewing), so a preview or a merely-visible split neighbour still comes back unfocused. - onMount's deferred requestAnimationFrame focus had no way to tell it had been superseded by an unmount (or a remount elsewhere) before it fired, so a parked pane could steal focus a frame later. A token bumped on every mount/unmount lets the callback recognize it's stale and no-op. Regression tests: 7 new (structured.test.ts, connections.test.ts). 495 tests total.
This commit is contained in:
@@ -9,6 +9,7 @@ import type { Session } from "./types.js";
|
||||
// the pool reaches the focused connection with them.
|
||||
const sent: { id: string; data: string }[] = [];
|
||||
const disposed: string[] = [];
|
||||
const mounts: { id: string; focus: boolean | undefined }[] = [];
|
||||
function fakeConn(kind: string, id: string) {
|
||||
const el = document.createElement("div");
|
||||
el.dataset.kind = kind;
|
||||
@@ -17,7 +18,9 @@ function fakeConn(kind: string, id: string) {
|
||||
sessionId: id,
|
||||
kind,
|
||||
el,
|
||||
onMount() {},
|
||||
onMount(opts?: { focus?: boolean }) {
|
||||
mounts.push({ id, focus: opts?.focus });
|
||||
},
|
||||
onUnmount() {},
|
||||
focusTerminal() {},
|
||||
sendInput(data: string) {
|
||||
@@ -39,6 +42,7 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
sent.length = 0;
|
||||
disposed.length = 0;
|
||||
mounts.length = 0;
|
||||
});
|
||||
|
||||
/** Minimal fixture for the `CatalogState` a `CatalogStore` broadcasts. */
|
||||
@@ -256,4 +260,52 @@ describe("ConnectionPool: view resolution races", () => {
|
||||
|
||||
expect(disposed).toEqual([]);
|
||||
});
|
||||
|
||||
it("re-focuses the fresh connection when the replaced pane was the focused session", () => {
|
||||
mockFetch();
|
||||
const { store, socket } = fakeStore();
|
||||
const pool = new ConnectionPool({ store });
|
||||
const pane = document.createElement("div");
|
||||
pool.mount(A, pane);
|
||||
pool.setFocus(A);
|
||||
mounts.length = 0;
|
||||
|
||||
socket.push([session(A, "Structured")]);
|
||||
|
||||
expect(disposed).toEqual(["attach:" + A]);
|
||||
expect(mounts).toEqual([{ id: A, focus: true }]);
|
||||
});
|
||||
|
||||
it("does not steal focus for a merely-visible pane that was never actually focused", () => {
|
||||
mockFetch();
|
||||
const { store, socket } = fakeStore();
|
||||
const pool = new ConnectionPool({ store });
|
||||
const paneA = document.createElement("div");
|
||||
const paneB = document.createElement("div");
|
||||
pool.mount(A, paneA);
|
||||
pool.mount(B, paneB);
|
||||
pool.setFocus(B); // B is the real keyboard focus; A is only visible
|
||||
mounts.length = 0;
|
||||
|
||||
socket.push([session(A, "Structured")]);
|
||||
|
||||
expect(disposed).toEqual(["attach:" + A]);
|
||||
expect(mounts).toEqual([{ id: A, focus: false }]);
|
||||
});
|
||||
|
||||
it("does not focus a parked (unmounted) connection it repairs", () => {
|
||||
mockFetch();
|
||||
const { store, socket } = fakeStore();
|
||||
const pool = new ConnectionPool({ store });
|
||||
const pane = document.createElement("div");
|
||||
pool.mount(A, pane);
|
||||
pool.setFocus(A);
|
||||
pool.unmount(A); // parked — no longer visible, `focusedId` still names it
|
||||
mounts.length = 0;
|
||||
|
||||
socket.push([session(A, "Structured")]);
|
||||
|
||||
expect(disposed).toEqual(["attach:" + A]);
|
||||
expect(mounts).toEqual([]); // parked repair calls onUnmount, never onMount
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user