62d1bdc462
- Add TerminalSessionTabs component with status dots, rename, close, max-5 limit - Add 7 component tests for tab rendering, selection, close, rename - TerminalComponent: sessionId prop, forwardRef with fit() method - TerminalPage: multi-session orchestration, tab switching, auto-create default - Fullscreen mode: Alt+Shift+F toggle, auto-hide tabs, Esc exit - Keyboard shortcuts: Alt+Shift+N/W/ArrowLeft/ArrowRight/R - Add CSS for tabs, fullscreen, mobile responsive - Update useTerminalSessions hook for session CRUD - terminal_manager.py: lookup by internal session_id fallback Quality gates: tsc --noEmit clean, vitest (7/7 new tests passed), pytest (182 passed)
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
TerminalSessionTabs,
|
|
type TerminalSessionInfo,
|
|
} from "./terminal-session-tabs";
|
|
|
|
const mockSessions: TerminalSessionInfo[] = [
|
|
{ id: "s1", name: "Session 1", status: "connected" },
|
|
{ id: "s2", name: "Session 2", status: "connecting" },
|
|
{ id: "s3", name: "Session 3", status: "disconnected" },
|
|
];
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe("TerminalSessionTabs", () => {
|
|
it("renders all tabs", () => {
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Session 1")).toBeInTheDocument();
|
|
expect(screen.getByText("Session 2")).toBeInTheDocument();
|
|
expect(screen.getByText("Session 3")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: /new session/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it("clicking a tab calls onSelect", () => {
|
|
const onSelect = vi.fn();
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={onSelect}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getAllByText("Session 2")[0]);
|
|
expect(onSelect).toHaveBeenCalledWith("s2");
|
|
});
|
|
|
|
it("close button calls onClose after confirmation", () => {
|
|
const onClose = vi.fn();
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={onClose}
|
|
onCreate={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
const closeButton = screen.getByLabelText("Close session Session 1");
|
|
// First click shows confirm
|
|
fireEvent.click(closeButton);
|
|
expect(screen.getByText("Close?")).toBeInTheDocument();
|
|
|
|
// Click confirm text
|
|
fireEvent.click(screen.getByText("Close?"));
|
|
expect(onClose).toHaveBeenCalledWith("s1");
|
|
});
|
|
|
|
it("double-click enables rename and Enter commits", () => {
|
|
const onRename = vi.fn();
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={onRename}
|
|
/>
|
|
);
|
|
|
|
fireEvent.doubleClick(screen.getAllByText("Session 1")[0]);
|
|
const input = screen.getByLabelText("Rename session");
|
|
expect(input).toBeInTheDocument();
|
|
|
|
fireEvent.change(input, { target: { value: "Renamed" } });
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
expect(onRename).toHaveBeenCalledWith("s1", "Renamed");
|
|
});
|
|
|
|
it("double-click enables rename and Escape cancels", () => {
|
|
const onRename = vi.fn();
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={onRename}
|
|
/>
|
|
);
|
|
|
|
fireEvent.doubleClick(screen.getAllByText("Session 1")[0]);
|
|
const input = screen.getByLabelText("Rename session");
|
|
fireEvent.change(input, { target: { value: "Renamed" } });
|
|
fireEvent.keyDown(input, { key: "Escape" });
|
|
expect(onRename).not.toHaveBeenCalled();
|
|
expect(screen.getByText("Session 1")).toBeInTheDocument();
|
|
});
|
|
|
|
it("plus button is disabled at 5 sessions", () => {
|
|
const fiveSessions: TerminalSessionInfo[] = Array.from({ length: 5 }, (_, i) => ({
|
|
id: `s${i + 1}`,
|
|
name: `Session ${i + 1}`,
|
|
status: "connected",
|
|
}));
|
|
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={fiveSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
const newButton = screen.getByRole("button", { name: /new session/i });
|
|
expect(newButton).toBeDisabled();
|
|
});
|
|
|
|
it("status dot reflects connection state", () => {
|
|
render(
|
|
<TerminalSessionTabs
|
|
sessions={mockSessions}
|
|
activeSessionId="s1"
|
|
onSelect={vi.fn()}
|
|
onClose={vi.fn()}
|
|
onCreate={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
const tabs = screen.getAllByRole("tab");
|
|
expect(tabs).toHaveLength(3);
|
|
expect(tabs[0].querySelector(".connected")).toBeInTheDocument();
|
|
expect(tabs[1].querySelector(".connecting")).toBeInTheDocument();
|
|
expect(tabs[2].querySelector(".disconnected")).toBeInTheDocument();
|
|
});
|
|
});
|