Files
AgentHub/client/src/components/run/__tests__/ConsoleTabs.test.tsx
T
nntrivi2001 7a88b74016 feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:21:33 +07:00

127 lines
3.9 KiB
TypeScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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(
<MemoryRouter>
<ConsoleTabs
lane={LANE}
tabs={[run(0), run(1), run(2)]}
groups={GROUPS}
onGroupsChange={onGroupsChange}
onOpen={vi.fn()}
onClosed={vi.fn()}
/>
</MemoryRouter>
);
// jsdom lays nothing out: tab i spans x 100i … 100i+100.
for (const el of document.querySelectorAll<HTMLElement>("[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();
});
});