/** * @file ConsoleTabs.test.tsx * @description The tab bar's spring-loaded tabs: a pane drag resting on * another tab shows it after `SPRING_MS`, so the pane can be dropped on that * tab's panes; moving off before then, or resting on the tab already holding * the pane, shows nothing. * @author Nguyễn Ngọc Trí Vĩ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, createEvent, fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import type { RunHandle } from "../../../lib/api"; import type { Lane } from "../../../lib/types"; import { pane, split, type PaneGroups } from "../../../lib/paneLayout"; import { ConsoleTabs, SPRING_MS } from "../ConsoleTabs"; import { PANE_DRAG_TYPE, paneDragSlot } from "../PaneGrid"; vi.mock("../../../lib/api", () => ({ api: { aiTools: { list: vi.fn().mockResolvedValue({ default: "claude", tools: [] }), models: vi.fn().mockResolvedValue({ flag: true, models: [], defaultModel: null }), }, }, })); const LANE = { id: 1, tool: "claude", cwd: "/w", title: "w" } as unknown as Lane; const run = (slot: number): RunHandle => ({ id: slot ? `agenthub-lane-1-${slot}` : "agenthub-lane-1", laneId: 1, slot, status: "running", cwd: "/w", model: null, permissionMode: null, effort: null, resumeSessionId: null, sessionId: null, startedAt: null, promptPreview: null, tool: "shell", }); // Tab 0: pane 0 alone (on screen); tab 1: panes 1 and 2. const GROUPS: PaneGroups = { groups: [ { root: pane(0), focus: 0 }, { root: split("row", pane(1), pane(2)), focus: 1 }, ], active: 0, }; function setup() { let groups = GROUPS; const onGroupsChange = vi.fn((fn: (g: PaneGroups) => PaneGroups) => { groups = fn(groups); }); render( ); // jsdom lays nothing out: tab i spans x 100i … 100i+100. for (const el of document.querySelectorAll("[data-tab-index]")) { const i = Number(el.dataset.tabIndex); el.getBoundingClientRect = () => ({ left: i * 100, right: i * 100 + 100, width: 100, top: 0, bottom: 40, height: 40, }) as DOMRect; } return { onGroupsChange, groups: () => groups }; } const dataTransfer = () => ({ types: [PANE_DRAG_TYPE], setData: vi.fn(), getData: () => "", effectAllowed: "", dropEffect: "", }); function drag(type: "dragStart" | "dragOver", el: Element, clientX = 0) { const ev = createEvent[type](el, { dataTransfer: dataTransfer() }); Object.defineProperty(ev, "clientX", { value: clientX }); fireEvent(el, ev); } const tabEl = (key: string) => screen.getByTestId(`console-tab-${key}`).parentElement as Element; describe("ConsoleTabs spring-loaded tabs", () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); it("a pane drag resting on another tab shows that tab", () => { const { onGroupsChange, groups } = setup(); drag("dragStart", tabEl("0")); expect(paneDragSlot()).toBe(0); drag("dragOver", tabEl("1-2"), 150); act(() => void vi.advanceTimersByTime(SPRING_MS - 1)); expect(onGroupsChange).not.toHaveBeenCalled(); act(() => void vi.advanceTimersByTime(1)); expect(groups().active).toBe(1); }); it("moving off the tab first, or resting on the pane's own tab, shows nothing", () => { const { onGroupsChange } = setup(); drag("dragStart", tabEl("0")); drag("dragOver", tabEl("1-2"), 150); act(() => void vi.advanceTimersByTime(SPRING_MS / 2)); drag("dragOver", tabEl("0"), 50); act(() => void vi.advanceTimersByTime(SPRING_MS * 2)); expect(onGroupsChange).not.toHaveBeenCalled(); }); });