b7efba8747
The status/name/back-arrow row is gone; Tools sits at the tab bar's right end and the pipeline/feature pickers show only with a template. LaneConsolePane is reduced to the terminal (no title, no Active runs, no New run): RunHistory.tsx, api.run.history and the page's active-sessions poll are removed.
193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
/**
|
|
* @file Workspace.changesPanel.test.tsx
|
|
* @description The Changes panel is ONE control: it steps 30/70 → 50/50 →
|
|
* full → closed, and the rail reopens it at 30/70. The width survives a reload.
|
|
* All of it lives in localStorage and reaches no server — the same rule as the
|
|
* lane strip's order.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, act, cleanup, screen, fireEvent } from "@testing-library/react";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
import i18n from "i18next";
|
|
|
|
// Hoisted: `vi.mock`'s factory is lifted above the module body, so the
|
|
// fixtures it reads have to be lifted with it.
|
|
const { HOME, LANE_A, LANE_B } = vi.hoisted(() => {
|
|
const laneStub = (id: number, cwd: string) => ({
|
|
id,
|
|
title: `lane-${id}`,
|
|
cwd,
|
|
branch: null,
|
|
kind: "adopted",
|
|
source_repo: null,
|
|
pipeline: "default",
|
|
session_id: null,
|
|
run_id: null,
|
|
stage: "idle",
|
|
stage_since: null,
|
|
status: "idle",
|
|
gate_decision: null,
|
|
ci_status: null,
|
|
needs_action: null,
|
|
links: {},
|
|
stages: {},
|
|
notes: null,
|
|
pipeline_name: "Default",
|
|
pipeline_nodes: [],
|
|
progress: 0,
|
|
stage_seconds: null,
|
|
last_event_seconds: null,
|
|
liveness: "idle",
|
|
detected_stage: null,
|
|
detected_signal: null,
|
|
slot: null,
|
|
ports: {},
|
|
active_feature_id: null,
|
|
});
|
|
return {
|
|
HOME: { kind: "home", path: "/Users/tester", label: "Home" },
|
|
LANE_A: laneStub(1, "/workspace/alpha"),
|
|
LANE_B: laneStub(2, "/workspace/beta"),
|
|
};
|
|
});
|
|
|
|
// The workspace's home (its own suite) fetches sources and memory; these
|
|
// tests are about the session view it hides, so it is a stub here.
|
|
vi.mock("../../components/lanes/WorkspaceHome", () => ({
|
|
WorkspaceHome: () => <div data-testid="workspace-home" />,
|
|
paneRunId: (id: number, slot: number) =>
|
|
slot ? `agenthub-lane-${id}-${slot}` : `agenthub-lane-${id}`,
|
|
freeSlot: () => 0,
|
|
}));
|
|
vi.mock("../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
const r = (value: unknown) => vi.fn().mockResolvedValue(value);
|
|
return {
|
|
...actual,
|
|
api: {
|
|
run: {
|
|
list: r({ items: [] }),
|
|
binary: r({ found: true, path: "/usr/bin/claude" }),
|
|
cwds: r({ items: [HOME] }),
|
|
files: r({ items: [] }),
|
|
start: r({ id: "run-1", status: "running" }),
|
|
get: r({ id: "run-1", status: "running" }),
|
|
},
|
|
lanes: {
|
|
list: r({
|
|
lanes: [LANE_A, LANE_B],
|
|
counts: { total: 2, running: 0, needs_you: 0, dead: 0 },
|
|
}),
|
|
pipelines: r({ pipelines: [] }),
|
|
git: r({ available: false }),
|
|
gitTree: r({ entries: [] }),
|
|
runtime: r({ up: false, ports: {}, lastError: null }),
|
|
features: { list: r({ features: [] }), show: r({ feature: null }) },
|
|
proof: { list: r({ features: [] }), imageUrl: () => "" },
|
|
},
|
|
aiRouters: { list: r({ items: [] }) },
|
|
sessions: { list: r({ sessions: [], total: 0, limit: 50, offset: 0 }) },
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("../../lib/eventBus", () => ({
|
|
eventBus: {
|
|
subscribe: () => () => {},
|
|
publish: () => {},
|
|
onConnection: () => () => {},
|
|
connected: true,
|
|
setConnected: () => {},
|
|
},
|
|
}));
|
|
|
|
import { Workspace } from "../Workspace";
|
|
|
|
class ObserverStub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver =
|
|
globalThis.ResizeObserver || (ObserverStub as unknown as typeof ResizeObserver);
|
|
|
|
async function settle() {
|
|
await act(async () => {
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
i18n.changeLanguage("en");
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("Workspace — the Changes panel's width", () => {
|
|
beforeEach(() => window.localStorage.clear());
|
|
|
|
async function openPanel() {
|
|
render(
|
|
<MemoryRouter initialEntries={["/run"]}>
|
|
<Workspace />
|
|
</MemoryRouter>
|
|
);
|
|
await settle();
|
|
// Open/closed is itself remembered, so the second mount of a test comes up
|
|
// already open — clicking the rail then would close it.
|
|
const rail = screen.queryByTestId("lane-changes-expand");
|
|
if (rail) {
|
|
fireEvent.click(rail);
|
|
await settle();
|
|
}
|
|
}
|
|
|
|
it("steps 30/70 → 50/50 → full → closed on one control, with no 70/30 step", async () => {
|
|
await openPanel();
|
|
const card = () => screen.getByTestId("lane-changes-card");
|
|
expect(card().className).toContain("w-[30%]");
|
|
|
|
fireEvent.click(screen.getByTestId("lane-changes-resize"));
|
|
expect(card().className).toContain("w-1/2");
|
|
|
|
// Full width HIDES the console beside it — hidden, still mounted.
|
|
fireEvent.click(screen.getByTestId("lane-changes-resize"));
|
|
expect(card().className).toContain("w-full");
|
|
expect(screen.getByTestId("lane-detail").className).toContain("hidden");
|
|
|
|
// Past the widest step the panel closes — and reopens at 30/70, not back
|
|
// at full width, which is why closing resets the width.
|
|
fireEvent.click(screen.getByTestId("lane-changes-resize"));
|
|
await settle();
|
|
expect(screen.queryByTestId("lane-changes-card")).toBeNull();
|
|
|
|
fireEvent.click(screen.getByTestId("lane-changes-expand"));
|
|
await settle();
|
|
expect(card().className).toContain("w-[30%]");
|
|
});
|
|
|
|
it("remembers the width across a reload, and never asks the server", async () => {
|
|
await openPanel();
|
|
fireEvent.click(screen.getByTestId("lane-changes-resize"));
|
|
expect(window.localStorage.getItem("agenthub.workspace.changesSize")).toBe("half");
|
|
|
|
cleanup();
|
|
await openPanel();
|
|
expect(screen.getByTestId("lane-changes-card").className).toContain("w-1/2");
|
|
});
|
|
|
|
it("keeps the rail as the only way back in", async () => {
|
|
await openPanel();
|
|
expect(screen.queryByTestId("lane-changes-collapse")).toBeNull();
|
|
expect(screen.getByTestId("lane-changes-card")).toBeInTheDocument();
|
|
});
|
|
});
|