feat: add observation chart viewport zoom
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user