test(webui): dedupe prompt rail viewport setup

This commit is contained in:
chengyongru 2026-07-07 16:43:29 +08:00 committed by Xubin Ren
parent 4333d6f103
commit 815f15993c

View File

@ -150,6 +150,63 @@ function stubElementRect(element: HTMLElement, left: number, width: number, heig
element.getBoundingClientRect = () => elementRect(left, width, height); element.getBoundingClientRect = () => elementRect(left, width, height);
} }
interface PromptRailLayoutStub {
scrollerWidth: number;
messageColumnLeft: number;
messageColumnWidth: number;
}
async function renderPromptRailViewport({
layout,
scrollTo,
}: {
layout?: PromptRailLayoutStub;
scrollTo?: (options?: ScrollToOptions) => void;
} = {}) {
const promptMessages = makePromptExchangeMessages(5);
const { container } = render(
<ThreadViewport
messages={promptMessages}
isStreaming={false}
composer={<div />}
/>,
);
const scroller = container.firstElementChild?.firstElementChild as HTMLElement;
Object.defineProperties(scroller, {
scrollHeight: { configurable: true, value: 1800 },
clientHeight: { configurable: true, value: 600 },
scrollTop: { configurable: true, value: 0 },
...(scrollTo ? { scrollTo: { configurable: true, value: scrollTo } } : {}),
});
if (layout) {
stubElementRect(scroller, 0, layout.scrollerWidth);
stubElementRect(
screen.getByTestId("thread-message-column"),
layout.messageColumnLeft,
layout.messageColumnWidth,
);
}
const promptEls = Array.from(
container.querySelectorAll<HTMLElement>("[data-user-prompt-id]"),
);
promptEls.forEach((el, index) => {
Object.defineProperty(el, "offsetTop", {
configurable: true,
value: index * 360,
});
});
await act(async () => {
window.dispatchEvent(new Event("resize"));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
});
return { promptEls };
}
function ViewportWithPromptNavigator({ messages }: { messages: UIMessage[] }) { function ViewportWithPromptNavigator({ messages }: { messages: UIMessage[] }) {
const viewportRef = useRef<ThreadViewportHandle | null>(null); const viewportRef = useRef<ThreadViewportHandle | null>(null);
return ( return (
@ -756,39 +813,10 @@ describe("ThreadViewport", () => {
}); });
it("renders a prompt rail that jumps to user messages", async () => { it("renders a prompt rail that jumps to user messages", async () => {
const promptMessages = makePromptExchangeMessages(5);
const { container } = render(
<ThreadViewport
messages={promptMessages}
isStreaming={false}
composer={<div />}
/>,
);
const scroller = container.firstElementChild?.firstElementChild as HTMLElement;
const scrollTo = vi.fn(); const scrollTo = vi.fn();
Object.defineProperties(scroller, { const { promptEls } = await renderPromptRailViewport({ scrollTo });
scrollHeight: { configurable: true, value: 1800 },
clientHeight: { configurable: true, value: 600 },
scrollTop: { configurable: true, value: 0 },
scrollTo: { configurable: true, value: scrollTo },
});
const promptEls = Array.from(
container.querySelectorAll<HTMLElement>("[data-user-prompt-id]"),
);
expect(promptEls).toHaveLength(5); expect(promptEls).toHaveLength(5);
promptEls.forEach((el, index) => {
Object.defineProperty(el, "offsetTop", {
configurable: true,
value: index * 360,
});
});
await act(async () => {
window.dispatchEvent(new Event("resize"));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
});
expect(screen.getByLabelText("User prompt navigation")).toBeInTheDocument(); expect(screen.getByLabelText("User prompt navigation")).toBeInTheDocument();
const promptMarkers = screen.getAllByRole("button", { name: /Jump to prompt:/ }); const promptMarkers = screen.getAllByRole("button", { name: /Jump to prompt:/ });
@ -826,74 +854,24 @@ describe("ThreadViewport", () => {
}); });
it("positions the prompt rail in the gutter before the message column", async () => { it("positions the prompt rail in the gutter before the message column", async () => {
const promptMessages = makePromptExchangeMessages(5); await renderPromptRailViewport({
const { container } = render( layout: {
<ThreadViewport scrollerWidth: 1200,
messages={promptMessages} messageColumnLeft: 180,
isStreaming={false} messageColumnWidth: 792,
composer={<div />} },
/>,
);
const scroller = container.firstElementChild?.firstElementChild as HTMLElement;
Object.defineProperties(scroller, {
scrollHeight: { configurable: true, value: 1800 },
clientHeight: { configurable: true, value: 600 },
scrollTop: { configurable: true, value: 0 },
});
stubElementRect(scroller, 0, 1200);
stubElementRect(screen.getByTestId("thread-message-column"), 180, 792);
const promptEls = Array.from(
container.querySelectorAll<HTMLElement>("[data-user-prompt-id]"),
);
promptEls.forEach((el, index) => {
Object.defineProperty(el, "offsetTop", {
configurable: true,
value: index * 360,
});
});
await act(async () => {
window.dispatchEvent(new Event("resize"));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
}); });
expect(screen.getByLabelText("User prompt navigation")).toHaveStyle({ left: "124px" }); expect(screen.getByLabelText("User prompt navigation")).toHaveStyle({ left: "124px" });
}); });
it("hides the prompt rail when the message column leaves no side gutter", async () => { it("hides the prompt rail when the message column leaves no side gutter", async () => {
const promptMessages = makePromptExchangeMessages(5); await renderPromptRailViewport({
const { container } = render( layout: {
<ThreadViewport scrollerWidth: 560,
messages={promptMessages} messageColumnLeft: 48,
isStreaming={false} messageColumnWidth: 480,
composer={<div />} },
/>,
);
const scroller = container.firstElementChild?.firstElementChild as HTMLElement;
Object.defineProperties(scroller, {
scrollHeight: { configurable: true, value: 1800 },
clientHeight: { configurable: true, value: 600 },
scrollTop: { configurable: true, value: 0 },
});
stubElementRect(scroller, 0, 560);
stubElementRect(screen.getByTestId("thread-message-column"), 48, 480);
const promptEls = Array.from(
container.querySelectorAll<HTMLElement>("[data-user-prompt-id]"),
);
promptEls.forEach((el, index) => {
Object.defineProperty(el, "offsetTop", {
configurable: true,
value: index * 360,
});
});
await act(async () => {
window.dispatchEvent(new Event("resize"));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
}); });
expect(screen.queryByLabelText("User prompt navigation")).not.toBeInTheDocument(); expect(screen.queryByLabelText("User prompt navigation")).not.toBeInTheDocument();