import { beforeEach, describe, expect, it, vi } from "vitest"; import { TransitionTransaction } from "@/scene/TransitionTransaction"; import { SceneType } from "@/enums/SceneType"; import type { IBaseScene } from "@/scene/types"; class MockContainer { visible = true; label = ""; _isHolderLast?: boolean; children: MockContainer[] = []; addChild(child: MockContainer): MockContainer { this.children.push(child); return child; } removeChild(child: MockContainer): MockContainer { this.children = this.children.filter((item) => item !== child); return child; } destroy(options?: { children?: boolean }): void { if (options?.children) { this.children = []; } } } vi.mock("pixi.js", () => ({ Container: MockContainer, })); vi.mock("@/core/Game", () => { class MockGame { private static instance: MockGame; readonly stage = new MockContainer(); static getInstance(): MockGame { if (!MockGame.instance) { MockGame.instance = new MockGame(); } return MockGame.instance; } } return { default: MockGame }; }); function createScene( name: string, type: SceneType = SceneType.Normal, overrides?: Partial ): IBaseScene { const stage = new MockContainer(); stage.label = name; return { name, type, stage: stage as never, _assetsLoaded: true, _layoutDone: true, changeScene: () => {}, loadBundle: async () => {}, unLoadBundle: async () => {}, layout: async () => {}, onLoad: async () => {}, onUnLoad: async () => {}, ...overrides, }; } describe("TransitionTransaction", () => { it("rolls back when prepare fails and never commits", async () => { const prepareError = new Error("prepare failed"); const prepare = vi.fn(async () => { throw prepareError; }); const commit = vi.fn(async () => {}); const rollback = vi.fn(async () => {}); const transaction = new TransitionTransaction({ prepare, commit, rollback, }); await expect(transaction.execute()).rejects.toThrowError("prepare failed"); expect(prepare).toHaveBeenCalledTimes(1); expect(rollback).toHaveBeenCalledTimes(1); expect(commit).not.toHaveBeenCalled(); }); it("rolls back when commit fails", async () => { const commitError = new Error("commit failed"); const prepare = vi.fn(async () => {}); const commit = vi.fn(async () => { throw commitError; }); const rollback = vi.fn(async () => {}); const transaction = new TransitionTransaction({ prepare, commit, rollback, }); await expect(transaction.execute()).rejects.toThrowError("commit failed"); expect(prepare).toHaveBeenCalledTimes(1); expect(commit).toHaveBeenCalledTimes(1); expect(rollback).toHaveBeenCalledTimes(1); }); }); describe("SceneManager rollback safety", () => { beforeEach(async () => { vi.resetModules(); const module = await import("@/scene/SceneManager"); (module.default as unknown as { instance?: unknown }).instance = undefined; }); it("keeps previous stage content intact when target load fails", async () => { const module = await import("@/scene/SceneManager"); const SceneManager = module.default; const manager = SceneManager.getInstance(); const previous = createScene("previous"); const previousContent = new MockContainer(); (previous.stage as unknown as MockContainer).addChild(previousContent); const target = createScene("target", SceneType.Normal, { onLoad: async () => { throw new Error("target onLoad failed"); }, }); manager.registerScene(previous); manager.registerScene(target); await manager.initScene("previous"); await expect(manager.changeScene("target")).rejects.toThrowError( "target onLoad failed" ); const stage = manager["game"].stage as unknown as MockContainer; expect(manager.currentScene).toBe(previous); expect(previous.stage).not.toBeUndefined(); expect((previous.stage as unknown as MockContainer).children).toContain( previousContent ); expect((previous.stage as unknown as MockContainer).visible).toBe(true); expect(stage.children.includes(previous.stage as never)).toBe(true); expect(stage.children.includes(target.stage as never)).toBe(false); }); });