The pane waited 150 ms on hover and 60 ms on a keyboard step before following the sidebar highlight, so every preview paid for the fast walk it might have been. Replace both with a leading-edge 120 ms gap: the first move shows at once, a burst is coalesced to its start and its end. The row's own hover no longer eases either — `background` was on a 120 ms transition, which is what read as lag on the row itself. The sidebar also rebuilt its whole tree on every `SessionUpdated`, a couple of times a minute per live session, dropping `:hover` from the row under the pointer. Refreshes that change nothing structural now patch the activity rail and meta line into the standing rows instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { createPreviewSchedule, PREVIEW_MIN_GAP_MS } from "./previewSchedule.js";
|
|
|
|
/** Hand-driven clock + timer queue, so the schedule is asserted without waiting. */
|
|
function harness() {
|
|
let now = 1_000;
|
|
const timers = new Map<number, { at: number; fn: () => void }>();
|
|
let nextHandle = 1;
|
|
const emitted: (string | null)[] = [];
|
|
|
|
const schedule = createPreviewSchedule((id) => emitted.push(id), {
|
|
now: () => now,
|
|
setTimer: (fn, ms) => {
|
|
const h = nextHandle++;
|
|
timers.set(h, { at: now + ms, fn });
|
|
return h;
|
|
},
|
|
clearTimer: (h) => {
|
|
timers.delete(h);
|
|
},
|
|
});
|
|
|
|
/** Advance the clock, firing any timer that comes due. */
|
|
const advance = (ms: number) => {
|
|
now += ms;
|
|
for (const [h, t] of [...timers]) {
|
|
if (t.at <= now) {
|
|
timers.delete(h);
|
|
t.fn();
|
|
}
|
|
}
|
|
};
|
|
|
|
return { schedule, emitted, advance };
|
|
}
|
|
|
|
describe("preview schedule", () => {
|
|
it("shows the first preview immediately", () => {
|
|
const { schedule, emitted } = harness();
|
|
schedule("s1");
|
|
expect(emitted).toEqual(["s1"]);
|
|
});
|
|
|
|
it("coalesces a burst into one trailing preview", () => {
|
|
const { schedule, emitted, advance } = harness();
|
|
// A pointer sweep across the list, or j held down: every row in between
|
|
// is walked well inside one gap.
|
|
schedule("s1");
|
|
schedule("s2");
|
|
schedule("s3");
|
|
expect(emitted).toEqual(["s1"]);
|
|
advance(PREVIEW_MIN_GAP_MS);
|
|
expect(emitted).toEqual(["s1", "s3"]);
|
|
});
|
|
|
|
it("shows the next preview immediately once the gap has passed", () => {
|
|
const { schedule, emitted, advance } = harness();
|
|
schedule("s1");
|
|
advance(PREVIEW_MIN_GAP_MS);
|
|
schedule("s2");
|
|
expect(emitted).toEqual(["s1", "s2"]);
|
|
});
|
|
|
|
it("drops a superseded trailing preview rather than replaying it", () => {
|
|
const { schedule, emitted, advance } = harness();
|
|
schedule("s1");
|
|
schedule("s2");
|
|
schedule("s1"); // back where we started inside the same gap
|
|
advance(PREVIEW_MIN_GAP_MS);
|
|
// s1 is already on screen, so the trailing edge has nothing to do.
|
|
expect(emitted).toEqual(["s1"]);
|
|
});
|
|
|
|
it("clears through the same gap, so leaving and re-entering does not blink", () => {
|
|
const { schedule, emitted, advance } = harness();
|
|
schedule("s1");
|
|
schedule(null); // pointer crosses the gap between two rows
|
|
schedule("s2");
|
|
advance(PREVIEW_MIN_GAP_MS);
|
|
expect(emitted).toEqual(["s1", "s2"]);
|
|
});
|
|
|
|
it("cancel drops a pending trailing preview", () => {
|
|
const { schedule, emitted, advance } = harness();
|
|
schedule("s1");
|
|
schedule("s2");
|
|
schedule.cancel();
|
|
advance(PREVIEW_MIN_GAP_MS);
|
|
expect(emitted).toEqual(["s1"]);
|
|
});
|
|
});
|