feat: polish trace delivery and slash menu UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ThreadComposer } from "@/components/thread/ThreadComposer";
|
||||
import type { SlashCommand } from "@/lib/types";
|
||||
@@ -19,6 +19,33 @@ const COMMANDS: SlashCommand[] = [
|
||||
argHint: "[n]",
|
||||
},
|
||||
];
|
||||
const ORIGINAL_INNER_HEIGHT = window.innerHeight;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
Object.defineProperty(window, "innerHeight", {
|
||||
value: ORIGINAL_INNER_HEIGHT,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
function rect(init: Partial<DOMRect>): DOMRect {
|
||||
const top = init.top ?? 0;
|
||||
const left = init.left ?? 0;
|
||||
const width = init.width ?? 0;
|
||||
const height = init.height ?? 0;
|
||||
return {
|
||||
x: init.x ?? left,
|
||||
y: init.y ?? top,
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
height,
|
||||
right: init.right ?? left + width,
|
||||
bottom: init.bottom ?? top + height,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("ThreadComposer", () => {
|
||||
it("renders a readonly hero model composer when provided", () => {
|
||||
@@ -74,7 +101,9 @@ describe("ThreadComposer", () => {
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, { target: { value: "/" } });
|
||||
|
||||
expect(screen.getByRole("listbox", { name: "Slash commands" })).toBeInTheDocument();
|
||||
const palette = screen.getByRole("listbox", { name: "Slash commands" });
|
||||
expect(palette).toBeInTheDocument();
|
||||
expect(palette).toHaveStyle({ maxHeight: "288px" });
|
||||
expect(screen.getByRole("option", { name: /\/stop/i })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
@@ -92,6 +121,55 @@ describe("ThreadComposer", () => {
|
||||
expect(screen.queryByRole("listbox", { name: "Slash commands" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the slash command palette downward when there is more room below", async () => {
|
||||
vi.spyOn(HTMLFormElement.prototype, "getBoundingClientRect").mockReturnValue(
|
||||
rect({ top: 40, bottom: 160, width: 800, height: 120 }),
|
||||
);
|
||||
Object.defineProperty(window, "innerHeight", {
|
||||
value: 330,
|
||||
configurable: true,
|
||||
});
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Ask anything..."
|
||||
slashCommands={COMMANDS}
|
||||
variant="hero"
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText("Message input");
|
||||
|
||||
fireEvent.change(input, { target: { value: "/" } });
|
||||
|
||||
await waitFor(() => {
|
||||
const palette = screen.getByRole("listbox", { name: "Slash commands" });
|
||||
expect(palette.className).toContain("top-full");
|
||||
expect(palette).toHaveStyle({ maxHeight: "162px" });
|
||||
});
|
||||
});
|
||||
|
||||
it("dismisses the slash command palette on outside click", () => {
|
||||
render(
|
||||
<div>
|
||||
<button type="button">outside</button>
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Type your message..."
|
||||
slashCommands={COMMANDS}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Message input"), {
|
||||
target: { value: "/" },
|
||||
});
|
||||
expect(screen.getByRole("listbox", { name: "Slash commands" })).toBeInTheDocument();
|
||||
|
||||
fireEvent.pointerDown(screen.getByRole("button", { name: "outside" }));
|
||||
|
||||
expect(screen.queryByRole("listbox", { name: "Slash commands" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends image generation mode with automatic aspect ratio", () => {
|
||||
const onSend = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -573,7 +573,7 @@ describe("ThreadShell", () => {
|
||||
await waitFor(() => expect(screen.getByText("live assistant reply")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("does not open slash commands on the blank welcome page", async () => {
|
||||
it("opens slash commands on the blank welcome page", async () => {
|
||||
const client = makeClient();
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -583,10 +583,11 @@ describe("ThreadShell", () => {
|
||||
return httpJson({
|
||||
commands: [
|
||||
{
|
||||
command: "/stop",
|
||||
title: "Stop current task",
|
||||
description: "Cancel the active agent turn.",
|
||||
icon: "square",
|
||||
command: "/history",
|
||||
title: "Show conversation history",
|
||||
description: "Print the last N persisted messages.",
|
||||
icon: "history",
|
||||
arg_hint: "[n]",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -622,7 +623,8 @@ describe("ThreadShell", () => {
|
||||
target: { value: "/" },
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("listbox", { name: "Slash commands" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("listbox", { name: "Slash commands" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: /\/history/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches welcome quick actions when image mode is enabled", async () => {
|
||||
|
||||
Reference in New Issue
Block a user