Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 5m3s
Build App (Preview) / build-linux (pull_request) Successful in 5m21s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Sending already switched to the target terminal's tab, which looks like it should be enough: `TerminalView` focuses xterm whenever a terminal becomes active. But that effect keys off `active`, so it only fires on a *change* — and the dock's ordinary case is sending to the terminal already on screen. `setActiveTabKey` writes the key that is already set, nothing changes, no effect re-runs, and focus stays on the Send button. The note is sitting in the prompt and the user still has to click the terminal before pressing Enter. So the send now asks for focus explicitly, through a one-shot request in the store that `TerminalView` consumes and clears — the shape `pendingHomeTab` already uses. Clearing is not tidiness: hold the id and the second send to the same terminal writes a value that is already there, which is precisely the no-op this exists to fix. Focus is requested only on success. A failed send toasts and leaves the user where they are, because there is nothing in the prompt to press Enter on. The three `TerminalView` tests give focus away after mounting before making any assertion, so what they observe is the request landing and never the focus that `active` already grants on mount — which would pass with the feature absent. 752 tests pass, 62 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { useAppState, homeTabKey, terminalTabKey } from "./appState";
|
|
|
|
const A = homeTabKey("a");
|
|
const B = terminalTabKey("b");
|
|
const C = terminalTabKey("c");
|
|
|
|
function seed(tabOrder: string[], activeTabKey: string | null = null) {
|
|
useAppState.setState({
|
|
tabOrder,
|
|
activeTabKey,
|
|
activeSessionId: null,
|
|
selectedProjectId: null,
|
|
});
|
|
}
|
|
|
|
const order = () => useAppState.getState().tabOrder;
|
|
|
|
describe("tab reordering", () => {
|
|
beforeEach(() => seed([A, B, C]));
|
|
|
|
it("moves a tab to an earlier slot", () => {
|
|
useAppState.getState().moveTab(C, 0);
|
|
expect(order()).toEqual([C, A, B]);
|
|
});
|
|
|
|
it("moves a tab to a later slot", () => {
|
|
useAppState.getState().moveTab(A, 2);
|
|
expect(order()).toEqual([B, C, A]);
|
|
});
|
|
|
|
it("clamps a destination past the ends rather than dropping the tab", () => {
|
|
useAppState.getState().moveTab(A, 99);
|
|
expect(order()).toEqual([B, C, A]);
|
|
useAppState.getState().moveTab(A, -5);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("ignores a tab that isn't in the strip", () => {
|
|
useAppState.getState().moveTab("term:gone", 0);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("does not change what's active — dragging a tab is not selecting it", () => {
|
|
seed([A, B, C], B);
|
|
useAppState.getState().moveTab(C, 0);
|
|
const state = useAppState.getState();
|
|
expect(state.tabOrder).toEqual([C, A, B]);
|
|
expect(state.activeTabKey).toBe(B);
|
|
});
|
|
|
|
it("nudges the active tab with the keyboard, in both directions", () => {
|
|
seed([A, B, C], B);
|
|
useAppState.getState().moveActiveTab(-1);
|
|
expect(order()).toEqual([B, A, C]);
|
|
useAppState.getState().moveActiveTab(1);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("stops the active tab at the ends instead of wrapping it around", () => {
|
|
seed([A, B, C], A);
|
|
useAppState.getState().moveActiveTab(-1);
|
|
// A held-down key must not teleport the tab to the far end.
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("does nothing when no tab is active", () => {
|
|
seed([A, B, C], null);
|
|
useAppState.getState().moveActiveTab(1);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("keeps Ctrl+1..9 addressing the strip as reordered", () => {
|
|
seed([A, B, C], A);
|
|
useAppState.getState().moveTab(C, 0);
|
|
useAppState.getState().focusTabIndex(0);
|
|
expect(useAppState.getState().activeTabKey).toBe(C);
|
|
});
|
|
});
|
|
|
|
describe("toasts", () => {
|
|
beforeEach(() => useAppState.setState({ toasts: [] }));
|
|
|
|
const toasts = () => useAppState.getState().toasts;
|
|
|
|
it("stacks unkeyed toasts, which are one-off reports", () => {
|
|
const { pushToast } = useAppState.getState();
|
|
pushToast({ kind: "error", message: "one" });
|
|
pushToast({ kind: "error", message: "two" });
|
|
expect(toasts().map((t) => t.message)).toEqual(["one", "two"]);
|
|
});
|
|
|
|
it("lets a keyed toast supersede the one already on screen", () => {
|
|
// A recurring notice — "File drop ignored" is the one that exists — must
|
|
// not build a wall of identical cards when the user tries three times.
|
|
const { pushToast } = useAppState.getState();
|
|
const first = pushToast({ kind: "info", message: "ignored", dedupeKey: "drop-blocked" });
|
|
pushToast({ kind: "error", message: "unrelated" });
|
|
const second = pushToast({ kind: "info", message: "ignored", dedupeKey: "drop-blocked" });
|
|
|
|
expect(toasts().map((t) => t.message)).toEqual(["unrelated", "ignored"]);
|
|
// A fresh id, so the card re-mounts and its dismissal timer restarts
|
|
// rather than the replacement inheriting the first one's remaining time.
|
|
expect(second).not.toBe(first);
|
|
expect(toasts().some((t) => t.id === first)).toBe(false);
|
|
});
|
|
|
|
it("keeps toasts with different keys apart", () => {
|
|
const { pushToast } = useAppState.getState();
|
|
pushToast({ kind: "info", message: "a", dedupeKey: "x" });
|
|
pushToast({ kind: "info", message: "b", dedupeKey: "y" });
|
|
expect(toasts()).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("terminal focus requests", () => {
|
|
beforeEach(() => useAppState.setState({ pendingTerminalFocus: null }));
|
|
|
|
const pending = () => useAppState.getState().pendingTerminalFocus;
|
|
|
|
it("names the session that should take focus", () => {
|
|
useAppState.getState().requestTerminalFocus("s1");
|
|
expect(pending()).toBe("s1");
|
|
});
|
|
|
|
// Consumed once, exactly like `pendingHomeTab`. Without the clear, the
|
|
// second send to a terminal already holding the request would set the same
|
|
// value, no state would change, and no effect would re-run — which is the
|
|
// failure this whole mechanism exists to fix.
|
|
it("is cleared once consumed, so the same terminal can be asked again", () => {
|
|
useAppState.getState().requestTerminalFocus("s1");
|
|
useAppState.getState().clearPendingTerminalFocus();
|
|
expect(pending()).toBeNull();
|
|
|
|
useAppState.getState().requestTerminalFocus("s1");
|
|
expect(pending()).toBe("s1");
|
|
});
|
|
});
|