mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 17:12:44 +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 {
|
private startCurrentPhase(): void {
|
||||||
if (!this.currentPhase) {
|
if (!this.currentPhase) {
|
||||||
|
console.warn("trying to start null phase!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`%cStart Phase ${this.currentPhase.phaseName}`, "color:green;");
|
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 { UiMode } from "#enums/ui-mode";
|
||||||
import UI from "#app/ui/ui";
|
import UI from "#app/ui/ui";
|
||||||
import type { PhaseString } from "#app/@types/phase-types";
|
import type { PhaseString } from "#app/@types/phase-types";
|
||||||
import { vi } from "vitest";
|
import { vi, type MockInstance } from "vitest";
|
||||||
import { format } from "util";
|
import { format } from "util";
|
||||||
|
|
||||||
interface PromptHandler {
|
interface PromptHandler {
|
||||||
@ -16,7 +16,7 @@ interface PromptHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The PhaseInterceptor is a wrapper around the `BattleScene`'s {@linkcode PhaseManager}.
|
* 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 {
|
export default class PhaseInterceptor {
|
||||||
private scene: BattleScene;
|
private scene: BattleScene;
|
||||||
@ -44,16 +44,18 @@ export default class PhaseInterceptor {
|
|||||||
* Method to initialize various mocks for intercepting phases.
|
* Method to initialize various mocks for intercepting phases.
|
||||||
*/
|
*/
|
||||||
initMocks() {
|
initMocks() {
|
||||||
const originalSetMode = UI.prototype.setMode;
|
const originalSetMode = UI.prototype["setModeInternal"];
|
||||||
vi.spyOn(UI.prototype, "setMode").mockImplementation((mode, ...args) =>
|
// `any` assertion needed as we are mocking private property
|
||||||
this.setMode(originalSetMode, mode, ...args),
|
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
|
// Mock the private startCurrentPhase method to do nothing to let us
|
||||||
// start them manually ourselves.
|
// start them manually ourselves.
|
||||||
this.scene.phaseManager["startCurrentPhase"] satisfies () => void; // typecheck in case function is renamed/removed
|
vi.spyOn(this.scene.phaseManager as any, "startCurrentPhase").mockImplementation(() => {});
|
||||||
// @ts-expect-error - startCurrentPhase is private
|
|
||||||
vi.spyOn(this.scene.phaseManager, "startCurrentPhase").mockImplementation(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,6 +74,7 @@ export default class PhaseInterceptor {
|
|||||||
* @param runTarget - Whether or not to run the target phase; default `true`.
|
* @param runTarget - Whether or not to run the target phase; default `true`.
|
||||||
* @returns A promise that resolves when the transition is complete.
|
* @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> {
|
public async to(targetPhase: PhaseString, runTarget = true): Promise<void> {
|
||||||
let currentPhase = this.scene.phaseManager.getCurrentPhase();
|
let currentPhase = this.scene.phaseManager.getCurrentPhase();
|
||||||
while (!currentPhase?.is(targetPhase)) {
|
while (!currentPhase?.is(targetPhase)) {
|
||||||
@ -126,12 +129,13 @@ export default class PhaseInterceptor {
|
|||||||
* @param args - Additional arguments to pass to the original method.
|
* @param args - Additional arguments to pass to the original method.
|
||||||
*/
|
*/
|
||||||
private async setMode(
|
private async setMode(
|
||||||
originalSetMode: typeof UI.prototype.setMode,
|
originalSetMode: (typeof UI.prototype)["setModeInternal"],
|
||||||
mode: UiMode,
|
args: Parameters<(typeof UI.prototype)["setModeInternal"]>,
|
||||||
...args: unknown[]
|
): ReturnType<(typeof UI.prototype)["setModeInternal"]> {
|
||||||
): ReturnType<typeof UI.prototype.setMode> {
|
const mode = args[0];
|
||||||
|
|
||||||
console.log("setMode", `${UiMode[mode]} (=${mode})`, args);
|
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);
|
this.doPromptCheck(mode);
|
||||||
return ret;
|
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