7a88b74016
- 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>
127 lines
3.9 KiB
TypeScript
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();
|
|
});
|
|
});
|