106 lines
3.1 KiB
TypeScript
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);
|
|
});
|
|
});
|