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>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user