mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 23:55:23 +00:00
feat(split-screen): redesign split screen with free-form panes and pane picker
This commit is contained in:
1 parent
5f3372f9f6
commit
81e7aa6274
46 files changed
+5027
-2537
No files matched your search
@@ -1,9 +1,14 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render } from "@testing-library/react";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Tab } from "@/types/ui-types";
|
||||
import type { SplitSummary } from "@/shell/split/split-tabs";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: Record<string, unknown>) =>
|
||||
opts ? `${key}:${JSON.stringify(opts)}` : key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/electron", () => ({ isElectron: () => true }));
|
||||
@@ -17,52 +22,33 @@ const tabs = [
|
||||
{ id: "terminal-1", type: "terminal", label: "web-01" },
|
||||
] as Tab[];
|
||||
|
||||
function renderTabBar(activeTabId: string, onSetActiveTab = vi.fn()) {
|
||||
return render(
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
splitMode="none"
|
||||
paneTabIds={[]}
|
||||
focusedPaneIndex={null}
|
||||
onSetActiveTab={onSetActiveTab}
|
||||
onCloseTab={() => {}}
|
||||
onRefreshTab={() => {}}
|
||||
onReorderTabs={() => {}}
|
||||
onSplitTab={() => {}}
|
||||
onAddToSplit={() => {}}
|
||||
onRemoveFromSplit={() => {}}
|
||||
isAppFullscreen={false}
|
||||
onToggleAppFullscreen={() => {}}
|
||||
/>,
|
||||
);
|
||||
function props(
|
||||
overrides: Partial<ComponentProps<typeof TabBar>> = {},
|
||||
): ComponentProps<typeof TabBar> {
|
||||
return {
|
||||
tabs,
|
||||
activeTabId: "dashboard",
|
||||
splits: [],
|
||||
activeSplitFull: false,
|
||||
onSetActiveTab: () => {},
|
||||
onCloseTab: () => {},
|
||||
onRefreshTab: () => {},
|
||||
onReorderTabs: () => {},
|
||||
onSplitAction: () => {},
|
||||
isAppFullscreen: false,
|
||||
onToggleAppFullscreen: () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("TabBar workspace continuity", () => {
|
||||
it("keeps one shared indicator on the active workspace", () => {
|
||||
const { container, rerender } = renderTabBar("dashboard");
|
||||
const { container, rerender } = render(<TabBar {...props()} />);
|
||||
expect(
|
||||
container.querySelector('[data-tab-indicator="dashboard"]'),
|
||||
).toBeTruthy();
|
||||
|
||||
rerender(
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId="terminal-1"
|
||||
splitMode="none"
|
||||
paneTabIds={[]}
|
||||
focusedPaneIndex={null}
|
||||
onSetActiveTab={() => {}}
|
||||
onCloseTab={() => {}}
|
||||
onRefreshTab={() => {}}
|
||||
onReorderTabs={() => {}}
|
||||
onSplitTab={() => {}}
|
||||
onAddToSplit={() => {}}
|
||||
onRemoveFromSplit={() => {}}
|
||||
isAppFullscreen={false}
|
||||
onToggleAppFullscreen={() => {}}
|
||||
/>,
|
||||
);
|
||||
rerender(<TabBar {...props({ activeTabId: "terminal-1" })} />);
|
||||
|
||||
expect(container.querySelectorAll("[data-tab-indicator]")).toHaveLength(1);
|
||||
expect(
|
||||
@@ -72,10 +58,104 @@ describe("TabBar workspace continuity", () => {
|
||||
|
||||
it("activates a workspace immediately when its tab is clicked", () => {
|
||||
const onSetActiveTab = vi.fn();
|
||||
const { getByText } = renderTabBar("dashboard", onSetActiveTab);
|
||||
const { getByText } = render(<TabBar {...props({ onSetActiveTab })} />);
|
||||
|
||||
fireEvent.click(getByText("web-01"));
|
||||
|
||||
expect(onSetActiveTab).toHaveBeenCalledWith("terminal-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TabBar split menu", () => {
|
||||
const split: SplitSummary = {
|
||||
id: "split-1",
|
||||
label: "Split 1",
|
||||
full: false,
|
||||
panes: [
|
||||
{ id: "p1", index: 1, tabId: "terminal-9", label: "db-01" },
|
||||
{ id: "p2", index: 2, tabId: null, label: null },
|
||||
],
|
||||
};
|
||||
|
||||
function openMenu(overrides: Partial<ComponentProps<typeof TabBar>> = {}) {
|
||||
const onSplitAction = vi.fn();
|
||||
const view = render(<TabBar {...props({ onSplitAction, ...overrides })} />);
|
||||
fireEvent.contextMenu(view.getByText("web-01"));
|
||||
return { ...view, onSplitAction };
|
||||
}
|
||||
|
||||
it("splits the clicked tab right or down", () => {
|
||||
const { getByText, onSplitAction } = openMenu();
|
||||
fireEvent.click(getByText("splitScreen.splitDown"));
|
||||
expect(onSplitAction).toHaveBeenCalledWith({
|
||||
kind: "split",
|
||||
tabId: "terminal-1",
|
||||
edge: "bottom",
|
||||
});
|
||||
});
|
||||
|
||||
it("lists every pane of every split, so the target is always clear", () => {
|
||||
const { getByText, onSplitAction } = openMenu({ splits: [split] });
|
||||
|
||||
expect(
|
||||
getByText('splitScreen.addToSplit:{"split":"Split 1"}'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getByText('splitScreen.paneItem:{"index":1,"label":"db-01"}'),
|
||||
).toBeTruthy();
|
||||
fireEvent.click(getByText('splitScreen.paneEmptyItem:{"index":2}'));
|
||||
|
||||
expect(onSplitAction).toHaveBeenCalledWith({
|
||||
kind: "addToPane",
|
||||
tabId: "terminal-1",
|
||||
splitTabId: "split-1",
|
||||
paneId: "p2",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds a new pane to an existing split", () => {
|
||||
const { getByText, onSplitAction } = openMenu({ splits: [split] });
|
||||
fireEvent.click(getByText("splitScreen.newPaneBelow"));
|
||||
expect(onSplitAction).toHaveBeenCalledWith({
|
||||
kind: "addPane",
|
||||
tabId: "terminal-1",
|
||||
splitTabId: "split-1",
|
||||
edge: "bottom",
|
||||
});
|
||||
});
|
||||
|
||||
it("disables new panes on a full split", () => {
|
||||
const { getByText } = openMenu({ splits: [{ ...split, full: true }] });
|
||||
expect(
|
||||
(
|
||||
getByText("splitScreen.newPaneRight").closest(
|
||||
"button",
|
||||
) as HTMLButtonElement
|
||||
).disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("offers unsplit on a split tab instead of splitting it", () => {
|
||||
const splitTab = {
|
||||
id: "split-1",
|
||||
type: "split-screen",
|
||||
label: "Split 1",
|
||||
split: {
|
||||
root: { kind: "pane", id: "p1", tabId: null },
|
||||
focusedPaneId: "p1",
|
||||
},
|
||||
} as Tab;
|
||||
const onSplitAction = vi.fn();
|
||||
const { getByText, queryByText } = render(
|
||||
<TabBar {...props({ tabs: [...tabs, splitTab], onSplitAction })} />,
|
||||
);
|
||||
fireEvent.contextMenu(getByText("Split 1"));
|
||||
|
||||
expect(queryByText("splitScreen.splitRight")).toBeNull();
|
||||
fireEvent.click(getByText("splitScreen.unsplit"));
|
||||
expect(onSplitAction).toHaveBeenCalledWith({
|
||||
kind: "unsplit",
|
||||
splitTabId: "split-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user