import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { MockViewportHost, viewportHostInstances } = vi.hoisted(() => { const viewportHostInstances: Array<{ setTransformCommitHandler: ReturnType; setRenderEnabled: ReturnType; mount: ReturnType; dispose: ReturnType; updateWorld: ReturnType; updateSimulation: ReturnType; updateAssets: ReturnType; updateDocument: ReturnType; updateSelection: ReturnType; setPanelId: ReturnType; setViewMode: ReturnType; setDisplayMode: ReturnType; setGridVisible: ReturnType; setCameraState: ReturnType; setBrushSelectionChangeHandler: ReturnType; setCameraStateChangeHandler: ReturnType; setCreationPreviewChangeHandler: ReturnType; setCreationCommitHandler: ReturnType; setTransformSessionChangeHandler: ReturnType; setTransformPreviewChangeHandler: ReturnType; setTransformCancelHandler: ReturnType; setWhiteboxHoverLabelChangeHandler: ReturnType; setWhiteboxSelectionMode: ReturnType; setWhiteboxSnapSettings: ReturnType; setToolMode: ReturnType; setCreationPreview: ReturnType; setTransformSession: ReturnType; focusSelection: ReturnType; }> = []; class MockViewportHost { setPanelId = vi.fn(); setRenderEnabled = vi.fn(); mount = vi.fn(); dispose = vi.fn(); updateWorld = vi.fn(); updateSimulation = vi.fn(); updateAssets = vi.fn(); updateDocument = vi.fn(); updateSelection = vi.fn(); setViewMode = vi.fn(); setDisplayMode = vi.fn(); setGridVisible = vi.fn(); setCameraState = vi.fn(); setBrushSelectionChangeHandler = vi.fn(); setCameraStateChangeHandler = vi.fn(); setCreationPreviewChangeHandler = vi.fn(); setCreationCommitHandler = vi.fn(); setTransformSessionChangeHandler = vi.fn(); setTransformPreviewChangeHandler = vi.fn(); setTransformCommitHandler = vi.fn(); setTransformCancelHandler = vi.fn(); setWhiteboxHoverLabelChangeHandler = vi.fn(); setWhiteboxSelectionMode = vi.fn(); setWhiteboxSnapSettings = vi.fn(); setToolMode = vi.fn(); setCreationPreview = vi.fn(); setTransformSession = vi.fn(); focusSelection = vi.fn(); constructor() { viewportHostInstances.push(this); } } return { MockViewportHost, viewportHostInstances }; }); const { loadProjectPackageMock, saveProjectPackageMock } = vi.hoisted(() => ({ saveProjectPackageMock: vi.fn(async () => new Uint8Array([1, 2, 3])), loadProjectPackageMock: vi.fn() })); vi.mock("../../src/viewport-three/viewport-host", () => ({ ViewportHost: MockViewportHost })); vi.mock("../../src/assets/project-asset-storage", () => ({ getBrowserProjectAssetStorageAccess: vi.fn(async () => ({ storage: null, diagnostic: null })) })); vi.mock("../../src/serialization/project-package", () => ({ PROJECT_PACKAGE_FILE_EXTENSION: ".we3d", saveProjectPackage: saveProjectPackageMock, loadProjectPackage: loadProjectPackageMock })); import { App } from "../../src/app/App"; import { createEditorStore } from "../../src/app/editor-store"; describe("App project persistence controls", () => { let clickedDownloads: string[]; beforeEach(() => { clickedDownloads = []; viewportHostInstances.length = 0; saveProjectPackageMock.mockClear(); loadProjectPackageMock.mockClear(); vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => ({}) as never); vi.stubGlobal("URL", { createObjectURL: vi.fn(() => "blob:project"), revokeObjectURL: vi.fn() }); vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation( function (this: HTMLAnchorElement) { clickedDownloads.push(this.download); } ); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("shows Save Project and Load Project instead of the old draft/json actions", async () => { render(); await waitFor(() => { expect(viewportHostInstances.length).toBeGreaterThan(0); }); expect(screen.getByRole("button", { name: "Save Project" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Load Project" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Save Draft" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Load Draft" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Export JSON" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Import JSON" })).not.toBeInTheDocument(); }); it("invokes Save Project when Cmd/Ctrl+S is pressed", async () => { const store = createEditorStore(); render(); await waitFor(() => { expect(viewportHostInstances.length).toBeGreaterThan(0); }); fireEvent.keyDown(window, { code: "KeyS", ctrlKey: true }); await waitFor(() => { expect(saveProjectPackageMock).toHaveBeenCalledWith( store.getState().projectDocument, null ); }); }); it("uses the project name rather than the active scene name for saved packages", async () => { const store = createEditorStore(); render(); await waitFor(() => { expect(viewportHostInstances.length).toBeGreaterThan(0); }); fireEvent.change(screen.getByTestId("toolbar-scene-name"), { target: { value: "Dungeon Scene" } }); fireEvent.blur(screen.getByTestId("toolbar-scene-name")); fireEvent.click(screen.getByRole("button", { name: "Save Project" })); await waitFor(() => { expect(clickedDownloads).toEqual(["untitled-project.we3d"]); }); fireEvent.change(screen.getByTestId("toolbar-project-name"), { target: { value: "Castle Layout" } }); fireEvent.blur(screen.getByTestId("toolbar-project-name")); fireEvent.click(screen.getByRole("button", { name: "Save Project" })); await waitFor(() => { expect(clickedDownloads).toEqual([ "untitled-project.we3d", "castle-layout.we3d" ]); }); expect(store.getState().document.name).toBe("Dungeon Scene"); expect(store.getState().projectDocument.name).toBe("Castle Layout"); }); });