nexus/ui/src/components/assistant/ActionStrip.test.tsx
Nexus Dev cd75772a6a feat(nexus): add ActionStrip for assistant actions (phase 9)
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>
2026-04-11 12:17:46 +00:00

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");
});
});