Submatter/apps/web/components/ai/workspace-interaction.test.ts

126 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

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();
});
});