Files
AgentHub/client/src/components/__tests__/Select.test.tsx
T
nntrivi2001 fa156d6a59 feat: v2.6.0 — Chat UI composer (+ menu, project files, activity panel, permission mode, Stop), git on a workspace's extra folders (#70), long paste as a file (#68), S3 server mode (experimental)
Chat UI:
- Each Claude/Codex session keeps its own chat/terminal view; Settings only
  sets how new sessions open.
- Composer + menu: upload, project files and folders (#54, also dragged from
  the Files/Changes tree), context files, recent uploads, skills, plugins.
- Activity side panel (progress, outputs, files read, uploads, memory,
  skills, connectors) with a file preview; edited-file counts per turn.
- Permission-mode chip (Shift+Tab steps, read back from the pane:
  server/lib/pane-mode.js, GET|POST /api/run/:id/permission-mode).
- Stop (Esc) while the main agent works; the status is checked per agent
  (with session_id /api/agents ignores its status filter) and re-read every
  5 s while Stop shows.
- Dropped images shown in the conversation (GET /api/run/drop/:name, the
  requester's own drop dir), full-size viewer.
- A plain-text paste over 5,000 characters is attached as pasted-text.txt,
  so no prompt reaches tmux/argv limits (#68).

Changes panel: every /api/lanes/:id/git* route takes ?source=<lane_sources
id> (sourceFolder; ENOSOURCE otherwise) and a folder picker switches the
whole panel to a workspace's extra folder (#70).

Server mode S3 (one Linux user per account) — experimental, verified with
fake sudo plus a P6 managed-settings spike on a test account:
- member tmux listing runs in the background (cached, 15 s => unknown);
- transcript snapshot copy is async, chunked, temp file + rename;
- Express Layer patch only on Express 4;
- tmux capturePane/sendKey go to the member's own server.

Docs: the multi-user design moved in as docs/plans/server-mode/README.md
(the AgentHub-Server repo is gone); S3-PLAN P6 result; LANES/API/openapi.
2026-10-09 16:35:07 +07:00

112 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Select.test.tsx
* @description The shared Select as a WAI-ARIA listbox button: expanded state,
* listbox/option roles with aria-selected, aria-activedescendant tracking,
* Arrow/Home/End/Enter/Escape/type-ahead, focus back on the trigger, and a
* list that flips up by the room in its split pane, not the window's.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { Select } from "../Select";
const OPTIONS = [
{ value: "a", label: "Apple" },
{ value: "b", label: "Banana" },
{ value: "c", label: "Cherry" },
{ value: "d", label: "Blueberry" },
];
function setup(value = "b") {
const onChange = vi.fn();
render(<Select value={value} onChange={onChange} options={OPTIONS} label="Fruit" />);
const button = screen.getByRole("button", { name: "Fruit" });
return { onChange, button };
}
const activeLabel = () => {
const list = screen.getByRole("listbox");
return document.getElementById(list.getAttribute("aria-activedescendant")!)?.textContent;
};
describe("Select (listbox button)", () => {
it("opens on ArrowDown into a focused listbox on the chosen option", () => {
const { button } = setup();
expect(button).toHaveAttribute("aria-haspopup", "listbox");
expect(button).toHaveAttribute("aria-expanded", "false");
fireEvent.keyDown(button, { key: "ArrowDown" });
expect(button).toHaveAttribute("aria-expanded", "true");
const list = screen.getByRole("listbox", { name: "Fruit" });
expect(list).toHaveFocus();
expect(button).toHaveAttribute("aria-controls", list.id);
expect(screen.getByRole("option", { name: "Banana" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("option", { name: "Apple" })).toHaveAttribute("aria-selected", "false");
expect(activeLabel()).toBe("Banana");
});
it("moves with arrows/Home/End and picks with Enter, focus back on the button", () => {
const { button, onChange } = setup();
fireEvent.click(button);
const list = screen.getByRole("listbox");
fireEvent.keyDown(list, { key: "ArrowDown" });
expect(activeLabel()).toBe("Cherry");
fireEvent.keyDown(list, { key: "Home" });
expect(activeLabel()).toBe("Apple");
fireEvent.keyDown(list, { key: "End" });
expect(activeLabel()).toBe("Blueberry");
fireEvent.keyDown(list, { key: "ArrowUp" });
fireEvent.keyDown(list, { key: "Enter" });
expect(onChange).toHaveBeenCalledWith("c");
expect(screen.queryByRole("listbox")).toBeNull();
expect(button).toHaveFocus();
});
it("Escape closes without picking and does not bubble", () => {
const { button, onChange } = setup();
const behind = vi.fn();
document.addEventListener("keydown", behind);
fireEvent.click(button);
fireEvent.keyDown(screen.getByRole("listbox"), { key: "Escape" });
document.removeEventListener("keydown", behind);
expect(onChange).not.toHaveBeenCalled();
expect(behind).not.toHaveBeenCalled();
expect(screen.queryByRole("listbox")).toBeNull();
expect(button).toHaveFocus();
});
it("type-ahead jumps to the next option starting with the typed letter", () => {
const { button } = setup("a");
fireEvent.click(button);
const list = screen.getByRole("listbox");
fireEvent.keyDown(list, { key: "b" });
expect(activeLabel()).toBe("Banana");
fireEvent.keyDown(list, { key: "b" });
expect(activeLabel()).toBe("Blueberry"); // a repeated letter cycles
});
it("picks with a click on an option", () => {
const { button, onChange } = setup();
fireEvent.click(button);
fireEvent.click(screen.getByText("Apple"));
expect(onChange).toHaveBeenCalledWith("a");
});
it("opens upward when its pane (data-menu-bounds) has no room below, whatever the window has", () => {
const rect = (top: number, bottom: number) => ({ top, bottom }) as DOMRect;
render(
<div data-menu-bounds data-testid="pane">
<Select value="b" onChange={() => {}} options={OPTIONS} label="Fruit" />
</div>
);
// A top pane 0–400 in a 2000px window; the trigger near its bottom.
vi.spyOn(screen.getByTestId("pane"), "getBoundingClientRect").mockReturnValue(rect(0, 400));
const button = screen.getByRole("button", { name: "Fruit" });
vi.spyOn(button, "getBoundingClientRect").mockReturnValue(rect(360, 380));
vi.stubGlobal("innerHeight", 2000);
fireEvent.click(button);
expect(screen.getByRole("listbox").className).toContain("bottom-full");
vi.unstubAllGlobals();
});
});