From cc180d305adf0d557475fd07dad2f6c812b75f20 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 31 Mar 2026 02:06:44 +0200 Subject: [PATCH] Add tests for box brush commands, E2E, geometry, and update scene document serialization test --- tests/domain/create-box-brush.command.test.ts | 120 ++++++++++++++++++ tests/e2e/box-brush-authoring.e2e.ts | 35 +++++ tests/geometry/box-brush-geometry.test.ts | 40 ++++++ .../serialization/scene-document-json.test.ts | 44 +++++++ 4 files changed, 239 insertions(+) create mode 100644 tests/domain/create-box-brush.command.test.ts create mode 100644 tests/e2e/box-brush-authoring.e2e.ts create mode 100644 tests/geometry/box-brush-geometry.test.ts diff --git a/tests/domain/create-box-brush.command.test.ts b/tests/domain/create-box-brush.command.test.ts new file mode 100644 index 00000000..58816d3b --- /dev/null +++ b/tests/domain/create-box-brush.command.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; + +import { createEditorStore } from "../../src/app/editor-store"; +import { createCreateBoxBrushCommand } from "../../src/commands/create-box-brush-command"; +import { createMoveBoxBrushCommand } from "../../src/commands/move-box-brush-command"; +import { createResizeBoxBrushCommand } from "../../src/commands/resize-box-brush-command"; + +describe("box brush commands", () => { + it("creates a canonical box brush and supports undo/redo", () => { + const store = createEditorStore(); + + store.executeCommand( + createCreateBoxBrushCommand({ + center: { + x: 1.2, + y: 1.1, + z: -0.6 + }, + size: { + x: 2.2, + y: 2.7, + z: 3.6 + } + }) + ); + + const brush = Object.values(store.getState().document.brushes)[0]; + + expect(brush).toBeDefined(); + expect(brush.kind).toBe("box"); + expect(brush.center).toEqual({ + x: 1, + y: 1, + z: -1 + }); + expect(brush.size).toEqual({ + x: 2, + y: 3, + z: 4 + }); + expect(Object.keys(brush.faces)).toEqual(["posX", "negX", "posY", "negY", "posZ", "negZ"]); + expect(store.getState().selection).toEqual({ + kind: "brushes", + ids: [brush.id] + }); + + expect(store.undo()).toBe(true); + expect(store.getState().document.brushes).toEqual({}); + + expect(store.redo()).toBe(true); + expect(store.getState().document.brushes[brush.id]).toEqual(brush); + }); + + it("moves and resizes a box brush through undoable commands", () => { + const store = createEditorStore(); + + store.executeCommand(createCreateBoxBrushCommand()); + + const createdBrush = Object.values(store.getState().document.brushes)[0]; + + store.executeCommand( + createMoveBoxBrushCommand({ + brushId: createdBrush.id, + center: { + x: 2.4, + y: 3.2, + z: -1.7 + } + }) + ); + store.executeCommand( + createResizeBoxBrushCommand({ + brushId: createdBrush.id, + size: { + x: 4.2, + y: 1.2, + z: 0.2 + } + }) + ); + + expect(store.getState().document.brushes[createdBrush.id].center).toEqual({ + x: 2, + y: 3, + z: -2 + }); + expect(store.getState().document.brushes[createdBrush.id].size).toEqual({ + x: 4, + y: 1, + z: 1 + }); + + expect(store.undo()).toBe(true); + expect(store.getState().document.brushes[createdBrush.id].size).toEqual({ + x: 2, + y: 2, + z: 2 + }); + + expect(store.undo()).toBe(true); + expect(store.getState().document.brushes[createdBrush.id].center).toEqual({ + x: 0, + y: 1, + z: 0 + }); + + expect(store.redo()).toBe(true); + expect(store.redo()).toBe(true); + expect(store.getState().document.brushes[createdBrush.id].center).toEqual({ + x: 2, + y: 3, + z: -2 + }); + expect(store.getState().document.brushes[createdBrush.id].size).toEqual({ + x: 4, + y: 1, + z: 1 + }); + }); +}); diff --git a/tests/e2e/box-brush-authoring.e2e.ts b/tests/e2e/box-brush-authoring.e2e.ts new file mode 100644 index 00000000..9ea6800f --- /dev/null +++ b/tests/e2e/box-brush-authoring.e2e.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; + +test("user can create a box brush and keep it through a draft reload", async ({ page }) => { + const pageErrors: string[] = []; + const consoleErrors: string[] = []; + + page.on("pageerror", (error) => { + pageErrors.push(error.message); + }); + + page.on("console", (message) => { + if (message.type() === "error") { + consoleErrors.push(message.text()); + } + }); + + await page.goto("/"); + await page.evaluate((storageKey) => { + window.localStorage.removeItem(storageKey); + }, "webeditor3d.scene-document-draft"); + await page.reload(); + + await page.getByTestId("create-box-brush").click(); + await expect(page.getByText("Box Brush 1")).toBeVisible(); + await expect(page.getByText("1 brush selected (Box Brush 1)")).toBeVisible(); + + await page.getByRole("button", { name: "Save Draft" }).click(); + await page.reload(); + + await expect(page.getByText("Box Brush 1")).toBeVisible(); + await expect(page.getByText("1 box brushes loaded. Click a brush in the viewport or outliner to select it.")).toBeVisible(); + + expect(pageErrors).toEqual([]); + expect(consoleErrors).toEqual([]); +}); diff --git a/tests/geometry/box-brush-geometry.test.ts b/tests/geometry/box-brush-geometry.test.ts new file mode 100644 index 00000000..f8697e08 --- /dev/null +++ b/tests/geometry/box-brush-geometry.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { createBoxBrush } from "../../src/document/brushes"; +import { getBoxBrushBounds, getBoxBrushCornerPositions } from "../../src/geometry/box-brush"; + +describe("box brush geometry", () => { + it("builds finite bounds and eight corner positions from canonical box data", () => { + const brush = createBoxBrush({ + center: { + x: 2, + y: 4, + z: -3 + }, + size: { + x: 6, + y: 2, + z: 4 + } + }); + + expect(getBoxBrushBounds(brush)).toEqual({ + min: { + x: -1, + y: 3, + z: -5 + }, + max: { + x: 5, + y: 5, + z: -1 + } + }); + + const corners = getBoxBrushCornerPositions(brush); + + expect(corners).toHaveLength(8); + expect(new Set(corners.map((corner) => `${corner.x}:${corner.y}:${corner.z}`)).size).toBe(8); + expect(corners.every((corner) => Number.isFinite(corner.x) && Number.isFinite(corner.y) && Number.isFinite(corner.z))).toBe(true); + }); +}); diff --git a/tests/serialization/scene-document-json.test.ts b/tests/serialization/scene-document-json.test.ts index 9631b989..7ce35212 100644 --- a/tests/serialization/scene-document-json.test.ts +++ b/tests/serialization/scene-document-json.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import { createBoxBrush } from "../../src/document/brushes"; import { createEmptySceneDocument } from "../../src/document/scene-document"; import { migrateSceneDocument } from "../../src/document/migrate-scene-document"; import { parseSceneDocumentJson, serializeSceneDocument } from "../../src/serialization/scene-document-json"; @@ -12,6 +13,49 @@ describe("scene document JSON", () => { expect(parseSceneDocumentJson(serializedDocument)).toEqual(document); }); + it("round-trips a document containing a canonical box brush", () => { + const brush = createBoxBrush({ + id: "brush-box-room", + center: { + x: 0, + y: 1, + z: 0 + }, + size: { + x: 4, + y: 2, + z: 6 + } + }); + const document = { + ...createEmptySceneDocument({ name: "Brush Scene" }), + brushes: { + [brush.id]: brush + } + }; + + expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document); + }); + + it("migrates the foundation schema to the current schema version", () => { + const migratedDocument = migrateSceneDocument({ + version: 1, + name: "Foundation Scene", + world: createEmptySceneDocument().world, + materials: {}, + textures: {}, + assets: {}, + brushes: {}, + modelInstances: {}, + entities: {}, + interactionLinks: {} + }); + + expect(migratedDocument.version).toBe(2); + expect(migratedDocument.brushes).toEqual({}); + expect(migratedDocument.name).toBe("Foundation Scene"); + }); + it("rejects unsupported versions", () => { expect(() => migrateSceneDocument({