Closes #246. The transcript, composer and approval controls move from structured.ts/structuredRender.ts into components under web/src/components/structured/, against docs/STRUCTURED-VIEW.md: eight row kinds, both projections, the single-active approval nonce, and diff rendering with its own line counts. The pure layers stay shared — structuredDiff.ts computes the diff, structuredRender.ts still sanitizes markdown — and model.ts holds the client-side decisions (activity grouping, notice classification, offered decisions, `/` completion) so they are testable without a DOM. Like TerminalPane, the pane draws the body only; pane chrome reads the connection through onStatusChange. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { ApprovalView } from "./ApprovalView.js";
|
|
|
|
afterEach(cleanup);
|
|
|
|
const ALL_OPTIONS = [
|
|
{ optionId: "a1", name: "Allow once", kind: "allow_once" as const },
|
|
{ optionId: "a2", name: "Allow always", kind: "allow_always" as const },
|
|
{ optionId: "r1", name: "Reject", kind: "reject_once" as const },
|
|
];
|
|
|
|
function buttons(): string[] {
|
|
return [...document.querySelectorAll(".st-approval-acts button")].map((b) => b.textContent ?? "");
|
|
}
|
|
|
|
describe("ApprovalView", () => {
|
|
it("asks the operator with the three decisions the daemon exposes", () => {
|
|
render(
|
|
<ApprovalView
|
|
request={{ toolCall: { title: "Write(a.ts)", locations: [{ path: "/repo/a.ts" }] }, options: ALL_OPTIONS }}
|
|
pending
|
|
onDecide={() => Promise.resolve()}
|
|
/>,
|
|
);
|
|
expect(buttons()).toEqual(["Allow once", "Allow for session", "Deny"]);
|
|
expect(screen.getByText("Write(a.ts)")).toBeTruthy();
|
|
expect(screen.getByText("/repo/a.ts")).toBeTruthy();
|
|
});
|
|
|
|
it("submits the decision the operator picked", async () => {
|
|
const onDecide = vi.fn().mockResolvedValue(undefined);
|
|
render(<ApprovalView request={{ options: ALL_OPTIONS }} pending onDecide={onDecide} />);
|
|
fireEvent.click(screen.getByText("Deny"));
|
|
await waitFor(() => expect(onDecide).toHaveBeenCalledWith("deny"));
|
|
});
|
|
|
|
it("disables every button once one is pressed, so a decision is sent once", () => {
|
|
render(<ApprovalView request={{ options: ALL_OPTIONS }} pending onDecide={() => new Promise(() => {})} />);
|
|
fireEvent.click(screen.getByText("Allow once"));
|
|
const all = [...document.querySelectorAll<HTMLButtonElement>(".st-approval-acts button")];
|
|
expect(all.every((b) => b.disabled)).toBe(true);
|
|
});
|
|
|
|
it("restores the controls when the decision failed and this approval is still pending", async () => {
|
|
const onDecide = vi.fn().mockRejectedValue(new Error("nope"));
|
|
render(<ApprovalView request={{ options: ALL_OPTIONS }} pending onDecide={onDecide} />);
|
|
fireEvent.click(screen.getByText("Allow once"));
|
|
await waitFor(() => {
|
|
const all = [...document.querySelectorAll<HTMLButtonElement>(".st-approval-acts button")];
|
|
expect(all.every((b) => !b.disabled)).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("offers no button for a decision the request cannot back", () => {
|
|
render(
|
|
<ApprovalView
|
|
request={{ options: [{ optionId: "a1", name: "Allow once", kind: "allow_once" }] }}
|
|
pending
|
|
onDecide={() => Promise.resolve()}
|
|
/>,
|
|
);
|
|
expect(buttons()).toEqual(["Allow once"]);
|
|
});
|
|
|
|
it("says so when the request offers no decision at all", () => {
|
|
render(<ApprovalView request={{ options: [] }} pending onDecide={() => Promise.resolve()} />);
|
|
expect(screen.getByText("no decision offered")).toBeTruthy();
|
|
});
|
|
|
|
it("renders a resolved approval as a one-liner, with no controls", () => {
|
|
render(
|
|
<ApprovalView
|
|
request={{ toolCall: { title: "Write(a.ts)" }, options: ALL_OPTIONS }}
|
|
pending={false}
|
|
onDecide={() => Promise.resolve()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Write(a.ts) — approved")).toBeTruthy();
|
|
expect(document.querySelector(".st-approval-acts")).toBeNull();
|
|
});
|
|
});
|