Files
semantica/explorer/tests/temporalScrubberBounds.test.ts
T
Wei TaoandSameer Kadam 6b55cf1101 fix(explorer): end the temporal scrubber at now instead of inventing 2030 (#1541)
TimelinePanel fell back to a hardcoded 2030-01-01 whenever
/api/temporal/bounds reported max: null, then placed the playhead at the
midpoint of that fabricated window. session.get_temporal_bounds leaves max
open for any graph whose nodes carry valid_from instants and no
valid_until, so this was the normal response shape rather than bad data:
the header advertised a range no data supported and the workspace's first
/api/temporal/snapshot request asked about a time years ahead of the
present.

The fallback is now a `now` captured once per mount, and defaultTime is
that same `now` clamped into the range, so the initial snapshot describes
the current state. Three settings tuned for the fictional ~60-year window
follow from it: zoomMin drops from a year to a day, the timeAxis/format
pinning to 5-year ticks is removed so vis-timeline picks a granularity for
the real span, and the fixed 6-month play step becomes span/60 with a
one-day floor, which keeps a play-through at roughly 60 frames whether the
graph covers months or a decade.

The bound and step arithmetic moves into temporalScrubberBounds.ts so it
can be asserted directly, alongside the existing temporalLifecycle
predicate tests.

Closes #1536

Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
2026-09-09 17:26:23 +05:30

114 lines
4.2 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
DEFAULT_MIN_DATE,
resolvePlayStepMs,
resolveScrubberBounds,
} from "../src/workspaces/GraphWorkspace/temporalScrubberBounds.ts";
const NOW = new Date("2026-09-09T10:30:00Z");
// ── resolveScrubberBounds ────────────────────────────────────────────────────
test("scrubber bounds: open max ends the window at now, not at a future year", () => {
const { minBound, maxBound } = resolveScrubberBounds({ minDate: "2026-01-15T00:00:00Z", now: NOW });
assert.equal(minBound.toISOString(), "2026-01-15T00:00:00.000Z");
assert.equal(
maxBound.getTime(),
NOW.getTime(),
"a graph carrying only valid_from instants is known up to the present and no further",
);
});
test("scrubber bounds: playhead starts at now so the first snapshot describes the present", () => {
const { defaultTime } = resolveScrubberBounds({ minDate: "2026-01-15T00:00:00Z", now: NOW });
assert.equal(defaultTime.getTime(), NOW.getTime());
});
test("scrubber bounds: playhead is not the midpoint of the range", () => {
const { minBound, maxBound, defaultTime } = resolveScrubberBounds({
minDate: "2020-01-01T00:00:00Z",
maxDate: "2030-01-01T00:00:00Z",
now: NOW,
});
const midpoint = Math.round((minBound.getTime() + maxBound.getTime()) / 2);
assert.notEqual(defaultTime.getTime(), midpoint, "the midpoint was the source of the future start time");
assert.equal(defaultTime.getTime(), NOW.getTime());
});
test("scrubber bounds: reported max is honoured when the data supplies one", () => {
const { maxBound } = resolveScrubberBounds({
minDate: "2020-01-01T00:00:00Z",
maxDate: "2030-06-01T00:00:00Z",
now: NOW,
});
assert.equal(maxBound.toISOString(), "2030-06-01T00:00:00.000Z");
});
test("scrubber bounds: playhead clamps into a range that ends before now", () => {
const { maxBound, defaultTime } = resolveScrubberBounds({
minDate: "2019-01-01T00:00:00Z",
maxDate: "2020-01-01T00:00:00Z",
now: NOW,
});
assert.equal(defaultTime.getTime(), maxBound.getTime());
});
test("scrubber bounds: playhead clamps into a range that starts after now", () => {
const { minBound, defaultTime } = resolveScrubberBounds({
minDate: "2030-01-01T00:00:00Z",
maxDate: "2031-01-01T00:00:00Z",
now: NOW,
});
assert.equal(defaultTime.getTime(), minBound.getTime());
});
test("scrubber bounds: min ahead of an open max keeps the window ordered", () => {
const { minBound, maxBound } = resolveScrubberBounds({ minDate: "2031-01-01T00:00:00Z", now: NOW });
assert.ok(maxBound >= minBound, "vis-timeline requires min <= max");
});
test("scrubber bounds: malformed and missing dates fall back without producing Invalid Date", () => {
const { minBound, maxBound } = resolveScrubberBounds({ minDate: "not-a-date", maxDate: "also-bad", now: NOW });
assert.equal(minBound.getTime(), DEFAULT_MIN_DATE.getTime());
assert.equal(maxBound.getTime(), NOW.getTime());
});
// ── resolvePlayStepMs ────────────────────────────────────────────────────────
test("play step: a one-year span advances in ~60 frames, not 2", () => {
const minBound = new Date("2026-01-01T00:00:00Z");
const maxBound = new Date("2027-01-01T00:00:00Z");
const span = maxBound.getTime() - minBound.getTime();
const frames = span / resolvePlayStepMs(minBound, maxBound);
assert.ok(frames > 50 && frames < 70, `expected ~60 frames, got ${frames}`);
});
test("play step: a decade-long span also advances in ~60 frames", () => {
const minBound = new Date("2016-01-01T00:00:00Z");
const maxBound = new Date("2026-01-01T00:00:00Z");
const span = maxBound.getTime() - minBound.getTime();
const frames = span / resolvePlayStepMs(minBound, maxBound);
assert.ok(frames > 50 && frames < 70, `expected ~60 frames, got ${frames}`);
});
test("play step: a span of hours still advances by at least a day", () => {
const minBound = new Date("2026-09-09T00:00:00Z");
const maxBound = new Date("2026-09-09T06:00:00Z");
assert.equal(resolvePlayStepMs(minBound, maxBound), 1000 * 60 * 60 * 24);
});