142 lines
5.9 KiB
TypeScript
142 lines
5.9 KiB
TypeScript
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { beforeEach } from "vitest";
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
import { CentralStatus } from "./CentralStatus";
|
|
|
|
beforeEach(() => useSessionStore.getState().resetSession());
|
|
|
|
function setScrollGeometry(
|
|
element: HTMLElement,
|
|
values: { scrollHeight: number; clientHeight: number; scrollTop: number },
|
|
) {
|
|
Object.defineProperties(element, {
|
|
scrollHeight: { configurable: true, value: values.scrollHeight },
|
|
clientHeight: { configurable: true, value: values.clientHeight },
|
|
scrollTop: { configurable: true, writable: true, value: values.scrollTop },
|
|
});
|
|
}
|
|
|
|
test("renders only the chronological assistant transcript log while working", () => {
|
|
useSessionStore.setState({
|
|
lastUserEntry: { kind: "input", text: "how many patients?" },
|
|
stepMessages: [{ level: "warning", text: "Searching the schema…" }],
|
|
transcript: [
|
|
{ role: "assistant", text: "First line\n\nSecond line" },
|
|
{ role: "assistant", text: "Third line" },
|
|
],
|
|
});
|
|
|
|
render(<CentralStatus working />);
|
|
|
|
const log = screen.getByRole("log", { name: "Live model activity" });
|
|
expect(log).toHaveTextContent("First line");
|
|
expect(log).toHaveTextContent("Second line");
|
|
expect(log).toHaveTextContent("Third line");
|
|
expect(
|
|
within(log).getAllByRole("listitem").map((row) => row.lastElementChild?.textContent),
|
|
).toEqual(["First line", "Second line", "Third line"]);
|
|
expect(log).toHaveClass("max-h-28", "overflow-y-auto");
|
|
expect(screen.queryByText("how many patients?")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("Searching the schema…")).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("wraps live log lines instead of truncating them", () => {
|
|
useSessionStore.setState({
|
|
transcript: [{ role: "assistant", text: "a_very_long_model_activity_line" }],
|
|
});
|
|
render(<CentralStatus working />);
|
|
expect(screen.getByText("a_very_long_model_activity_line")).toHaveClass(
|
|
"whitespace-pre-wrap",
|
|
"break-words",
|
|
);
|
|
expect(screen.getByText("a_very_long_model_activity_line")).not.toHaveClass("truncate");
|
|
});
|
|
|
|
test("uses accessible muted text for historical live-log rows", () => {
|
|
useSessionStore.setState({
|
|
transcript: [
|
|
{ role: "assistant", text: "Earlier activity" },
|
|
{ role: "assistant", text: "Current activity" },
|
|
],
|
|
});
|
|
render(<CentralStatus working />);
|
|
|
|
const historicalRow = screen.getByText("Earlier activity").closest("li");
|
|
expect(historicalRow).not.toBeNull();
|
|
expect(historicalRow).toHaveClass("text-muted-foreground");
|
|
expect(historicalRow).not.toHaveClass("text-muted-foreground/55");
|
|
});
|
|
|
|
test.each([
|
|
{ working: false, transcript: [{ role: "assistant" as const, text: "hidden" }] },
|
|
{ working: true, transcript: [] },
|
|
{ working: true, transcript: [{ role: "assistant" as const, text: " \n " }] },
|
|
])("renders nothing without active non-blank transcript content", ({ working, transcript }) => {
|
|
useSessionStore.setState({ transcript });
|
|
const { container } = render(<CentralStatus working={working} />);
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
test("bottom-follows appended transcript text only while near the bottom", () => {
|
|
useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] });
|
|
render(<CentralStatus working />);
|
|
const log = screen.getByRole("log", { name: "Live model activity" });
|
|
setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 });
|
|
|
|
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" }));
|
|
|
|
expect(log.scrollTop).toBe(400);
|
|
});
|
|
|
|
test("preserves manual live-log scroll position away from the bottom", () => {
|
|
useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] });
|
|
render(<CentralStatus working />);
|
|
const log = screen.getByRole("log", { name: "Live model activity" });
|
|
setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 });
|
|
fireEvent.scroll(log);
|
|
|
|
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" }));
|
|
|
|
expect(log.scrollTop).toBe(100);
|
|
});
|
|
|
|
test("resets bottom-follow when the live log resumes after becoming inactive", () => {
|
|
useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] });
|
|
const { rerender } = render(<CentralStatus working />);
|
|
const log = screen.getByRole("log", { name: "Live model activity" });
|
|
setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 });
|
|
fireEvent.scroll(log);
|
|
|
|
rerender(<CentralStatus working={false} />);
|
|
expect(screen.queryByRole("log", { name: "Live model activity" })).not.toBeInTheDocument();
|
|
|
|
const scrollProperties = ["scrollHeight", "clientHeight", "scrollTop"] as const;
|
|
const originalDescriptors = scrollProperties.map((property) =>
|
|
Object.getOwnPropertyDescriptor(HTMLElement.prototype, property),
|
|
);
|
|
Object.defineProperties(HTMLElement.prototype, {
|
|
scrollHeight: { configurable: true, value: 400 },
|
|
clientHeight: { configurable: true, value: 100 },
|
|
scrollTop: { configurable: true, writable: true, value: 0 },
|
|
});
|
|
|
|
try {
|
|
rerender(<CentralStatus working />);
|
|
const resumedLog = screen.getByRole("log", { name: "Live model activity" });
|
|
expect(resumedLog.scrollTop).toBe(400);
|
|
|
|
Object.defineProperty(resumedLog, "scrollHeight", { configurable: true, value: 500 });
|
|
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" }));
|
|
|
|
expect(resumedLog.scrollTop).toBe(500);
|
|
} finally {
|
|
scrollProperties.forEach((property, index) => {
|
|
const descriptor = originalDescriptors[index];
|
|
if (descriptor) Object.defineProperty(HTMLElement.prototype, property, descriptor);
|
|
else Reflect.deleteProperty(HTMLElement.prototype, property);
|
|
});
|
|
}
|
|
});
|