mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
Added unit tests for phase interceptor
This commit is contained in:
parent
1bcad94568
commit
3ed587de6d
@ -400,6 +400,7 @@ export class PhaseManager {
|
||||
*/
|
||||
private startCurrentPhase(): void {
|
||||
if (!this.currentPhase) {
|
||||
console.warn("trying to start null phase!");
|
||||
return;
|
||||
}
|
||||
console.log(`%cStart Phase ${this.currentPhase.phaseName}`, "color:green;");
|
||||
|
@ -3,7 +3,7 @@ import type { Phase } from "#app/phase";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import UI from "#app/ui/ui";
|
||||
import type { PhaseString } from "#app/@types/phase-types";
|
||||
import { vi } from "vitest";
|
||||
import { vi, type MockInstance } from "vitest";
|
||||
import { format } from "util";
|
||||
|
||||
interface PromptHandler {
|
||||
@ -16,7 +16,7 @@ interface PromptHandler {
|
||||
|
||||
/**
|
||||
* The PhaseInterceptor is a wrapper around the `BattleScene`'s {@linkcode PhaseManager}.
|
||||
* It allows tests to exert finer control over the phase system, providing logging,
|
||||
* It allows tests to exert finer control over the phase system, providing logging, manual advancing, etc etc.
|
||||
*/
|
||||
export default class PhaseInterceptor {
|
||||
private scene: BattleScene;
|
||||
@ -44,16 +44,18 @@ export default class PhaseInterceptor {
|
||||
* Method to initialize various mocks for intercepting phases.
|
||||
*/
|
||||
initMocks() {
|
||||
const originalSetMode = UI.prototype.setMode;
|
||||
vi.spyOn(UI.prototype, "setMode").mockImplementation((mode, ...args) =>
|
||||
this.setMode(originalSetMode, mode, ...args),
|
||||
);
|
||||
const originalSetMode = UI.prototype["setModeInternal"];
|
||||
// `any` assertion needed as we are mocking private property
|
||||
const uiSpy = vi.spyOn(UI.prototype as any, "setModeInternal") as MockInstance<
|
||||
(typeof UI.prototype)["setModeInternal"]
|
||||
>;
|
||||
uiSpy.mockImplementation(async (...args) => {
|
||||
this.setMode(originalSetMode, args);
|
||||
});
|
||||
|
||||
// Mock the private startCurrentPhase method to do nothing to let us
|
||||
// start them manually ourselves.
|
||||
this.scene.phaseManager["startCurrentPhase"] satisfies () => void; // typecheck in case function is renamed/removed
|
||||
// @ts-expect-error - startCurrentPhase is private
|
||||
vi.spyOn(this.scene.phaseManager, "startCurrentPhase").mockImplementation(() => {});
|
||||
vi.spyOn(this.scene.phaseManager as any, "startCurrentPhase").mockImplementation(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,6 +74,7 @@ export default class PhaseInterceptor {
|
||||
* @param runTarget - Whether or not to run the target phase; default `true`.
|
||||
* @returns A promise that resolves when the transition is complete.
|
||||
*/
|
||||
// TODO: Does this need to be asynchronous?
|
||||
public async to(targetPhase: PhaseString, runTarget = true): Promise<void> {
|
||||
let currentPhase = this.scene.phaseManager.getCurrentPhase();
|
||||
while (!currentPhase?.is(targetPhase)) {
|
||||
@ -126,12 +129,13 @@ export default class PhaseInterceptor {
|
||||
* @param args - Additional arguments to pass to the original method.
|
||||
*/
|
||||
private async setMode(
|
||||
originalSetMode: typeof UI.prototype.setMode,
|
||||
mode: UiMode,
|
||||
...args: unknown[]
|
||||
): ReturnType<typeof UI.prototype.setMode> {
|
||||
originalSetMode: (typeof UI.prototype)["setModeInternal"],
|
||||
args: Parameters<(typeof UI.prototype)["setModeInternal"]>,
|
||||
): ReturnType<(typeof UI.prototype)["setModeInternal"]> {
|
||||
const mode = args[0];
|
||||
|
||||
console.log("setMode", `${UiMode[mode]} (=${mode})`, args);
|
||||
const ret = originalSetMode.apply(this.scene.ui, [mode, ...args]);
|
||||
const ret = originalSetMode.apply(this.scene.ui, [args]);
|
||||
this.doPromptCheck(mode);
|
||||
return ret;
|
||||
}
|
||||
|
106
test/utils/phase-interceptor-prompts.test.ts
Normal file
106
test/utils/phase-interceptor-prompts.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import type { PhaseString } from "#app/@types/phase-types";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import type { Constructor } from "#app/utils/common";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
abstract class mockPhase extends Phase {
|
||||
public override readonly phaseName: any;
|
||||
|
||||
override start() {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
class testDialogueUiPhase extends mockPhase {
|
||||
public readonly phaseName = "testDialogueUiPhase";
|
||||
override start() {
|
||||
void globalScene.ui.setMode(UiMode.TEST_DIALOGUE);
|
||||
super.start();
|
||||
}
|
||||
}
|
||||
|
||||
class titleUiPhase extends mockPhase {
|
||||
public readonly phaseName = "titleUiPhase";
|
||||
override start() {
|
||||
void globalScene.ui.setMode(UiMode.TITLE);
|
||||
super.start();
|
||||
}
|
||||
}
|
||||
|
||||
class dualUiPhase extends mockPhase {
|
||||
public readonly phaseName = "dualUiPhase";
|
||||
override start() {
|
||||
void globalScene.ui.setMode(UiMode.TEST_DIALOGUE);
|
||||
void globalScene.ui.setMode(UiMode.TITLE);
|
||||
super.start();
|
||||
}
|
||||
}
|
||||
|
||||
describe("Utils - Phase Interceptor Prompts", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
setPhases(testDialogueUiPhase, titleUiPhase, dualUiPhase);
|
||||
});
|
||||
|
||||
function setPhases(...phases: Constructor<mockPhase>[]) {
|
||||
game.scene.phaseManager.clearAllPhases();
|
||||
game.scene.phaseManager.phaseQueue = phases.map(m => new m());
|
||||
game.scene.phaseManager.shiftPhase(); // start the thing going
|
||||
}
|
||||
|
||||
/** Wrapper function to make TS not complain about `PhaseString` stuff */
|
||||
function to(phaseName: string, runTarget = false) {
|
||||
return game.phaseInterceptor.to(phaseName as unknown as PhaseString, runTarget);
|
||||
}
|
||||
|
||||
function onNextPrompt(target: string, mode: UiMode, callback: () => void, expireFn?: () => boolean) {
|
||||
game.onNextPrompt(target as unknown as PhaseString, mode, callback, expireFn);
|
||||
}
|
||||
|
||||
it("should run the specified callback when the given ui mode is reached", async () => {
|
||||
const callback = vi.fn();
|
||||
onNextPrompt("testDialogueUiPhase", UiMode.TEST_DIALOGUE, () => callback());
|
||||
|
||||
await to("testDialogueUiPhase");
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run callback if wrong ui mode", async () => {
|
||||
const callback = vi.fn();
|
||||
onNextPrompt("testDialogueUiPhase", UiMode.TITLE, () => callback());
|
||||
|
||||
await to("testDialogueUiPhase");
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run callback if wrong phase", async () => {
|
||||
const callback = vi.fn();
|
||||
onNextPrompt("titleUiPhase", UiMode.TITLE, () => callback());
|
||||
|
||||
await to("testDialogueUiPhase");
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should work in succession", async () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
onNextPrompt("dualUiPhase", UiMode.TEST_DIALOGUE, () => callback1());
|
||||
onNextPrompt("dualUiPhase", UiMode.TITLE, () => callback2());
|
||||
|
||||
await to("dualUiPhase");
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
126
test/utils/phase-interceptor.test.ts
Normal file
126
test/utils/phase-interceptor.test.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import type { PhaseString } from "#app/@types/phase-types";
|
||||
import { Phase } from "#app/phase";
|
||||
import type { Constructor } from "#app/utils/common";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
abstract class mockPhase extends Phase {
|
||||
public override readonly phaseName: any;
|
||||
|
||||
override start() {
|
||||
console.log(this.phaseName);
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
class normalPhase extends mockPhase {
|
||||
public readonly phaseName = "normalPhase";
|
||||
}
|
||||
|
||||
class applePhase extends mockPhase {
|
||||
public readonly phaseName = "applePhase";
|
||||
}
|
||||
|
||||
class oneSecTimerPhase extends mockPhase {
|
||||
public readonly phaseName = "oneSecTimerPhase";
|
||||
override start() {
|
||||
setInterval(() => {
|
||||
super.start();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
class unshifterPhase extends mockPhase {
|
||||
public readonly phaseName = "unshifterPhase";
|
||||
override start() {
|
||||
game.scene.phaseManager.unshiftPhase(new normalPhase() as unknown as Phase);
|
||||
game.scene.phaseManager.unshiftPhase(new applePhase() as unknown as Phase);
|
||||
super.start();
|
||||
}
|
||||
}
|
||||
|
||||
// reduce timeout so failed tests don't hang as long
|
||||
describe("Utils - Phase Interceptor", { timeout: 4000 }, () => {
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
setPhases(normalPhase, applePhase, oneSecTimerPhase, unshifterPhase, normalPhase);
|
||||
});
|
||||
|
||||
function setPhases(...phases: Constructor<mockPhase>[]) {
|
||||
game.scene.phaseManager.clearAllPhases();
|
||||
game.scene.phaseManager.phaseQueue = phases.map(m => new m());
|
||||
game.scene.phaseManager.shiftPhase(); // start the thing going
|
||||
}
|
||||
|
||||
function getQueuedPhases(): string[] {
|
||||
return game.scene.phaseManager["phaseQueuePrepend"]
|
||||
.concat(game.scene.phaseManager.phaseQueue)
|
||||
.map(p => p.phaseName);
|
||||
}
|
||||
|
||||
function getCurrentPhaseName(): string {
|
||||
return game.scene.phaseManager.getCurrentPhase()?.phaseName ?? "null";
|
||||
}
|
||||
|
||||
/** Wrapper function to make TS not complain about `PhaseString` stuff */
|
||||
function to(phaseName: string, runTarget = true) {
|
||||
return game.phaseInterceptor.to(phaseName as unknown as PhaseString, runTarget);
|
||||
}
|
||||
|
||||
describe("to", () => {
|
||||
it("should run the specified phase and halt after it ends", async () => {
|
||||
await to("normalPhase");
|
||||
expect(getCurrentPhaseName()).toBe("applePhase");
|
||||
expect(getQueuedPhases()).toEqual(["oneSecTimerPhase", "unshifterPhase", "normalPhase"]);
|
||||
expect(game.phaseInterceptor.log).toEqual(["normalPhase"]);
|
||||
});
|
||||
|
||||
it("should run to the specified phase without starting/logging", async () => {
|
||||
await to("normalPhase", false);
|
||||
expect(getCurrentPhaseName()).toBe("normalPhase");
|
||||
expect(getQueuedPhases()).toEqual(["applePhase", "oneSecTimerPhase", "unshifterPhase", "normalPhase"]);
|
||||
expect(game.phaseInterceptor.log).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should start all phases between start and target", async () => {
|
||||
await to("oneSecTimerPhase");
|
||||
expect(getQueuedPhases()).toEqual(["unshifterPhase", "normalPhase"]);
|
||||
expect(game.phaseInterceptor.log).toEqual(["normalPhase", "applePhase", "oneSecTimerPhase"]);
|
||||
});
|
||||
|
||||
it("should work on newly unshifted phases", async () => {
|
||||
setPhases(unshifterPhase); // adds normalPhase and applePhase to queue
|
||||
await to("applePhase");
|
||||
expect(game.phaseInterceptor.log).toEqual(["unshifterPhase", "normalPhase", "applePhase"]);
|
||||
});
|
||||
|
||||
it("should wait until phase finishes before starting next", async () => {
|
||||
setPhases(oneSecTimerPhase, applePhase);
|
||||
setTimeout(() => expect(getCurrentPhaseName()).toBe("oneSecTimerPhase"), 500);
|
||||
await to("applePhase");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shift", () => {
|
||||
it("should skip the next phase without starting", async () => {
|
||||
expect(getCurrentPhaseName()).toBe("normalPhase");
|
||||
expect(getQueuedPhases()).toEqual(["applePhase", "oneSecTimerPhase", "unshifterPhase", "normalPhase"]);
|
||||
|
||||
game.phaseInterceptor.shiftPhase();
|
||||
|
||||
expect(getCurrentPhaseName()).toBe("applePhase");
|
||||
expect(getQueuedPhases()).toEqual(["oneSecTimerPhase", "unshifterPhase", "normalPhase"]);
|
||||
expect(game.phaseInterceptor.log).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user