You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

227 lines
6.0 KiB

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 = [];
}
}
}
class MockGraphics extends MockContainer {
clear(): this {
return this;
}
rect(): this {
return this;
}
roundRect(): this {
return this;
}
fill(): this {
return this;
}
stroke(): this {
return this;
}
}
class MockText extends MockContainer {
text = "";
style: unknown;
anchor = {
set: () => {},
};
position = {
set: () => {},
};
}
class MockTextStyle {
constructor(_style?: Record<string, unknown>) {}
}
vi.mock("pixi.js", () => ({
Container: MockContainer,
Graphics: MockGraphics,
Text: MockText,
TextStyle: MockTextStyle,
}));
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>
): 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);
});
it("reloads current scene without leaving stage empty", async () => {
const module = await import("@/scene/SceneManager");
const SceneManager = module.default;
const manager = SceneManager.getInstance();
const current = createScene("same", SceneType.Normal, {
_assetsLoaded: true,
_layoutDone: true,
onUnLoad: vi.fn(async () => {}),
unLoadBundle: vi.fn(async () => {}),
loadBundle: vi.fn(async () => {}),
layout: vi.fn(async () => {}),
onLoad: vi.fn(async function (this: IBaseScene) {
(this.stage as unknown as MockContainer).addChild(new MockContainer());
}),
});
manager.registerScene(current);
await manager.initScene("same");
const beforeStage = current.stage;
await manager.changeScene("same");
const afterStage = current.stage as unknown as MockContainer;
const rootStage = manager["game"].stage as unknown as MockContainer;
expect(manager.currentScene).toBe(current);
expect(afterStage).not.toBe(beforeStage);
expect(afterStage.children.length).toBeGreaterThan(0);
expect(rootStage.children.includes(afterStage as never)).toBe(true);
});
});