test(commands): cover viewport command flow
This commit is contained in:
18
commands/dispatcher.test.ts
Normal file
18
commands/dispatcher.test.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { createInitialAppState } from "@editor/initial-state";
|
||||||
|
import { createAppStore } from "@editor/store";
|
||||||
|
import { createCommandRegistry } from "./registry";
|
||||||
|
import { viewportPanCommand } from "./viewport";
|
||||||
|
|
||||||
|
describe("command dispatcher", () => {
|
||||||
|
test("throws for unknown commands", () => {
|
||||||
|
const store = createAppStore(createInitialAppState("Test"), createCommandRegistry([]));
|
||||||
|
expect(() => store.dispatch("missing", undefined)).toThrow("Unknown command: missing");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatch applies command result to store", () => {
|
||||||
|
const store = createAppStore(createInitialAppState("Test"), createCommandRegistry([viewportPanCommand]));
|
||||||
|
store.dispatch("viewport.pan", { delta: { x: 3, y: 7 } });
|
||||||
|
expect(store.getState().editor.viewport.center).toEqual({ x: 3, y: 7 });
|
||||||
|
});
|
||||||
|
});
|
||||||
44
commands/viewport.test.ts
Normal file
44
commands/viewport.test.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { createInitialAppState } from "@editor/initial-state";
|
||||||
|
import {
|
||||||
|
viewportPanCommand,
|
||||||
|
viewportResetCommand,
|
||||||
|
viewportSetSizeCommand,
|
||||||
|
viewportSetZoomCommand,
|
||||||
|
viewportZoomAroundPointCommand,
|
||||||
|
} from "./viewport";
|
||||||
|
|
||||||
|
const context = () => ({ state: createInitialAppState("Test") });
|
||||||
|
|
||||||
|
describe("viewport commands", () => {
|
||||||
|
test("pans viewport center", () => {
|
||||||
|
const next = viewportPanCommand.execute(context(), { delta: { x: 10, y: -5 } });
|
||||||
|
expect(next.editor.viewport.center).toEqual({ x: 10, y: -5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets zoom with minimum clamp", () => {
|
||||||
|
const next = viewportSetZoomCommand.execute(context(), { zoom: -1 });
|
||||||
|
expect(next.editor.viewport.zoom).toBe(0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets non-negative viewport size", () => {
|
||||||
|
const next = viewportSetSizeCommand.execute(context(), { w: 800, h: -1 });
|
||||||
|
expect(next.editor.viewport.size).toEqual({ w: 800, h: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("zooms around a viewport point", () => {
|
||||||
|
const state = viewportSetSizeCommand.execute(context(), { w: 100, h: 100 });
|
||||||
|
const next = viewportZoomAroundPointCommand.execute({ state }, { zoom: 2, point: { x: 100, y: 50 } });
|
||||||
|
|
||||||
|
expect(next.editor.viewport.zoom).toBe(2);
|
||||||
|
expect(next.editor.viewport.center).toEqual({ x: 25, y: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resets viewport but preserves size", () => {
|
||||||
|
const sized = viewportSetSizeCommand.execute(context(), { w: 640, h: 480 });
|
||||||
|
const panned = viewportPanCommand.execute({ state: sized }, { delta: { x: 4, y: 8 } });
|
||||||
|
const next = viewportResetCommand.execute({ state: panned }, undefined);
|
||||||
|
|
||||||
|
expect(next.editor.viewport).toEqual({ center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: { w: 640, h: 480 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
68
input/viewport.test.ts
Normal file
68
input/viewport.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { createViewportPointerPanHandler, handleViewportWheel } from "./viewport";
|
||||||
|
|
||||||
|
const ignoredState = undefined as never;
|
||||||
|
|
||||||
|
describe("viewport input", () => {
|
||||||
|
test("wheel dispatches zoom around point", () => {
|
||||||
|
const dispatched: unknown[] = [];
|
||||||
|
const consumed = handleViewportWheel({
|
||||||
|
event: { position: { x: 10, y: 20 }, delta: { x: 0, y: -100 }, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false },
|
||||||
|
globalConsumer: () => false,
|
||||||
|
currentZoom: 1,
|
||||||
|
dispatch: (commandId, payload) => {
|
||||||
|
dispatched.push({ commandId, payload });
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
expect(dispatched[0]).toEqual({ commandId: "viewport.zoomAroundPoint", payload: { zoom: Math.exp(0.1), point: { x: 10, y: 20 } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("global wheel consumer prevents command dispatch", () => {
|
||||||
|
let called = false;
|
||||||
|
const consumed = handleViewportWheel({
|
||||||
|
event: { position: { x: 0, y: 0 }, delta: { x: 0, y: 1 }, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false },
|
||||||
|
globalConsumer: () => true,
|
||||||
|
currentZoom: 1,
|
||||||
|
dispatch: () => {
|
||||||
|
called = true;
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
expect(called).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("middle mouse drag dispatches viewport pan", () => {
|
||||||
|
const dispatched: unknown[] = [];
|
||||||
|
const pan = createViewportPointerPanHandler({
|
||||||
|
globalConsumer: () => false,
|
||||||
|
getCurrentZoom: () => 2,
|
||||||
|
dispatch: (commandId, payload) => {
|
||||||
|
dispatched.push({ commandId, payload });
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pan.pointerDown(basePointer({ buttons: 4, position: { x: 10, y: 10 } }))).toBe(true);
|
||||||
|
expect(pan.pointerMove(basePointer({ buttons: 4, position: { x: 14, y: 6 } }))).toBe(true);
|
||||||
|
expect(dispatched[0]).toEqual({ commandId: "viewport.pan", payload: { delta: { x: -2, y: 2 } } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function basePointer(overrides: Partial<Parameters<ReturnType<typeof createViewportPointerPanHandler>["pointerDown"]>[0]> = {}) {
|
||||||
|
return {
|
||||||
|
pointerId: 1,
|
||||||
|
pointerType: "mouse" as const,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
buttons: 0,
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "bun --hot index.ts",
|
"dev": "bun --hot index.ts",
|
||||||
"start": "NODE_ENV=production bun index.ts",
|
"start": "NODE_ENV=production bun index.ts",
|
||||||
"build": "bun run build.ts",
|
"build": "bun run build.ts",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user