feat: add observation chart viewport zoom

This commit is contained in:
梁薄云
2026-08-07 10:05:43 +08:00
parent b7772370dc
commit 8a51629edb
3 changed files with 271 additions and 7 deletions
@@ -234,6 +234,47 @@ function loadApp({ staticSnapshot = makeStaticSnapshot(), dynamicSnapshot = make
return dom;
}
function deepFreeze(value) {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
Object.freeze(value);
Object.values(value).forEach(deepFreeze);
}
return value;
}
function dispatchPointer(target, type, clientX, clientY, options = {}) {
const window = target.ownerDocument.defaultView;
const EventCtor = window.PointerEvent || window.MouseEvent;
target.dispatchEvent(new EventCtor(type, {
bubbles: true,
cancelable: true,
pointerId: 1,
button: 0,
clientX,
clientY,
...options
}));
}
function dispatchWheel(target, clientX, clientY, deltaY) {
const window = target.ownerDocument.defaultView;
const EventCtor = window.WheelEvent || window.MouseEvent;
target.dispatchEvent(new EventCtor("wheel", {
bubbles: true,
cancelable: true,
clientX,
clientY,
deltaY
}));
}
function chartSnapshot(host) {
return {
ticks: [...host.querySelectorAll(".axis-tick-x")].map(tick => tick.textContent).join("|"),
path: host.querySelector("path.chart-data")?.getAttribute("d") || ""
};
}
test("tab switching hides inactive sections despite CSS grid rules", () => {
const dom = loadApp();
const { document, getComputedStyle } = dom.window;
@@ -331,3 +372,97 @@ test("test mode exposes frozen hooks without fetch or SSE", () => {
}
assert.equal("state" in window.__trajectoryVisualizationTestHooks, false);
});
test("wheel and box zoom change ST ticks and path without mutating frozen snapshot", () => {
const dynamicSnapshot = deepFreeze(makeDynamicSnapshot());
const serializedBefore = JSON.stringify(dynamicSnapshot);
const dom = loadApp({ dynamicSnapshot });
const { document } = dom.window;
document.querySelector('button[data-tab="ls-st"]').click();
const st = document.querySelector("#st");
const svg = st.querySelector("svg");
const before = chartSnapshot(st);
dispatchPointer(svg, "pointerdown", 400, 100);
dispatchPointer(svg, "pointermove", 360, 80);
dispatchPointer(svg, "pointerup", 360, 80);
const afterBox = chartSnapshot(st);
assert.notEqual(afterBox.ticks, before.ticks);
assert.notEqual(afterBox.path, before.path);
dispatchWheel(st.querySelector("svg"), 300, 100, -120);
const afterWheel = chartSnapshot(st);
assert.notEqual(afterWheel.ticks, afterBox.ticks);
assert.notEqual(afterWheel.path, afterBox.path);
assert.equal(JSON.stringify(dynamicSnapshot), serializedBefore);
});
test("box zoom ignores drags shorter than 4 CSS pixels", () => {
const dom = loadApp();
const { document } = dom.window;
document.querySelector('button[data-tab="ls-st"]').click();
const st = document.querySelector("#st");
const svg = st.querySelector("svg");
const before = chartSnapshot(st);
dispatchPointer(svg, "pointerdown", 200, 100);
dispatchPointer(svg, "pointermove", 202, 102);
dispatchPointer(svg, "pointerup", 202, 102);
assert.deepEqual(chartSnapshot(st), before);
});
test("reset restores the original chart data domain", () => {
const dom = loadApp();
const { document } = dom.window;
document.querySelector('button[data-tab="ls-st"]').click();
const st = document.querySelector("#st");
const before = chartSnapshot(st);
dispatchWheel(st.querySelector("svg"), 300, 100, -120);
const zoomed = chartSnapshot(st);
assert.notEqual(zoomed.ticks, before.ticks);
st.querySelector('button[data-action="reset"]').click();
assert.deepEqual(chartSnapshot(st), before);
});
test("fullscreen requests only the selected chart host", () => {
const dom = loadApp();
const { document, Element } = dom.window;
const requested = [];
Element.prototype.requestFullscreen = function requestFullscreen() {
requested.push(this.id);
};
document.querySelector('button[data-tab="ls-st"]').click();
const st = document.querySelector("#st");
const ls = document.querySelector("#ls");
st.querySelector('button[data-action="fullscreen"]').click();
assert.deepEqual(requested, ["st"]);
assert.equal(st.classList.contains("is-fullscreen"), true);
assert.equal(ls.classList.contains("is-fullscreen"), false);
});
test("new SSE frame keeps an active viewport and leaves its snapshot unchanged", () => {
const first = deepFreeze(makeDynamicSnapshot());
const dom = loadApp({ dynamicSnapshot: first });
const { document } = dom.window;
document.querySelector('button[data-tab="ls-st"]').click();
const st = document.querySelector("#st");
dispatchWheel(st.querySelector("svg"), 300, 100, -120);
const zoomed = chartSnapshot(st);
const second = JSON.parse(JSON.stringify(first));
second.sequence = 2;
deepFreeze(second);
const serializedSecond = JSON.stringify(second);
dom.window.__trajectoryVisualizationTestHooks.receiveFrame({ snapshot: second, history: [] });
assert.deepEqual(chartSnapshot(st), zoomed);
assert.equal(JSON.stringify(second), serializedSecond);
});