import { describe, expect, it, vi } from "vitest"; import { createComposerFocusMemory, createChatScrollMemory, } from "./workspace-interaction"; import { changeAIMode, parseAIMode } from "./workspace-mode"; import { submitComposer } from "./composer-events"; import { agentBuilderStub } from "./agent-builder"; describe("mode navigation events", () => { it("pushes canonical URLs, preserves hash/query, and reads history navigation", () => { const history = ["https://example.test/acme/ai?filter=mine#library"]; let index = 0; const navigate = (mode: "chat" | "agents") => { const url = new URL(history[index]); changeAIMode(url, mode, (href) => { history.splice(++index); history.push(new URL(href, url).href); }); }; navigate("chat"); expect(history).toHaveLength(1); navigate("agents"); expect(history[index]).toBe( "https://example.test/acme/ai?filter=mine&tab=agents#library", ); navigate("agents"); expect(history).toHaveLength(2); navigate("chat"); expect(history[index]).toBe( "https://example.test/acme/ai?filter=mine&tab=chat#library", ); index--; expect(parseAIMode(new URL(history[index]).searchParams.get("tab"))).toBe( "agents", ); index++; expect(parseAIMode(new URL(history[index]).searchParams.get("tab"))).toBe( "chat", ); }); }); describe("composer focus memory", () => { it("restores the previously focused composer on history return, not initial entry", () => { const memory = createComposerFocusMemory(); expect(memory.activate("chat", false)).toBe(false); memory.record("chat", true); expect(memory.activate("agents", false)).toBe(false); expect(memory.activate("chat", false)).toBe(true); }); it("never steals keyboard tab focus and forgets focus moved elsewhere in a panel", () => { const memory = createComposerFocusMemory(); memory.record("chat", true); expect(memory.activate("agents", true)).toBe(false); expect(memory.activate("chat", true)).toBe(false); memory.record("chat", false); memory.activate("agents", false); expect(memory.activate("chat", false)).toBe(false); }); }); describe("Chat scroll memory", () => { it("follows streamed content only when already near the bottom", () => { const memory = createChatScrollMemory(); const container = { scrollTop: 0, scrollHeight: 1000, clientHeight: 200 }; memory.update(container, true); container.scrollTop = 400; memory.record(container, true); container.scrollHeight = 1200; memory.update(container, true); expect(container.scrollTop).toBe(400); container.scrollTop = 980; memory.record(container, true); container.scrollHeight = 1400; memory.update(container, true); expect(container.scrollTop).toBe(1200); }); it("ignores hidden scroll events and restores saved position before following new updates", () => { const memory = createChatScrollMemory(); const container = { scrollTop: 0, scrollHeight: 1000, clientHeight: 200 }; memory.update(container, true); container.scrollTop = 300; memory.record(container, true); memory.update(container, false); container.scrollTop = 0; memory.record(container, false); container.scrollHeight = 1500; memory.update(container, false); memory.update(container, true); expect(container.scrollTop).toBe(300); }); }); describe("composer submission events", () => { it("submits only through the supplied mode adapter and restores focus", async () => { const chat = vi.fn(); const focus = vi.fn(); const fetchSpy = vi.spyOn(globalThis, "fetch"); const agents = vi.fn(() => agentBuilderStub({ workspaceSlug: "acme", prompt: "Triage tasks" }), ); const event = { preventDefault: vi.fn() }; try { submitComposer(event, "Triage tasks", false, agents, focus); expect((await agents.mock.results[0].value).status).toBe("unavailable"); expect(chat).not.toHaveBeenCalled(); expect(fetchSpy).not.toHaveBeenCalled(); expect(focus).toHaveBeenCalledOnce(); expect(event.preventDefault).toHaveBeenCalledOnce(); } finally { fetchSpy.mockRestore(); } }); it.each([ [" ", false], ["Draft", true], ])("blocks blank/busy submissions", (input, busy) => { const send = vi.fn(); const focus = vi.fn(); submitComposer({ preventDefault: vi.fn() }, input, busy, send, focus); expect(send).not.toHaveBeenCalled(); expect(focus).not.toHaveBeenCalled(); }); });