6c605849d8
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note. REPL `help lanes|workspace|workspaces` resolves through the aliases. - A workspace holds up to ten sessions (slots 0-9); every console tab is a group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout); parseGroups keeps the stored active tab when an earlier one is dropped. - Browse button beside path fields: POST /api/lanes/pick-path opens the OS picker on the server's desktop — loopback only and no forwarding header (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is killed when the request closes, a folder field falls back to the in-app browser on any failure. - Changes rail counts chip, image viewer (/git/raw), status bar session counts and per-day cost (`date=` on /api/pricing/cost). - /agenthub-open and /agenthub-doctor run from the INSTALLED build (supersededBy), doctor offers fixes behind two confirmations; plugin-open treats a locked bootstrap as non-fatal. - New-terminal shell waits for the first attach (issue #55). - repo-onboarding / debug-live-issue skills removed. - OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history, /api/workspaces mount.
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
/**
|
|
* @file PathPickButton.test.tsx
|
|
* @description Unit tests for the Browse button beside path fields: a chosen
|
|
* path fills the field, a Cancel changes nothing, a folder falls back to the
|
|
* in-app browser on ANY native-picker failure, and a file falls back to a
|
|
* "type the path" hint only when no picker exists for this browser.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { api } from "../../lib/api";
|
|
import { toast } from "../../lib/toast";
|
|
import { PathPickButton } from "../PathPickButton";
|
|
|
|
vi.mock("../../lib/api", () => ({
|
|
api: { lanes: { pickPath: vi.fn(), browse: vi.fn() } },
|
|
}));
|
|
vi.mock("../../lib/toast", () => ({
|
|
toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() },
|
|
}));
|
|
|
|
const fail = (code: string) => Object.assign(new Error(code), { code });
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(api.lanes.pickPath).mockReset();
|
|
vi.mocked(api.lanes.browse)
|
|
.mockReset()
|
|
.mockResolvedValue({ path: "/tmp", parent: "/", entries: [] });
|
|
vi.mocked(toast.info).mockReset();
|
|
vi.mocked(toast.error).mockReset();
|
|
});
|
|
|
|
describe("PathPickButton", () => {
|
|
it("fills the field with the chosen path and leaves it alone on Cancel", async () => {
|
|
const onPick = vi.fn();
|
|
vi.mocked(api.lanes.pickPath).mockResolvedValueOnce({ path: "/home/u/proj" });
|
|
vi.mocked(api.lanes.pickPath).mockResolvedValueOnce({ cancelled: true });
|
|
render(<PathPickButton start=" /home/u " onPick={onPick} />);
|
|
const button = screen.getByTitle("Browse for a folder");
|
|
await userEvent.click(button);
|
|
await waitFor(() => expect(onPick).toHaveBeenCalledWith("/home/u/proj"));
|
|
expect(api.lanes.pickPath).toHaveBeenCalledWith("dir", "/home/u");
|
|
await userEvent.click(button);
|
|
await waitFor(() => expect(api.lanes.pickPath).toHaveBeenCalledTimes(2));
|
|
expect(onPick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("opens the in-app folder browser on any failure, not only a missing picker", async () => {
|
|
vi.mocked(api.lanes.pickPath).mockRejectedValue(fail("EBUSY"));
|
|
render(<PathPickButton onPick={vi.fn()} />);
|
|
await userEvent.click(screen.getByTitle("Browse for a folder"));
|
|
expect(await screen.findByRole("dialog", { name: "Browse for a folder" })).toBeInTheDocument();
|
|
expect(toast.error).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("hints to type a file path when no picker exists, and reports any other failure", async () => {
|
|
vi.mocked(api.lanes.pickPath).mockRejectedValueOnce(fail("ENODIALOG"));
|
|
vi.mocked(api.lanes.pickPath).mockRejectedValueOnce(fail("EDIALOG"));
|
|
render(<PathPickButton kind="file" onPick={vi.fn()} />);
|
|
const button = screen.getByRole("button");
|
|
await userEvent.click(button);
|
|
await waitFor(() => expect(toast.info).toHaveBeenCalledTimes(1));
|
|
await userEvent.click(button);
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith("EDIALOG"));
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
});
|
|
});
|