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
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);
|
|
});
|
|
});
|
|
|