perf(chat): hidden panes stop polling, rows stop re-rendering, a covered terminal stops drawing
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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user