← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/ui/workbench-tools.test.tsx
17.4 KB · Sep 30, 2026 · 23:01 UTC
import { act, cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { WorkbenchToolController } from "./workbench-tool-controller";
import { WorkbenchDisclosure } from "./workbench-disclosure";
import { useWorkbenchFeedback } from "./workbench-feedback";
import {
WorkbenchToolControllerContext,
WorkbenchTools,
} from "./workbench-tools";
afterEach(cleanup);
describe("WorkbenchTools", () => {
it("starts closed and preserves input identity and value when switching tools", async () => {
const user = userEvent.setup();
render(
<WorkbenchTools
group="sequence-tools"
label="Sequence tools"
panels={[
{
content: (
<input aria-label="Feature name" defaultValue="promoter" />
),
id: "inspect",
label: "Inspect",
},
{
content: <p>Analysis settings</p>,
id: "analyze",
label: "Analyze",
},
]}
/>,
);
const input = screen.getByLabelText("Feature name");
expect(input).not.toBeVisible();
expect(input.closest("section")).toHaveAttribute("inert");
await user.click(screen.getByRole("button", { name: "Inspect" }));
await user.clear(input);
await user.type(input, "cI promoter");
await user.click(screen.getByRole("button", { name: "Analyze" }));
expect(input).not.toBeVisible();
expect(screen.getByText("Analysis settings")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Inspect" }));
expect(screen.getByLabelText("Feature name")).toBe(input);
expect(input).toHaveValue("cI promoter");
await user.click(screen.getByRole("button", { name: "Inspect" }));
expect(input).not.toBeVisible();
expect(screen.getByRole("button", { name: "Inspect" })).toHaveAttribute(
"aria-expanded",
"false",
);
});
it("keeps pending feedback visible until explicitly dismissed", async () => {
function CopyFeedback(): React.ReactElement {
const [pending, setPending] = useState(true);
return pending ? (
<div>
<textarea aria-label="Sequence to copy" readOnly value="ACGT" />
<button onClick={() => setPending(false)} type="button">
Dismiss copy feedback
</button>
</div>
) : (
<p>Ready</p>
);
}
const user = userEvent.setup();
render(
<WorkbenchTools
defaultPanel="export"
group="sequence-tools"
label="Sequence tools"
panels={[
{ content: <CopyFeedback />, id: "export", label: "Export" },
{ content: <p>Annotations</p>, id: "inspect", label: "Inspect" },
]}
/>,
);
await user.click(screen.getByRole("button", { name: "Inspect" }));
expect(screen.getByLabelText("Sequence to copy")).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent("Finish or dismiss");
await user.click(
screen.getByRole("button", { name: "Dismiss copy feedback" }),
);
await user.click(screen.getByRole("button", { name: "Inspect" }));
expect(screen.getByText("Annotations")).toBeVisible();
});
it("ignores hidden feedback and nonblocking information", async () => {
const user = userEvent.setup();
render(
<WorkbenchTools
defaultPanel="inspect"
group="sequence-tools"
label="Sequence tools"
panels={[
{
content: (
<div>
<section hidden inert>
<p role="alert">Old analysis error</p>
</section>
<textarea
aria-label="Saved project information"
data-workbench-nonblocking="true"
readOnly
value="source metadata"
/>
</div>
),
id: "inspect",
label: "Inspect",
},
{
content: <p>Analysis settings</p>,
id: "analyze",
label: "Analyze",
},
]}
/>,
);
await user.click(screen.getByRole("button", { name: "Analyze" }));
expect(screen.getByText("Analysis settings")).toBeVisible();
});
it("shares direct, idempotent panel operations with the agent without unmounting content", async () => {
const user = userEvent.setup();
const controller = new WorkbenchToolController();
render(
<WorkbenchToolControllerContext.Provider value={controller}>
<WorkbenchTools
group="sequence-tools"
label="Sequence tools"
panels={[
{
content: (
<input aria-label="Feature name" defaultValue="promoter" />
),
id: "inspect",
label: "Inspect",
},
{
content: <p>Analysis settings</p>,
id: "analyze",
label: "Analyze",
},
]}
/>
</WorkbenchToolControllerContext.Provider>,
);
const input = screen.getByLabelText("Feature name");
expect(controller.getSnapshots()).toEqual([
expect.objectContaining({
activePanel: null,
blocked: false,
group: "sequence-tools",
panels: [
{ id: "inspect", label: "Inspect" },
{ id: "analyze", label: "Analyze" },
],
}),
]);
act(() => {
expect(controller.setPanel("sequence-tools", "inspect")).toMatchObject({
applied: true,
state: { activePanel: "inspect" },
});
});
expect(input).toBeVisible();
await user.type(input, " edited");
act(() => {
expect(controller.setPanel("sequence-tools", "inspect")).toMatchObject({
applied: true,
state: { activePanel: "inspect" },
});
});
expect(input).toBeVisible();
await user.click(screen.getByRole("button", { name: "Analyze" }));
expect(controller.getSnapshots()[0]?.activePanel).toBe("analyze");
act(() => {
controller.setPanel("sequence-tools", null);
controller.setPanel("sequence-tools", "inspect");
});
expect(screen.getByLabelText("Feature name")).toBe(input);
expect(input).toHaveValue("promoter edited");
expect(controller.setPanel("sequence-tools", "missing")).toMatchObject({
applied: false,
state: { activePanel: "inspect" },
});
expect(controller.setPanel("alignment-tools", "inspect")).toMatchObject({
applied: false,
state: { mountedGroups: ["sequence-tools"] },
});
});
it("rejects agent switches and closes while the same visible UI approval is pending", async () => {
const controller = new WorkbenchToolController();
const user = userEvent.setup();
function Approval(): React.ReactElement {
const [pending, setPending] = useState(true);
return pending ? (
<div role="alertdialog">
<p>Confirm workspace destination</p>
<button onClick={() => setPending(false)} type="button">
Cancel pending export
</button>
</div>
) : (
<p>Export cancelled</p>
);
}
render(
<WorkbenchToolControllerContext.Provider value={controller}>
<WorkbenchTools
defaultPanel="export"
group="sequence-tools"
label="Sequence tools"
panels={[
{ content: <Approval />, id: "export", label: "Export" },
{
content: <p>Analysis settings</p>,
id: "analyze",
label: "Analyze",
},
]}
/>
</WorkbenchToolControllerContext.Provider>,
);
act(() => {
expect(controller.setPanel("sequence-tools", "analyze")).toMatchObject({
applied: false,
state: { activePanel: "export", blocked: true },
});
expect(controller.setPanel("sequence-tools", null)).toMatchObject({
applied: false,
});
expect(controller.setPanel("sequence-tools", "export")).toMatchObject({
applied: true,
});
});
expect(screen.getByRole("alertdialog")).toBeVisible();
expect(controller.getSnapshots()[0]?.blocked).toBe(true);
await user.click(
screen.getByRole("button", { name: "Cancel pending export" }),
);
act(() => {
expect(controller.setPanel("sequence-tools", "analyze")).toMatchObject({
applied: true,
});
});
expect(screen.getByText("Analysis settings")).toBeVisible();
});
it("discovers mounted hidden disclosures and reveals their parent tools through shared operations", async () => {
const controller = new WorkbenchToolController();
const user = userEvent.setup();
render(
<WorkbenchToolControllerContext.Provider value={controller}>
<WorkbenchTools
group="sequence-tools"
label="Sequence tools"
panels={[
{
id: "inspect",
label: "Inspect",
content: (
<WorkbenchDisclosure
id="sequence.outer"
label="Inspector details"
>
<WorkbenchDisclosure
id="sequence.inner"
label="Nested methods"
>
<input
aria-label="Method notes"
defaultValue="Original notes"
/>
</WorkbenchDisclosure>
</WorkbenchDisclosure>
),
},
{
id: "analyze",
label: "Analyze",
content: <p>Analysis settings</p>,
},
]}
/>
</WorkbenchToolControllerContext.Provider>,
);
const input = screen.getByLabelText("Method notes");
expect(input).not.toBeVisible();
expect(controller.getDisclosureSnapshots()).toEqual([
expect.objectContaining({
id: "sequence.inner",
expanded: false,
ancestors: ["sequence.outer"],
location: { group: "sequence-tools", panel: "inspect" },
}),
expect.objectContaining({
id: "sequence.outer",
expanded: false,
ancestors: [],
location: { group: "sequence-tools", panel: "inspect" },
}),
]);
act(() => {
expect(controller.setDisclosure("sequence.inner", true)).toMatchObject({
applied: true,
state: { disclosure: { id: "sequence.inner", expanded: true } },
});
});
expect(input).toBeVisible();
expect(screen.getByRole("button", { name: "Inspect" })).toHaveAttribute(
"aria-expanded",
"true",
);
await user.type(input, " updated");
await user.click(screen.getByText("Nested methods"));
expect(
controller
.getDisclosureSnapshots()
.find(({ id }) => id === "sequence.inner")?.expanded,
).toBe(false);
expect(input).not.toBeVisible();
act(() => {
controller.setDisclosure("sequence.inner", true);
});
expect(screen.getByLabelText("Method notes")).toBe(input);
expect(input).toHaveValue("Original notes updated");
});
it("uses source-scoped disclosure pages and resets disclosure state on source replacement", () => {
const controller = new WorkbenchToolController();
const renderDisclosures = (
active: WorkbenchToolController,
): React.ReactElement => (
<WorkbenchToolControllerContext.Provider value={active}>
<WorkbenchDisclosure id="sequence.first" label="First section">
<p>First body</p>
</WorkbenchDisclosure>
<WorkbenchDisclosure id="sequence.second" label="Second section">
<p>Second body</p>
</WorkbenchDisclosure>
</WorkbenchToolControllerContext.Provider>
);
const view = render(renderDisclosures(controller));
act(() => {
controller.setDisclosure("sequence.first", true);
});
const firstPage = controller.queryDisclosures({
limit: 1,
mode: "sequence",
});
expect(firstPage).toMatchObject({
target: "workbench-disclosures",
page: { count: 1, offset: 0, totalCount: 2 },
truncated: true,
items: [
expect.objectContaining({ id: "sequence.first", expanded: true }),
],
});
expect(firstPage.nextCursor).toEqual(expect.any(String));
const next = controller.queryDisclosures({
cursor: firstPage.nextCursor ?? undefined,
limit: 1,
mode: "sequence",
});
expect(next).toMatchObject({
nextCursor: null,
page: { count: 1, offset: 1, totalCount: 2 },
items: [expect.objectContaining({ id: "sequence.second" })],
});
expect(() =>
controller.queryDisclosures({
cursor: firstPage.nextCursor ?? undefined,
limit: 1,
mode: "alignment",
}),
).toThrow("Invalid workbench-disclosures cursor");
const replacement = new WorkbenchToolController();
view.rerender(renderDisclosures(replacement));
expect(screen.getByText("First body")).not.toBeVisible();
expect(
replacement.getDisclosureSnapshots().every(({ expanded }) => !expanded),
).toBe(true);
expect(() =>
replacement.queryDisclosures({
cursor: firstPage.nextCursor ?? undefined,
limit: 1,
mode: "sequence",
}),
).toThrow("Invalid workbench-disclosures cursor");
});
it("cannot collapse pending manual-copy feedback through either disclosure path", async () => {
const controller = new WorkbenchToolController();
const user = userEvent.setup();
render(
<WorkbenchToolControllerContext.Provider value={controller}>
<WorkbenchDisclosure
defaultExpanded
id="sequence.copy"
label="Copy details"
>
<textarea aria-label="Manual copy" readOnly value="ACGT" />
</WorkbenchDisclosure>
</WorkbenchToolControllerContext.Provider>,
);
act(() => {
expect(controller.setDisclosure("sequence.copy", false)).toMatchObject({
applied: false,
state: { disclosure: { expanded: true, blocked: true } },
});
});
await user.click(screen.getByText("Copy details"));
expect(screen.getByLabelText("Manual copy")).toBeVisible();
expect(controller.getDisclosureSnapshots()[0]).toMatchObject({
expanded: true,
blocked: true,
});
});
it("shares explicit feedback dismissal without exposing source text or an approval operation", async () => {
const controller = new WorkbenchToolController();
const onDismiss = vi.fn();
const user = userEvent.setup();
function CopyFeedback(): React.ReactElement {
const [visible, setVisible] = useState(true);
const dismiss = useWorkbenchFeedback({
id: "sequence.copy-feedback",
kind: "copy",
label: "Copy feedback",
message: visible ? "Clipboard unavailable" : undefined,
onDismiss: () => {
onDismiss();
setVisible(false);
},
visible,
});
return (
<div>
{visible ? (
<textarea
aria-label="Copy sequence manually"
readOnly
value="ACGT_PRIVATE_SOURCE"
/>
) : null}
<button onClick={() => dismiss()} type="button">
Dismiss copy feedback
</button>
<button onClick={() => setVisible(true)} type="button">
Show copy feedback
</button>
</div>
);
}
render(
<WorkbenchToolControllerContext.Provider value={controller}>
<WorkbenchTools
defaultPanel="copy"
group="sequence-display"
label="Display tools"
panels={[
{ content: <CopyFeedback />, id: "copy", label: "Copy" },
{ content: <p>View settings</p>, id: "display", label: "Display" },
]}
/>
</WorkbenchToolControllerContext.Provider>,
);
const query = controller.queryFeedback({ limit: 100, mode: "sequence" });
expect(query).toMatchObject({
target: "workbench-feedback",
items: [
{
id: "sequence.copy-feedback",
kind: "copy",
label: "Copy feedback",
message: "Clipboard unavailable",
visible: true,
},
],
});
expect(JSON.stringify(query)).not.toContain("ACGT_PRIVATE_SOURCE");
act(() => {
expect(controller.setPanel("sequence-display", "display").applied).toBe(
false,
);
expect(
controller.dismissFeedback("sequence.copy-feedback"),
).toMatchObject({
applied: true,
state: { feedback: { visible: false } },
});
expect(
controller.dismissFeedback("sequence.copy-feedback"),
).toMatchObject({ applied: true });
});
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(
screen.queryByLabelText("Copy sequence manually"),
).not.toBeInTheDocument();
expect(controller.getFeedbackSnapshots()[0]?.visible).toBe(false);
expect(
controller.dismissFeedback("sequence.source-write-approval"),
).toMatchObject({ applied: false });
await user.click(
screen.getByRole("button", { name: "Show copy feedback" }),
);
await user.click(
screen.getByRole("button", { name: "Dismiss copy feedback" }),
);
expect(onDismiss).toHaveBeenCalledTimes(2);
act(() => {
expect(controller.setPanel("sequence-display", "display").applied).toBe(
true,
);
});
expect(screen.getByText("View settings")).toBeVisible();
});
});
SHA-256: a47ee913ce24c8399d43a00b50384940dd6171a7874a72dc4a27edc1dde8ba10