Files
kandev/apps/web/lib/desktop/contextual-close.test.ts
2026-07-15 23:19:00 +01:00

117 lines
3.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { closeDesktopContext, type ContextualDockApi } from "./contextual-close";
const FILE_EDITOR_COMPONENT = "file-editor";
const DIALOG_CONTENT_SLOT = "dialog-content";
function addOverlay(slot: string, zIndex = 50): HTMLElement {
const element = document.createElement("div");
element.dataset.slot = slot;
element.dataset.state = "open";
element.style.zIndex = String(zIndex);
document.body.append(element);
return element;
}
function makeDockApi(component: string) {
const close = vi.fn();
const api: ContextualDockApi = {
activePanel: { api: { component, close } },
};
return { api, close };
}
afterEach(() => {
document.body.replaceChildren();
});
describe("closeDesktopContext", () => {
it("dismisses only the topmost overlay before considering a document", () => {
const first = addOverlay(DIALOG_CONTENT_SLOT);
const top = addOverlay("popover-content", 60);
const firstEscape = vi.fn();
const topEscape = vi.fn();
first.addEventListener("keydown", firstEscape);
top.addEventListener("keydown", topEscape);
const dock = makeDockApi(FILE_EDITOR_COMPONENT);
expect(closeDesktopContext(document, dock.api)).toBe("overlay");
expect(topEscape).toHaveBeenCalledOnce();
expect(firstEscape).not.toHaveBeenCalled();
expect(dock.close).not.toHaveBeenCalled();
});
it("does not close an alert dialog, an underlying overlay, or a document", () => {
const underlying = addOverlay(DIALOG_CONTENT_SLOT);
const alert = addOverlay("alert-dialog-content", 60);
const underlyingEscape = vi.fn();
const alertEscape = vi.fn();
underlying.addEventListener("keydown", underlyingEscape);
alert.addEventListener("keydown", alertEscape);
const dock = makeDockApi(FILE_EDITOR_COMPONENT);
expect(closeDesktopContext(document, dock.api)).toBe("blocked");
expect(alertEscape).not.toHaveBeenCalled();
expect(underlyingEscape).not.toHaveBeenCalled();
expect(dock.close).not.toHaveBeenCalled();
});
it.each([FILE_EDITOR_COMPONENT, "diff-viewer", "commit-detail", "browser"])(
"closes the active %s document through its panel close behavior",
(component) => {
const dock = makeDockApi(component);
expect(closeDesktopContext(document, dock.api)).toBe("document");
expect(dock.close).toHaveBeenCalledOnce();
},
);
it.each(["chat", "terminal", "changes", "files", "plan", "vscode", "pr-detail"])(
"leaves the active %s panel open",
(component) => {
const dock = makeDockApi(component);
expect(closeDesktopContext(document, dock.api)).toBe("none");
expect(dock.close).not.toHaveBeenCalled();
},
);
it("ignores an overlay that is already closing", () => {
const overlay = addOverlay(DIALOG_CONTENT_SLOT);
overlay.dataset.state = "closed";
const dock = makeDockApi("commit-detail");
expect(closeDesktopContext(document, dock.api)).toBe("document");
expect(dock.close).toHaveBeenCalledOnce();
});
it("ignores open structural content that is not an overlay", () => {
addOverlay("collapsible-content", 100);
const dock = makeDockApi(FILE_EDITOR_COMPONENT);
expect(closeDesktopContext(document, dock.api)).toBe("document");
expect(dock.close).toHaveBeenCalledOnce();
});
it("does not dismiss an overlay while an editable field has focus", () => {
const overlay = addOverlay(DIALOG_CONTENT_SLOT);
const escape = vi.fn();
overlay.addEventListener("keydown", escape);
const input = document.createElement("input");
overlay.append(input);
input.focus();
const dock = makeDockApi(FILE_EDITOR_COMPONENT);
expect(closeDesktopContext(document, dock.api)).toBe("none");
expect(escape).not.toHaveBeenCalled();
expect(dock.close).not.toHaveBeenCalled();
});
it("does nothing without an overlay or active dock API", () => {
expect(closeDesktopContext(document, null)).toBe("none");
});
});