c3f83d3922
The Workspace keeps panes of other workspaces and tabs mounted (display:none), and every one of them kept following the live stream as if shown. - useOnScreen (IntersectionObserver): ConversationView, the outline and the 700 ms menu scan skip while their pane is hidden and catch up once on show. - A new_event refetches the tail at most every 1.5 s (leading + trailing). - MessageList: WorkGroup/MessageRow/EditedFiles compare only their own blocks and results, so a tail append no longer re-renders every row. - A terminal under the chat is moved off the viewport with a transform, so xterm pauses drawing without a resize. - PaneChat and TerminalView are memoized; LaneConsolePane passes a stable onExit. - RunningStep's clock and the outline poll skip unchanged or hidden updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
/**
|
|
* @file useOnScreen.test.tsx
|
|
* @description useOnScreen follows the IntersectionObserver: on screen by
|
|
* default (and always without an observer, as in jsdom), off once the element
|
|
* stops intersecting, and `onShow` runs only on the way back on screen.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { act, render } from "@testing-library/react";
|
|
import { useRef } from "react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { useOnScreen, isShown } from "../useOnScreen";
|
|
|
|
const original = globalThis.IntersectionObserver;
|
|
afterEach(() => {
|
|
globalThis.IntersectionObserver = original;
|
|
});
|
|
|
|
let seen: { current: boolean | null } = { current: null };
|
|
function Probe({ onShow }: { onShow?: () => void }) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
seen = useOnScreen(ref, onShow);
|
|
return <div ref={ref} />;
|
|
}
|
|
|
|
describe("useOnScreen", () => {
|
|
it("counts as on screen without an IntersectionObserver", () => {
|
|
// @ts-expect-error removed for the test
|
|
delete globalThis.IntersectionObserver;
|
|
render(<Probe />);
|
|
expect(seen.current).toBe(true);
|
|
expect(isShown(seen as never)).toBe(true);
|
|
});
|
|
|
|
it("follows the observer and calls onShow only when it comes back", () => {
|
|
let fire: (v: boolean) => void = () => {};
|
|
// @ts-expect-error test stub
|
|
globalThis.IntersectionObserver = class {
|
|
constructor(cb: (e: { isIntersecting: boolean }[]) => void) {
|
|
fire = (v) => cb([{ isIntersecting: v }]);
|
|
}
|
|
observe() {}
|
|
disconnect() {}
|
|
};
|
|
const onShow = vi.fn();
|
|
render(<Probe onShow={onShow} />);
|
|
act(() => fire(true));
|
|
expect(onShow).not.toHaveBeenCalled();
|
|
act(() => fire(false));
|
|
expect(seen.current).toBe(false);
|
|
expect(isShown(seen as never)).toBe(false);
|
|
act(() => fire(true));
|
|
expect(seen.current).toBe(true);
|
|
expect(onShow).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|