Files
kandev/apps/web/components/task/chat/transcript-viewport-resize.test.ts
T

106 lines
3.1 KiB
TypeScript

import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useTranscriptViewportResize } from "./transcript-viewport-resize";
const observers = new Set<() => void>();
beforeEach(() => {
vi.stubGlobal(
"ResizeObserver",
class {
constructor(private callback: () => void) {}
observe() {
observers.add(this.callback);
}
disconnect() {
observers.delete(this.callback);
}
},
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
observers.clear();
});
function setup(
options: { enabled?: boolean; pending?: boolean; locked?: boolean; visible?: boolean } = {},
) {
const element = document.createElement("div");
const size = { width: 360, height: 500, content: 2000 };
Object.defineProperties(element, {
clientHeight: { get: () => size.height },
clientWidth: { get: () => size.width },
scrollHeight: { get: () => size.content },
});
element.scrollTop = 1500;
const hook = renderHook(
(props) =>
useTranscriptViewportResize({
scrollRef: { current: element },
sessionId: "a",
enabled: props.enabled ?? true,
isVisible: props.visible ?? true,
initialPlacementPending: props.pending ?? false,
isProgrammaticScrollLocked: () => props.locked ?? false,
}),
{ initialProps: options },
);
return {
...hook,
element,
size,
resize: () => act(() => observers.forEach((publish) => publish())),
};
}
describe("native transcript viewport allocation", () => {
// @covers AC-UI-THREADS-DECK-005.6
it("follows the bottom when composer disclosure shrinks the viewport", () => {
const { element, size, resize } = setup();
size.height = 300;
resize();
expect(element.scrollTop).toBe(1700);
});
it("keeps the prior bottom snapshot across a resize-generated scroll event", () => {
const { element, size, resize } = setup();
size.height = 300;
element.dispatchEvent(new Event("scroll"));
resize();
expect(element.scrollTop).toBe(1700);
});
it("preserves the reader after a real scroll into history", () => {
const { element, size, resize } = setup();
element.scrollTop = 600;
element.dispatchEvent(new Event("scroll"));
size.height = 300;
resize();
expect(element.scrollTop).toBe(600);
size.height = 500;
resize();
expect(element.scrollTop).toBe(600);
});
it.each([{ enabled: false }, { pending: true }, { locked: true }, { visible: false }])(
"respects frozen, initial placement, programmatic navigation, and hidden ownership: %j",
(options) => {
const { element, size, resize } = setup(options);
size.height = 300;
resize();
expect(element.scrollTop).toBe(1500);
},
);
it("leaves width reflow to existing message placement and releases observation", () => {
const { element, size, resize, unmount } = setup();
size.width = 500;
size.height = 300;
resize();
expect(element.scrollTop).toBe(1500);
unmount();
expect(observers.size).toBe(0);
});
});