Four-button row beneath the input bar — Promote (volt outline, disabled when the conversation is not promotable), Attach, Memory, History. Pure presentational component; the caller owns click handlers and the promotable predicate. Phase 12 will wire the promote animation itself. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
4.8 KiB
TypeScript
178 lines
4.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ActionStrip } from "./ActionStrip";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("<ActionStrip />", () => {
|
|
let container: HTMLDivElement;
|
|
let root: ReturnType<typeof createRoot> | null = null;
|
|
|
|
const noop = () => {};
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = null;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (root) {
|
|
act(() => {
|
|
root!.unmount();
|
|
});
|
|
root = null;
|
|
}
|
|
if (container.parentNode) container.remove();
|
|
});
|
|
|
|
function render(node: React.ReactNode) {
|
|
root = createRoot(container);
|
|
act(() => {
|
|
root!.render(node);
|
|
});
|
|
}
|
|
|
|
function buttons() {
|
|
return {
|
|
promote: container.querySelector('[data-testid="action-promote"]') as HTMLButtonElement,
|
|
attach: container.querySelector('[data-testid="action-attach"]') as HTMLButtonElement,
|
|
memory: container.querySelector('[data-testid="action-memory"]') as HTMLButtonElement,
|
|
history: container.querySelector('[data-testid="action-history"]') as HTMLButtonElement,
|
|
};
|
|
}
|
|
|
|
it("renders all four actions with aria labels in spec order", () => {
|
|
render(
|
|
<ActionStrip
|
|
canPromote
|
|
onPromote={noop}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
const b = buttons();
|
|
expect(b.promote).not.toBeNull();
|
|
expect(b.attach).not.toBeNull();
|
|
expect(b.memory).not.toBeNull();
|
|
expect(b.history).not.toBeNull();
|
|
expect(b.promote.getAttribute("aria-label")).toBe("Promote to project");
|
|
expect(b.attach.getAttribute("aria-label")).toBe("Attach");
|
|
expect(b.memory.getAttribute("aria-label")).toBe("Memory");
|
|
expect(b.history.getAttribute("aria-label")).toBe("History");
|
|
});
|
|
|
|
it("renders inside a toolbar with an accessible name", () => {
|
|
render(
|
|
<ActionStrip
|
|
canPromote={false}
|
|
onPromote={noop}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
const strip = container.querySelector('[data-testid="assistant-action-strip"]');
|
|
expect(strip).not.toBeNull();
|
|
expect(strip?.getAttribute("role")).toBe("toolbar");
|
|
expect(strip?.getAttribute("aria-label")).toBe("Assistant actions");
|
|
});
|
|
|
|
it("disables Promote when canPromote is false", () => {
|
|
render(
|
|
<ActionStrip
|
|
canPromote={false}
|
|
onPromote={noop}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
const b = buttons();
|
|
expect(b.promote.disabled).toBe(true);
|
|
// Other actions remain enabled.
|
|
expect(b.attach.disabled).toBe(false);
|
|
expect(b.memory.disabled).toBe(false);
|
|
expect(b.history.disabled).toBe(false);
|
|
});
|
|
|
|
it("enables Promote when canPromote is true", () => {
|
|
render(
|
|
<ActionStrip
|
|
canPromote
|
|
onPromote={noop}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
expect(buttons().promote.disabled).toBe(false);
|
|
});
|
|
|
|
it("fires the right callback on each click", () => {
|
|
const onPromote = vi.fn();
|
|
const onAttach = vi.fn();
|
|
const onOpenMemory = vi.fn();
|
|
const onOpenHistory = vi.fn();
|
|
|
|
render(
|
|
<ActionStrip
|
|
canPromote
|
|
onPromote={onPromote}
|
|
onAttach={onAttach}
|
|
onOpenMemory={onOpenMemory}
|
|
onOpenHistory={onOpenHistory}
|
|
/>,
|
|
);
|
|
|
|
const b = buttons();
|
|
act(() => {
|
|
b.promote.click();
|
|
b.attach.click();
|
|
b.memory.click();
|
|
b.history.click();
|
|
});
|
|
|
|
expect(onPromote).toHaveBeenCalledTimes(1);
|
|
expect(onAttach).toHaveBeenCalledTimes(1);
|
|
expect(onOpenMemory).toHaveBeenCalledTimes(1);
|
|
expect(onOpenHistory).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not fire Promote when disabled", () => {
|
|
const onPromote = vi.fn();
|
|
render(
|
|
<ActionStrip
|
|
canPromote={false}
|
|
onPromote={onPromote}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
act(() => {
|
|
buttons().promote.click();
|
|
});
|
|
expect(onPromote).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders Promote with the volt outline variant (text-primary class)", () => {
|
|
render(
|
|
<ActionStrip
|
|
canPromote
|
|
onPromote={noop}
|
|
onAttach={noop}
|
|
onOpenMemory={noop}
|
|
onOpenHistory={noop}
|
|
/>,
|
|
);
|
|
const promote = buttons().promote;
|
|
expect(promote.className).toContain("border-primary");
|
|
expect(promote.className).toContain("text-primary");
|
|
});
|
|
});
|