Closes #249: layoutStore.ts wraps the existing layout.ts tree model in Zustand, persisting to localStorage on a 400ms debounce and adding boot reconciliation to prune leaves for sessions no longer live. MultiView/TabBar/PaneContainer/PaneTitle render tabs, nested splits with draggable dividers, drag-and-drop tab reordering, and dispatch each pane to TerminalPane or StructuredPane by session view. Preview mode (#251) and sidebar drag-to-dock stay out of scope here.
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { createLayoutStore } from "../../stores/layoutStore.js";
|
|
import { TabBar } from "./TabBar.js";
|
|
|
|
afterEach(cleanup);
|
|
|
|
function store() {
|
|
return createLayoutStore({
|
|
load: () => null,
|
|
save: vi.fn(),
|
|
scheduler: { setTimer: () => 1, clearTimer: () => {} },
|
|
});
|
|
}
|
|
|
|
function dataTransfer() {
|
|
const data = new Map<string, string>();
|
|
return {
|
|
setData: (type: string, value: string) => data.set(type, value),
|
|
getData: (type: string) => data.get(type) ?? "",
|
|
effectAllowed: "",
|
|
};
|
|
}
|
|
|
|
describe("TabBar", () => {
|
|
it("renders one tab per layout tab, marking the active one", () => {
|
|
const s = store();
|
|
s.getState().addTab();
|
|
render(<TabBar store={s} />);
|
|
const tabs = screen.getAllByRole("tab");
|
|
expect(tabs).toHaveLength(2);
|
|
expect(tabs[1].getAttribute("aria-selected")).toBe("true");
|
|
expect(tabs[0].getAttribute("aria-selected")).toBe("false");
|
|
});
|
|
|
|
it("shows the open-session count next to a tab's title", () => {
|
|
const s = store();
|
|
s.getState().openSession("s1");
|
|
render(<TabBar store={s} />);
|
|
expect(screen.getByText(/· 1/)).toBeTruthy();
|
|
});
|
|
|
|
it("clicking a tab selects it", () => {
|
|
const s = store();
|
|
s.getState().addTab();
|
|
const [first] = s.getState().layout.tabs;
|
|
render(<TabBar store={s} />);
|
|
fireEvent.click(screen.getAllByRole("tab")[0]);
|
|
expect(s.getState().layout.activeTabId).toBe(first.id);
|
|
});
|
|
|
|
it("the + button adds a tab", () => {
|
|
const s = store();
|
|
render(<TabBar store={s} />);
|
|
fireEvent.click(screen.getByTitle("New tab"));
|
|
expect(s.getState().layout.tabs).toHaveLength(2);
|
|
});
|
|
|
|
it("hides the close control on the last remaining tab", () => {
|
|
const s = store();
|
|
render(<TabBar store={s} />);
|
|
expect(screen.queryByTitle("Close tab")).toBeNull();
|
|
});
|
|
|
|
it("closing a tab removes it", () => {
|
|
const s = store();
|
|
s.getState().addTab();
|
|
render(<TabBar store={s} />);
|
|
fireEvent.click(screen.getAllByTitle("Close tab")[0]);
|
|
expect(s.getState().layout.tabs).toHaveLength(1);
|
|
});
|
|
|
|
it("dragging a tab onto another reorders them", () => {
|
|
const s = store();
|
|
s.getState().addTab();
|
|
s.getState().addTab();
|
|
const [a, b, c] = s.getState().layout.tabs;
|
|
render(<TabBar store={s} />);
|
|
const tabs = screen.getAllByRole("tab");
|
|
const dt = dataTransfer();
|
|
fireEvent.dragStart(tabs[0], { dataTransfer: dt });
|
|
fireEvent.dragOver(tabs[2], { dataTransfer: dt });
|
|
fireEvent.drop(tabs[2], { dataTransfer: dt });
|
|
expect(s.getState().layout.tabs.map((t) => t.id)).toEqual([b.id, c.id, a.id]);
|
|
});
|
|
});
|