// The shared orbit primitive's bookkeeping. // // Real GeometryInstance objects — a plain Cesium class that constructs fine in // the node env. What is faked is the viewer: a clock whose ticks this test drives // by hand, which is the only way to observe the coalescing window and the // settled() promise deterministically. // // What is not covered is the build itself. Driving a Primitive through its // creation states calls into Cesium's renderer, which needs a real WebGL context, // so the cases here stop at the point a build starts and pick up again on the // paths that never reach one. That still leaves the scheduling, the coalescing, // the empty-batch teardown and settled(). import { ArcType, Cartesian3, Color, ColorGeometryInstanceAttribute, GeometryInstance, JulianDate, PolylineColorAppearance, PolylineGeometry, SceneMode } from "@cesium/engine"; import type { Viewer } from "@cesium/widgets"; import { describe, expect, test } from "vitest"; import { PolylineBatch } from "./PolylineBatch"; function orbitGeometry(id: string): GeometryInstance { return new GeometryInstance({ geometry: new PolylineGeometry({ positions: Cartesian3.fromDegreesArray([0, 0, 10, 10]), width: 2, arcType: ArcType.NONE, vertexFormat: PolylineColorAppearance.VERTEX_FORMAT, }), attributes: { color: ColorGeometryInstanceAttribute.fromColor(new Color(1, 1, 1, 0.15)) }, id, }); } /** A viewer with a hand-cranked clock and a primitive collection that records. */ function fakeViewer() { const listeners = new Set<() => void>(); const primitives = { added: [] as unknown[], removed: [] as unknown[], add(primitive: unknown) { this.added.push(primitive); }, remove(primitive: unknown) { this.removed.push(primitive); return true; }, }; let renders = 0; const viewer = { clock: { currentTime: JulianDate.fromIso8601("2026-01-01T00:00:00Z"), onTick: { addEventListener(listener: () => void) { listeners.add(listener); return () => listeners.delete(listener); }, }, }, scene: { mode: SceneMode.SCENE3D, primitives, frameState: {}, requestRender() { renders += 1; }, }, }; return { viewer: viewer as unknown as Viewer, primitives, get renders() { return renders; }, get listenerCount() { return listeners.size; }, tick(times = 1) { for (let i = 0; i < times; i += 1) { [...listeners].forEach((listener) => listener()); } }, }; } // Mirrors PolylineBatch's own window; the callback fires on the tick after it. const COALESCE_TICKS = 30; const PAST_WINDOW = COALESCE_TICKS + 2; describe("PolylineBatch", () => { test("nothing added, nothing pending, settled resolves at once", async () => { const { viewer } = fakeViewer(); const batch = new PolylineBatch(viewer); expect(batch.pending).toBe(false); await expect(batch.settled()).resolves.toBeUndefined(); }); test("an add is pending, and nothing reaches the scene before the window closes", () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer); batch.add(orbitGeometry("ISS")); expect(batch.pending).toBe(true); // Ticking short of the window must not start the build. host.tick(COALESCE_TICKS - 1); expect(host.primitives.added).toHaveLength(0); expect(batch.pending).toBe(true); }); test("many adds in one window cost one rebuild", () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer); const before = host.listenerCount; for (let i = 0; i < 50; i += 1) { batch.add(orbitGeometry(`SAT-${i}`)); } // One scheduled rebuild, not fifty: the second add sees `#scheduled` and // returns rather than registering another tick callback. expect(host.listenerCount).toBe(before + 1); }); test("removing the last geometry clears the batch and settles", async () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer); const geometry = orbitGeometry("ISS"); batch.add(geometry); batch.remove(geometry); expect(batch.pending).toBe(true); const settled = batch.settled(); host.tick(PAST_WINDOW); await expect(settled).resolves.toBeUndefined(); expect(batch.pending).toBe(false); }); test("the empty rebuild stops its own tick callback", () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer); // The permanent inertial-frame updater registered by the constructor. const permanent = host.listenerCount; const geometry = orbitGeometry("ISS"); batch.add(geometry); batch.remove(geometry); host.tick(PAST_WINDOW); // The rebuild callback used to `return` out of the empty branch without // removing itself, leaving a listener requesting a render every 30 ticks for // the rest of the session — and another one on every subsequent empty pass. expect(host.listenerCount).toBe(permanent); }); test("settled() called when idle does not wait for a tick", async () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer); const geometry = orbitGeometry("ISS"); batch.add(geometry); batch.remove(geometry); host.tick(PAST_WINDOW); await batch.settled(); // Resolves without anything having to tick again. await expect(batch.settled()).resolves.toBeUndefined(); }); test("replace swaps a member's geometry in place and schedules one rebuild", () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer, "fixed"); const first = orbitGeometry("ISS"); const second = orbitGeometry("ISS"); batch.add(first); expect(batch.replace(first, second)).toBe(true); // A swap is not a membership change: the batch is still one geometry long. expect(batch.size).toBe(1); expect(batch.pending).toBe(true); }); test("replace refuses a geometry that is no longer a member", () => { const host = fakeViewer(); const batch = new PolylineBatch(host.viewer, "fixed"); const stale = orbitGeometry("ISS"); // The satellite disabled its track between the refresh being scheduled and // it running: the caller has to be told, or the batch grows a line for a // component nobody is drawing. expect(batch.replace(stale, orbitGeometry("ISS"))).toBe(false); expect(batch.size).toBe(0); }); test("only the inertial batch installs a periodic re-orientation", () => { const host = fakeViewer(); const before = host.listenerCount; // The inertial batch keeps a permanent listener to spin its model matrix. const inertial = new PolylineBatch(host.viewer, "inertial"); expect(inertial.size).toBe(0); expect(host.listenerCount).toBe(before + 1); // An Earth-relative one is already in the frame it is drawn in, so there is // no matrix for a listener to maintain. const fixed = new PolylineBatch(host.viewer, "fixed"); expect(fixed.size).toBe(0); expect(host.listenerCount).toBe(before + 1); }); });