From b2d796eca35776dffb03d31bd47b5184da2fe233 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 20 Aug 2025 23:35:45 -0400 Subject: [PATCH 01/30] Squashed changes... again --- scripts/create-test/boilerplates/default.ts | 6 +- src/battle-scene.ts | 10 +- src/phase-manager.ts | 25 +- src/phases/title-phase.ts | 28 +- test/moves/parting-shot.test.ts | 27 +- .../bug-type-superfan-encounter.test.ts | 2 +- test/test-utils/error-interceptor.ts | 49 -- test/test-utils/game-manager.ts | 44 +- test/test-utils/game-wrapper.ts | 12 +- test/test-utils/helpers/prompt-handler.ts | 159 +++++ test/test-utils/helpers/reload-helper.ts | 9 +- test/test-utils/mocks/mock-phase.ts | 11 + test/test-utils/phase-interceptor.ts | 603 ++++++------------ .../tests/helpers/prompt-handler.test.ts | 153 +++++ .../phase-interceptor/integration.test.ts | 62 ++ .../tests/phase-interceptor/unit.test.ts | 150 +++++ test/vitest.setup.ts | 22 +- 17 files changed, 831 insertions(+), 541 deletions(-) delete mode 100644 test/test-utils/error-interceptor.ts create mode 100644 test/test-utils/helpers/prompt-handler.ts create mode 100644 test/test-utils/mocks/mock-phase.ts create mode 100644 test/test-utils/tests/helpers/prompt-handler.test.ts create mode 100644 test/test-utils/tests/phase-interceptor/integration.test.ts create mode 100644 test/test-utils/tests/phase-interceptor/unit.test.ts diff --git a/scripts/create-test/boilerplates/default.ts b/scripts/create-test/boilerplates/default.ts index fa914b150c2..b405fe9412e 100644 --- a/scripts/create-test/boilerplates/default.ts +++ b/scripts/create-test/boilerplates/default.ts @@ -3,7 +3,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("{{description}}", () => { let phaserGame: Phaser.Game; @@ -15,10 +15,6 @@ describe("{{description}}", () => { }); }); - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - beforeEach(() => { game = new GameManager(phaserGame); game.override diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4bd41299d8f..4e3996f687c 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -392,7 +392,11 @@ export class BattleScene extends SceneBase { }); } - create() { + /** + * Create game objects with loaded assets. + * Called by Phaser on new game start. + */ + create(): void { this.scene.remove(LoadingScene.KEY); initGameSpeed.apply(this); this.inputController = new InputsController(); @@ -417,6 +421,7 @@ export class BattleScene extends SceneBase { this.ui?.update(); } + // TODO: Split this up into multiple sub-methods launchBattle() { this.arenaBg = this.add.sprite(0, 0, "plains_bg"); this.arenaBg.setName("sprite-arena-bg"); @@ -597,6 +602,8 @@ export class BattleScene extends SceneBase { this.arenaNextEnemy.setVisible(false); for (const a of [this.arenaPlayer, this.arenaPlayerTransition, this.arenaEnemy, this.arenaNextEnemy]) { + // TODO: This seems questionable - we just initialized the arena sprites and then have to manually check if they're a sprite? + // This is likely the result of either extreme laziness or confusion if (a instanceof Phaser.GameObjects.Sprite) { a.setOrigin(0, 0); } @@ -1138,6 +1145,7 @@ export class BattleScene extends SceneBase { return this.currentBattle?.randSeedInt(range, min); } + // TODO: Break up function - this does far too much in 1 sitting reset(clearScene = false, clearData = false, reloadI18n = false): void { if (clearData) { this.gameData = new GameData(); diff --git a/src/phase-manager.ts b/src/phase-manager.ts index aa01a0ffc10..e971b732e37 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -389,12 +389,26 @@ export class PhaseManager { } this.conditionalQueue.push(...unactivatedConditionalPhases); - if (this.currentPhase) { - console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); - this.currentPhase.start(); - } + this.startCurrentPhase(); } + /** + * Helper method to start and log the current phase. + * + * @remarks + * This is disabled during tests by `phase-interceptor.ts` to allow for pausing execution at specific phases. + * As such, **do not remove or split this method** as it will break integration tests. + */ + private startCurrentPhase(): void { + if (!this.currentPhase) { + console.warn("Trying to start null phase!"); + return; + } + console.log(`%cStart Phase ${this.currentPhase.phaseName}`, "color:green;"); + this.currentPhase.start(); + } + + // TODO: Review if we can remove this overridePhase(phase: Phase): boolean { if (this.standbyPhase) { return false; @@ -402,8 +416,7 @@ export class PhaseManager { this.standbyPhase = this.currentPhase; this.currentPhase = phase; - console.log(`%cStart Phase ${phase.constructor.name}`, "color:green;"); - phase.start(); + this.startCurrentPhase(); return true; } diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 15d92ba2812..11a2ccc482c 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -168,24 +168,22 @@ export class TitlePhase extends Phase { globalScene.ui.setMode(UiMode.TITLE, config); } - loadSaveSlot(slotId: number): void { + async loadSaveSlot(slotId: number): Promise { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - globalScene.gameData - .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) - .then((success: boolean) => { - if (success) { - this.loaded = true; - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - }) - .catch(err => { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); + try { + const success = await globalScene.gameData.loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined); + if (success) { + this.loaded = true; + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + } catch (err) { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + } } initDailyRun(): void { diff --git a/test/moves/parting-shot.test.ts b/test/moves/parting-shot.test.ts index e9400aef29b..eafc80bcf0b 100644 --- a/test/moves/parting-shot.test.ts +++ b/test/moves/parting-shot.test.ts @@ -73,31 +73,9 @@ describe("Moves - Parting Shot", () => { SpeciesId.ABRA, ]); - // use Memento 3 times to debuff enemy - game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to("FaintPhase"); - expect(game.field.getPlayerPokemon().isFainted()).toBe(true); - game.doSelectPartyPokemon(1); - - await game.phaseInterceptor.to("TurnInitPhase", false); - game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to("FaintPhase"); - expect(game.field.getPlayerPokemon().isFainted()).toBe(true); - game.doSelectPartyPokemon(2); - - await game.phaseInterceptor.to("TurnInitPhase", false); - game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to("FaintPhase"); - expect(game.field.getPlayerPokemon().isFainted()).toBe(true); - game.doSelectPartyPokemon(3); - - // set up done - await game.phaseInterceptor.to("TurnInitPhase", false); const enemyPokemon = game.field.getEnemyPokemon(); - expect(enemyPokemon).toBeDefined(); - - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6); - expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6); + enemyPokemon.setStatStage(Stat.ATK, -6); + enemyPokemon.setStatStage(Stat.SPATK, -6); // now parting shot should fail game.move.select(MoveId.PARTING_SHOT); @@ -136,7 +114,6 @@ describe("Moves - Parting Shot", () => { await game.classicMode.startBattle([SpeciesId.SNORLAX, SpeciesId.MEOWTH]); const enemyPokemon = game.field.getEnemyPokemon(); - expect(enemyPokemon).toBeDefined(); game.move.select(MoveId.PARTING_SHOT); diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 13d3c030c63..aacc76e8a30 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -366,7 +366,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await skipBattleRunMysteryEncounterRewardsPhase(game, false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); - game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers + game.promptHandler["prompts"] = []; // Clear out prompt handlers game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { game.endPhase(); }); diff --git a/test/test-utils/error-interceptor.ts b/test/test-utils/error-interceptor.ts deleted file mode 100644 index c253561a71c..00000000000 --- a/test/test-utils/error-interceptor.ts +++ /dev/null @@ -1,49 +0,0 @@ -export class ErrorInterceptor { - private static instance: ErrorInterceptor; - public running; - - constructor() { - this.running = []; - } - - public static getInstance(): ErrorInterceptor { - if (!ErrorInterceptor.instance) { - ErrorInterceptor.instance = new ErrorInterceptor(); - } - return ErrorInterceptor.instance; - } - - clear() { - this.running = []; - } - - add(obj) { - this.running.push(obj); - } - - remove(obj) { - const index = this.running.indexOf(obj); - if (index !== -1) { - this.running.splice(index, 1); - } - } -} - -process.on("uncaughtException", error => { - console.log(error); - const toStop = ErrorInterceptor.getInstance().running; - for (const elm of toStop) { - elm.rejectAll(error); - } - global.testFailed = true; -}); - -// Global error handler for unhandled promise rejections -process.on("unhandledRejection", (reason, _promise) => { - console.log(reason); - const toStop = ErrorInterceptor.getInstance().running; - for (const elm of toStop) { - elm.rejectAll(reason); - } - global.testFailed = true; -}); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 57badd866b1..a4045c6c2c5 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -28,7 +28,6 @@ import type { SelectTargetPhase } from "#phases/select-target-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; -import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import { generateStarter } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; import { ChallengeModeHelper } from "#test/test-utils/helpers/challenge-mode-helper"; @@ -38,12 +37,14 @@ import { FieldHelper } from "#test/test-utils/helpers/field-helper"; import { ModifierHelper } from "#test/test-utils/helpers/modifiers-helper"; import { MoveHelper } from "#test/test-utils/helpers/move-helper"; import { OverridesHelper } from "#test/test-utils/helpers/overrides-helper"; +import { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; import { ReloadHelper } from "#test/test-utils/helpers/reload-helper"; import { SettingsHelper } from "#test/test-utils/helpers/settings-helper"; import type { InputsHandler } from "#test/test-utils/inputs-handler"; import { MockFetch } from "#test/test-utils/mocks/mock-fetch"; import { PhaseInterceptor } from "#test/test-utils/phase-interceptor"; import { TextInterceptor } from "#test/test-utils/text-interceptor"; +import type { PhaseClass, PhaseString } from "#types/phase-types"; import type { BallUiHandler } from "#ui/ball-ui-handler"; import type { BattleMessageUiHandler } from "#ui/battle-message-ui-handler"; import type { CommandUiHandler } from "#ui/command-ui-handler"; @@ -65,6 +66,7 @@ export class GameManager { public phaseInterceptor: PhaseInterceptor; public textInterceptor: TextInterceptor; public inputsHandler: InputsHandler; + public readonly promptHandler: PromptHandler; public readonly override: OverridesHelper; public readonly move: MoveHelper; public readonly classicMode: ClassicModeHelper; @@ -82,7 +84,6 @@ export class GameManager { */ constructor(phaserGame: Phaser.Game, bypassLogin = true) { localStorage.clear(); - ErrorInterceptor.getInstance().clear(); // Simulate max rolls on RNG functions // TODO: Create helpers for disabling/enabling battle RNG BattleScene.prototype.randBattleSeedInt = (range, min = 0) => min + range - 1; @@ -102,6 +103,7 @@ export class GameManager { } this.textInterceptor = new TextInterceptor(this.scene); + this.promptHandler = new PromptHandler(this); this.override = new OverridesHelper(this); this.move = new MoveHelper(this); this.classicMode = new ClassicModeHelper(this); @@ -157,7 +159,8 @@ export class GameManager { } /** - * End the currently running phase immediately. + * End the current phase immediately. + * @see {@linkcode PhaseInterceptor.shiftPhase} Function to skip the next upcoming phase */ endPhase() { this.scene.phaseManager.getCurrentPhase()?.end(); @@ -170,15 +173,18 @@ export class GameManager { * @param mode - The mode to wait for. * @param callback - The callback function to execute on next prompt. * @param expireFn - Optional function to determine if the prompt has expired. + * @param awaitingActionInput - If true, will prevent the prompt from activating until the current {@linkcode AwaitableUiHandler} + * is awaiting input; default `false` + * @todo Remove in favor of {@linkcode promptHandler.addToNextPrompt} */ onNextPrompt( - phaseTarget: string, + phaseTarget: PhaseString, mode: UiMode, callback: () => void, - expireFn?: () => void, + expireFn?: () => boolean, awaitingActionInput = false, ) { - this.phaseInterceptor.addToNextPrompt(phaseTarget, mode, callback, expireFn, awaitingActionInput); + this.promptHandler.addToNextPrompt(phaseTarget, mode, callback, expireFn, awaitingActionInput); } /** @@ -188,7 +194,7 @@ export class GameManager { async runToTitle(): Promise { // Go to login phase and skip past it await this.phaseInterceptor.to("LoginPhase", false); - this.phaseInterceptor.shiftPhase(true); + this.phaseInterceptor.shiftPhase(); await this.phaseInterceptor.to("TitlePhase"); // TODO: This should be moved to a separate initialization method @@ -365,14 +371,14 @@ export class GameManager { * Transition to the first {@linkcode CommandPhase} of the next turn. * @returns A promise that resolves once the next {@linkcode CommandPhase} has been reached. */ - async toNextTurn() { + async toNextTurn(): Promise { await this.phaseInterceptor.to("TurnInitPhase"); await this.phaseInterceptor.to("CommandPhase"); console.log("==================[New Turn]=================="); } /** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */ - async toEndOfTurn() { + async toEndOfTurn(): Promise { await this.phaseInterceptor.to("TurnEndPhase"); console.log("==================[End of Turn]=================="); } @@ -381,7 +387,7 @@ export class GameManager { * Queue up button presses to skip taking an item on the next {@linkcode SelectModifierPhase}, * and then transition to the next {@linkcode CommandPhase}. */ - async toNextWave() { + async toNextWave(): Promise { this.doSelectModifier(); // forcibly end the message box for switching pokemon @@ -404,7 +410,7 @@ export class GameManager { * Check if the player has won the battle. * @returns whether the player has won the battle (all opposing Pokemon have been fainted) */ - isVictory() { + isVictory(): boolean { return this.scene.currentBattle.enemyParty.every(pokemon => pokemon.isFainted()); } @@ -413,9 +419,17 @@ export class GameManager { * @param phaseTarget - The target phase. * @returns Whether the current phase matches the target phase */ - isCurrentPhase(phaseTarget) { - const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - return this.scene.phaseManager.getCurrentPhase()?.constructor.name === targetName; + isCurrentPhase(phaseTarget: PhaseString): boolean; + /** + * Checks if the current phase matches the target phase. + * @param phaseTarget - The target phase. + * @returns Whether the current phase matches the target phase + * @deprecated - Use phaseClass + */ + isCurrentPhase(phaseTarget: PhaseClass): boolean; + isCurrentPhase(phaseTarget: PhaseString | PhaseClass): boolean { + const targetName = typeof phaseTarget === "string" ? phaseTarget : (phaseTarget.name as PhaseString); + return this.scene.phaseManager.getCurrentPhase()?.is(targetName) ?? false; } /** @@ -503,7 +517,7 @@ export class GameManager { * @param inPhase - Which phase to expect the selection to occur in. Defaults to `SwitchPhase` * (which is where the majority of non-command switch operations occur). */ - doSelectPartyPokemon(slot: number, inPhase = "SwitchPhase") { + doSelectPartyPokemon(slot: number, inPhase: PhaseString = "SwitchPhase") { this.onNextPrompt(inPhase, UiMode.PARTY, () => { const partyHandler = this.scene.ui.getHandler() as PartyUiHandler; diff --git a/test/test-utils/game-wrapper.ts b/test/test-utils/game-wrapper.ts index 1a906bf8492..8069da027ef 100644 --- a/test/test-utils/game-wrapper.ts +++ b/test/test-utils/game-wrapper.ts @@ -47,12 +47,14 @@ export class GameWrapper { public scene: BattleScene; constructor(phaserGame: Phaser.Game, bypassLogin: boolean) { + // TODO: Figure out how to actually set RNG states correctly Phaser.Math.RND.sow(["test"]); // vi.spyOn(Utils, "apiFetch", "get").mockReturnValue(fetch); if (bypassLogin) { vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true); } this.game = phaserGame; + // TODO: Move these mocks elsewhere MoveAnim.prototype.getAnim = () => ({ frames: {}, }); @@ -71,10 +73,16 @@ export class GameWrapper { PokedexMonContainer.prototype.remove = MockContainer.prototype.remove; } - setScene(scene: BattleScene) { + /** + * Initialize the given {@linkcode BattleScene} and override various properties to avoid crashes with headless games. + * @param scene - The {@linkcode BattleScene} to initialize + * @returns A Promise that resolves once the initialization process has completed. + */ + // TODO: is asset loading & method overriding actually needed for a headless renderer? + async setScene(scene: BattleScene): Promise { this.scene = scene; this.injectMandatory(); - this.scene.preload?.(); + this.scene.preload(); this.scene.create(); } diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts new file mode 100644 index 00000000000..989c09ff4be --- /dev/null +++ b/test/test-utils/helpers/prompt-handler.ts @@ -0,0 +1,159 @@ +import type { AwaitableUiHandler } from "#app/ui/awaitable-ui-handler"; +import { UiMode } from "#enums/ui-mode"; +import type { GameManager } from "#test/test-utils/game-manager"; +import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import type { PhaseString } from "#types/phase-types"; +import chalk from "chalk"; +import { type MockInstance, vi } from "vitest"; + +interface UIPrompt { + /** The {@linkcode PhaseString | name} of the Phase during which to execute the callback. */ + phaseTarget: PhaseString; + /** The {@linkcode UIMode} to wait for. */ + mode: UiMode; + /** The callback function to execute. */ + callback: () => void; + /** + * An optional callback function to determine if the prompt has expired and should be removed. + * Expired prompts are removed upon the next UI mode change without executing their callback. + */ + expireFn?: () => boolean; + /** + * If `true`, restricts the prompt to only activate when the current {@linkcode AwaitableUiHandler} is waiting for input. + * @defaultValue `false` + */ + awaitingActionInput: boolean; +} + +/** + * Array of phases that hang whiile waiting for player input. + * Changing UI modes during these phases will halt the phase interceptor. + * @todo This is an extremely unintuitive solution that only works on a select few phases + * and does not account for UI handlers not accepting input + */ +const endBySetMode: ReadonlyArray = [ + "CommandPhase", + "TitlePhase", + "SelectGenderPhase", + "SelectStarterPhase", + "SelectModifierPhase", + "MysteryEncounterPhase", + "PostMysteryEncounterPhase", +]; + +/** Helper class to handle executing prompts upon UI mode changes. */ +export class PromptHandler extends GameManagerHelper { + /** An array of {@linkcode UIPrompt | prompts} with associated callbacks. */ + private prompts: UIPrompt[] = []; + /** The original `setModeInternal` function, stored for use in {@linkcode setMode}. */ + private originalSetModeInternal: (typeof this.game.scene.ui)["setModeInternal"]; + + public static runInterval?: NodeJS.Timeout; + + constructor(game: GameManager) { + super(game); + this.originalSetModeInternal = this.game.scene.ui["setModeInternal"]; + // `any` assertion needed as we are mocking private property + ( + vi.spyOn(this.game.scene.ui as any, "setModeInternal") as MockInstance< + (typeof this.game.scene.ui)["setModeInternal"] + > + ).mockImplementation((...args) => this.setMode(args)); + + // Set an interval to repeatedly check the current prompt. + if (PromptHandler.runInterval) { + throw new Error("Prompt handler run interval was not properly cleared on test end!"); + } + PromptHandler.runInterval = setInterval(() => this.doPromptCheck()); + } + + /** + * Helper method to wrap UI mode changing. + * @param args - Arguments being passed to the original method + * @returns The original return value. + */ + private setMode(args: Parameters) { + const mode = args[0]; + + this.doLog(`UI mode changed to ${UiMode[mode]} (=${mode})!`); + const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args) as ReturnType< + typeof this.originalSetModeInternal + >; + + const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName!; + if (endBySetMode.includes(currentPhase)) { + this.game.phaseInterceptor.checkMode(); + } + return ret; + } + + /** + * Method to perform prompt handling every so often. + * @param uiMode - The {@linkcode UiMode} being set + */ + private doPromptCheck(): void { + if (this.prompts.length === 0) { + return; + } + + const prompt = this.prompts[0]; + + // remove expired prompts + if (prompt.expireFn?.()) { + this.prompts.shift(); + return; + } + + const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName; + const currentHandler = this.game.scene.ui.getHandler(); + const mode = this.game.scene.ui.getMode(); + + // If the current mode, phase, and handler match the expected values, execute the callback and continue. + // If not, leave it there. + if ( + mode === prompt.mode && + currentPhase === prompt.phaseTarget && + currentHandler.active && + !(prompt.awaitingActionInput && !(currentHandler as AwaitableUiHandler)["awaitingActionInput"]) + ) { + prompt.callback(); + this.prompts.shift(); + } + } + + /** + * Queue a callback to be executed on the next UI mode change. + * This can be used to (among other things) simulate inputs or run callbacks mid-phase. + * @param phaseTarget - The {@linkcode PhaseString | name} of the Phase during which the callback will be executed + * @param mode - The {@linkcode UiMode} to wait for + * @param callback - The callback function to execute + * @param expireFn - Optional function to determine if the prompt has expired + * @param awaitingActionInput - If `true`, restricts the prompt to only activate when the current {@linkcode AwaitableUiHandler} is waiting for input; default `false` + * @remarks + * If multiple prompts are queued up in succession, each will be checked in turn **until the first prompt that neither expires nor matches**. + * @todo Review all uses of this function to check if they can be made synchronous + */ + public addToNextPrompt( + phaseTarget: PhaseString, + mode: UiMode, + callback: () => void, + expireFn?: () => boolean, + awaitingActionInput = false, + ) { + this.prompts.push({ + phaseTarget, + mode, + callback, + expireFn, + awaitingActionInput, + }); + } + + /** + * Wrapper function to add green coloration to phase logs. + * @param args - Arguments to original logging function. + */ + private doLog(...args: unknown[]): void { + console.log(chalk.hex("#ffa500")(...args)); + } +} diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index 7166f1b6cf9..7afc119d41d 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -38,11 +38,7 @@ export class ReloadHelper extends GameManagerHelper { scene.phaseManager.clearPhaseQueue(); // Set the last saved session to the desired session data - vi.spyOn(scene.gameData, "getSession").mockReturnValue( - new Promise((resolve, _reject) => { - resolve(this.sessionData); - }), - ); + vi.spyOn(scene.gameData, "getSession").mockReturnValue(Promise.resolve(this.sessionData)); scene.phaseManager.unshiftPhase(titlePhase); this.game.endPhase(); // End the currently ongoing battle @@ -56,8 +52,7 @@ export class ReloadHelper extends GameManagerHelper { ); this.game.scene.modifiers = []; } - titlePhase.loadSaveSlot(-1); // Load the desired session data - this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up + await titlePhase.loadSaveSlot(-1); // Load the desired session data // Run through prompts for switching Pokemon, copied from classicModeHelper.ts if (this.game.scene.battleStyle === BattleStyle.SWITCH) { diff --git a/test/test-utils/mocks/mock-phase.ts b/test/test-utils/mocks/mock-phase.ts new file mode 100644 index 00000000000..3d4e4870cd5 --- /dev/null +++ b/test/test-utils/mocks/mock-phase.ts @@ -0,0 +1,11 @@ +import { Phase } from "#app/phase"; +/** + * A rudimentary mock of a phase. + * Ends upon starting by default. + */ +export abstract class mockPhase extends Phase { + public phaseName: any; + start() { + this.end(); + } +} diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 0d357a75557..71d0aeee92e 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,445 +1,224 @@ +import type { PhaseString } from "#app/@types/phase-types"; import type { BattleScene } from "#app/battle-scene"; -import { Phase } from "#app/phase"; +import type { Phase } from "#app/phase"; +import type { Constructor } from "#app/utils/common"; import { UiMode } from "#enums/ui-mode"; -import { AttemptRunPhase } from "#phases/attempt-run-phase"; -import { BattleEndPhase } from "#phases/battle-end-phase"; -import { BerryPhase } from "#phases/berry-phase"; -import { CheckSwitchPhase } from "#phases/check-switch-phase"; -import { CommandPhase } from "#phases/command-phase"; -import { DamageAnimPhase } from "#phases/damage-anim-phase"; -import { EggLapsePhase } from "#phases/egg-lapse-phase"; -import { EncounterPhase } from "#phases/encounter-phase"; -import { EndEvolutionPhase } from "#phases/end-evolution-phase"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { EvolutionPhase } from "#phases/evolution-phase"; -import { ExpPhase } from "#phases/exp-phase"; -import { FaintPhase } from "#phases/faint-phase"; -import { FormChangePhase } from "#phases/form-change-phase"; -import { GameOverModifierRewardPhase } from "#phases/game-over-modifier-reward-phase"; -import { GameOverPhase } from "#phases/game-over-phase"; -import { LearnMovePhase } from "#phases/learn-move-phase"; -import { LevelCapPhase } from "#phases/level-cap-phase"; -import { LoginPhase } from "#phases/login-phase"; -import { MessagePhase } from "#phases/message-phase"; -import { ModifierRewardPhase } from "#phases/modifier-reward-phase"; -import { MoveEffectPhase } from "#phases/move-effect-phase"; -import { MoveEndPhase } from "#phases/move-end-phase"; -import { MovePhase } from "#phases/move-phase"; -import { - MysteryEncounterBattlePhase, - MysteryEncounterOptionSelectedPhase, - MysteryEncounterPhase, - MysteryEncounterRewardsPhase, - PostMysteryEncounterPhase, -} from "#phases/mystery-encounter-phases"; -import { NewBattlePhase } from "#phases/new-battle-phase"; -import { NewBiomeEncounterPhase } from "#phases/new-biome-encounter-phase"; -import { NextEncounterPhase } from "#phases/next-encounter-phase"; -import { PartyExpPhase } from "#phases/party-exp-phase"; -import { PartyHealPhase } from "#phases/party-heal-phase"; -import { PokemonTransformPhase } from "#phases/pokemon-transform-phase"; -import { PositionalTagPhase } from "#phases/positional-tag-phase"; -import { PostGameOverPhase } from "#phases/post-game-over-phase"; -import { PostSummonPhase } from "#phases/post-summon-phase"; -import { QuietFormChangePhase } from "#phases/quiet-form-change-phase"; -import { RevivalBlessingPhase } from "#phases/revival-blessing-phase"; -import { RibbonModifierRewardPhase } from "#phases/ribbon-modifier-reward-phase"; -import { SelectBiomePhase } from "#phases/select-biome-phase"; -import { SelectGenderPhase } from "#phases/select-gender-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; -import { SelectStarterPhase } from "#phases/select-starter-phase"; -import { SelectTargetPhase } from "#phases/select-target-phase"; -import { ShinySparklePhase } from "#phases/shiny-sparkle-phase"; -import { ShowAbilityPhase } from "#phases/show-ability-phase"; -import { StatStageChangePhase } from "#phases/stat-stage-change-phase"; -import { SummonPhase } from "#phases/summon-phase"; -import { SwitchPhase } from "#phases/switch-phase"; -import { SwitchSummonPhase } from "#phases/switch-summon-phase"; -import { TitlePhase } from "#phases/title-phase"; -import { ToggleDoublePositionPhase } from "#phases/toggle-double-position-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; -import { TurnStartPhase } from "#phases/turn-start-phase"; -import { UnavailablePhase } from "#phases/unavailable-phase"; -import { UnlockPhase } from "#phases/unlock-phase"; -import { VictoryPhase } from "#phases/victory-phase"; -import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; -import type { PhaseClass, PhaseString } from "#types/phase-types"; -import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; -import { UI } from "#ui/ui"; +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports +import type { GameManager } from "#test/test-utils/game-manager"; +import type { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports +import { format } from "util"; +import chalk from "chalk"; +import { vi } from "vitest"; -export interface PromptHandler { - phaseTarget?: string; - mode?: UiMode; - callback?: () => void; - expireFn?: () => void; - awaitingActionInput?: boolean; -} +/** + * A Set containing phase names that will not be shown in the console when started. + * + * Used to reduce console noise from very repetitive phases. + */ +const blacklistedPhaseNames: ReadonlySet = new Set(["ActivatePriorityQueuePhase"]); -type PhaseInterceptorPhase = PhaseClass | PhaseString; - -interface PhaseStub { - start(): void; - endBySetMode: boolean; -} - -interface InProgressStub { - name: string; - callback(): void; - onError(error: any): void; -} - -interface onHoldStub { - name: string; - call(): void; -} +/** + * The interceptor's current state. + * Possible values are the following: + * - `running`: The interceptor is currently running a phase. + * - `interrupted`: The interceptor has been interrupted by a UI prompt or similar mechanism, + * and is currently waiting for the current phase to end. + * - `idling`: The interceptor is not currently running a phase and is ready to start a new one. + */ +type StateType = "running" | "interrupted" | "idling"; +/** + * The PhaseInterceptor is a wrapper around the `BattleScene`'s {@linkcode PhaseManager}. + * It allows tests to exert finer control over the phase system, providing logging, manual advancing, and other helpful utilities. + */ export class PhaseInterceptor { - public scene: BattleScene; - // @ts-expect-error: initialized in `initPhases` - public phases: Record = {}; - public log: PhaseString[]; + private scene: BattleScene; /** - * TODO: This should not be an array; - * Our linear phase system means only 1 phase is ever started at once (if any) + * A log containing all phases having been executed in FIFO order. \ + * Entries are appended each time {@linkcode run} is called, and can be cleared manually with {@linkcode clearLogs}. */ - private onHold: onHoldStub[]; - private interval: NodeJS.Timeout; - private promptInterval: NodeJS.Timeout; - private intervalRun: NodeJS.Timeout; - private prompts: PromptHandler[]; - private inProgress?: InProgressStub; - private originalSetMode: UI["setMode"]; - private originalSuperEnd: Phase["end"]; - + public log: PhaseString[] = []; /** - * List of phases with their corresponding start methods. - * - * CAUTION: If a phase and its subclasses (if any) both appear in this list, - * make sure that this list contains said phase AFTER all of its subclasses. - * This way, the phase's `prototype.start` is properly preserved during - * `initPhases()` so that its subclasses can use `super.start()` properly. + * The interceptor's current state. + * Possible values are the following: + * - `running`: The interceptor is currently running a phase. + * - `interrupted`: The interceptor has been interrupted by a UI prompt and is waiting for the caller to end it. + * - `idling`: The interceptor is not currently running a phase and is ready to start a new one. */ - private PHASES = [ - LoginPhase, - TitlePhase, - SelectGenderPhase, - NewBiomeEncounterPhase, - SelectStarterPhase, - PostSummonPhase, - SummonPhase, - ToggleDoublePositionPhase, - CheckSwitchPhase, - ShowAbilityPhase, - MessagePhase, - TurnInitPhase, - CommandPhase, - EnemyCommandPhase, - TurnStartPhase, - MovePhase, - MoveEffectPhase, - DamageAnimPhase, - FaintPhase, - BerryPhase, - TurnEndPhase, - BattleEndPhase, - EggLapsePhase, - SelectModifierPhase, - NextEncounterPhase, - NewBattlePhase, - VictoryPhase, - LearnMovePhase, - MoveEndPhase, - StatStageChangePhase, - ShinySparklePhase, - SelectTargetPhase, - UnavailablePhase, - QuietFormChangePhase, - SwitchPhase, - SwitchSummonPhase, - PartyHealPhase, - FormChangePhase, - EvolutionPhase, - EndEvolutionPhase, - LevelCapPhase, - AttemptRunPhase, - SelectBiomePhase, - PositionalTagPhase, - PokemonTransformPhase, - MysteryEncounterPhase, - MysteryEncounterOptionSelectedPhase, - MysteryEncounterBattlePhase, - MysteryEncounterRewardsPhase, - PostMysteryEncounterPhase, - RibbonModifierRewardPhase, - GameOverModifierRewardPhase, - ModifierRewardPhase, - PartyExpPhase, - ExpPhase, - EncounterPhase, - GameOverPhase, - UnlockPhase, - PostGameOverPhase, - RevivalBlessingPhase, - ]; + private state: StateType = "idling"; - private endBySetMode = [ - TitlePhase, - SelectGenderPhase, - CommandPhase, - SelectStarterPhase, - SelectModifierPhase, - MysteryEncounterPhase, - PostMysteryEncounterPhase, - ]; + private target: PhaseString; /** * Constructor to initialize the scene and properties, and to start the phase handling. - * @param scene - The scene to be managed. + * @param scene - The scene to be managed */ constructor(scene: BattleScene) { this.scene = scene; - this.onHold = []; - this.prompts = []; - this.clearLogs(); - this.startPromptHandler(); - this.initPhases(); - } - - /** - * Clears phase logs - */ - clearLogs() { - this.log = []; - } - - rejectAll(error) { - if (this.inProgress) { - clearInterval(this.promptInterval); - clearInterval(this.interval); - clearInterval(this.intervalRun); - this.inProgress.onError(error); - } + // Mock the private startCurrentPhase method to toggle `isRunning` rather than actually starting anything + vi.spyOn(this.scene.phaseManager as any, "startCurrentPhase").mockImplementation(() => { + this.state = "idling"; + }); } /** * Method to transition to a target phase. - * @param phaseTo - The phase to transition to. - * @param runTarget - Whether or not to run the target phase; default `true`. - * @returns A promise that resolves when the transition is complete. + * @param target - The name of the {@linkcode Phase} to transition to + * @param runTarget - Whether or not to run the target phase before resolving; default `true` + * @returns A Promise that resolves once {@linkcode target} has been reached. + * @todo remove `Constructor` from type signature in favor of phase strings + * @remarks + * This will not resolve for *any* reason until the target phase has been reached. + * @example + * await game.phaseInterceptor.to("MoveEffectPhase", false); */ - async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise { - return new Promise(async (resolve, reject) => { - ErrorInterceptor.getInstance().add(this); - const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name; - this.intervalRun = setInterval(async () => { - const currentPhase = this.onHold?.length && this.onHold[0]; - if (!currentPhase) { - // No current phase means the manager either hasn't started yet - // or we were interrupted by prompt; wait for phase to finish - return; - } + public async to(target: PhaseString | Constructor, runTarget = true): Promise { + this.target = typeof target === "string" ? target : (target.name as PhaseString); - // If current phase is different, run it and wait for it to finish. - if (currentPhase.name !== targetName) { - await this.run().catch(e => { - clearInterval(this.intervalRun); - return reject(e); - }); - return; - } + const pm = this.scene.phaseManager; - // Hit target phase; run it and resolve - clearInterval(this.intervalRun); - if (!runTarget) { - return resolve(); - } - await this.run().catch(e => { - clearInterval(this.intervalRun); - return reject(e); - }); - return resolve(); - }); - }); - } + // TODO: remove bangs once signature is updated + let currentPhase: Phase = pm.getCurrentPhase()!; - /** - * Method to run the current phase with an optional skip function. - * @returns A promise that resolves when the phase is run. - */ - private run(): Promise { - // @ts-expect-error: This is apparently mandatory to avoid a crash; review if this is needed - this.scene.moveAnimations = null; - return new Promise(async (resolve, reject) => { - ErrorInterceptor.getInstance().add(this); - const interval = setInterval(async () => { - const currentPhase = this.onHold.shift(); - if (currentPhase) { - clearInterval(interval); - this.inProgress = { - name: currentPhase.name, - callback: () => { - ErrorInterceptor.getInstance().remove(this); - resolve(); - }, - onError: error => reject(error), - }; - currentPhase.call(); - } - }); - }); - } + let didLog = false; - /** - * Remove the current phase from the phase interceptor. - * - * Do not call this unless absolutely necessary. This function is intended - * for cleaning up the phase interceptor when, for whatever reason, a phase - * is manually ended without using the phase interceptor. - * - * @param shouldRun Whether or not the current scene should also be run. - */ - shiftPhase(shouldRun = false): void { - this.onHold.shift(); - if (shouldRun) { - this.scene.phaseManager.shiftPhase(); - } - } - - /** - * Method to initialize phases and their corresponding methods. - */ - initPhases() { - this.originalSetMode = UI.prototype.setMode; - this.originalSuperEnd = Phase.prototype.end; - UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args); - Phase.prototype.end = () => this.superEndPhase.call(this); - for (const phase of this.PHASES) { - const originalStart = phase.prototype.start; - this.phases[phase.name] = { - start: originalStart, - endBySetMode: this.endBySetMode.some(elm => elm.name === phase.name), - }; - phase.prototype.start = () => this.startPhase.call(this, phase); - } - } - - /** - * Method to start a phase and log it. - * @param phase - The phase to start. - */ - startPhase(phase: PhaseClass) { - this.log.push(phase.name as PhaseString); - const instance = this.scene.phaseManager.getCurrentPhase(); - this.onHold.push({ - name: phase.name, - call: () => { - this.phases[phase.name].start.apply(instance); - }, - }); - } - - /** - * Method to end a phase and log it. - * @param phase - The phase to start. - */ - private superEndPhase() { - const instance = this.scene.phaseManager.getCurrentPhase(); - this.originalSuperEnd.apply(instance); - this.inProgress?.callback(); - this.inProgress = undefined; - } - - /** - * m2m to set mode. - * @param mode - The {@linkcode UiMode} to set. - * @param args - Additional arguments to pass to the original method. - */ - setMode(mode: UiMode, ...args: unknown[]): Promise { - // TODO: remove the `!` in PR 6243 / after PR 6243 is merged - const currentPhase = this.scene.phaseManager.getCurrentPhase()!; - const instance = this.scene.ui; - console.log("setMode", `${UiMode[mode]} (=${mode})`, args); - const ret = this.originalSetMode.apply(instance, [mode, ...args]); - if (!this.phases[currentPhase.constructor.name]) { - throw new Error( - `missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list --- Add it to PHASES inside of /test/utils/phaseInterceptor.ts`, - ); - } - if (this.phases[currentPhase.constructor.name].endBySetMode) { - this.inProgress?.callback(); - this.inProgress = undefined; - } - return ret; - } - - /** - * Method to start the prompt handler. - */ - startPromptHandler() { - this.promptInterval = setInterval(() => { - if (this.prompts.length) { - const actionForNextPrompt = this.prompts[0]; - const expireFn = actionForNextPrompt.expireFn?.(); - const currentMode = this.scene.ui.getMode(); - const currentPhase = this.scene.phaseManager.getCurrentPhase()?.constructor.name; - const currentHandler = this.scene.ui.getHandler(); - if (expireFn) { - this.prompts.shift(); - } else if ( - currentMode === actionForNextPrompt.mode && - currentPhase === actionForNextPrompt.phaseTarget && - currentHandler.active && - (!actionForNextPrompt.awaitingActionInput || - (actionForNextPrompt.awaitingActionInput && (currentHandler as AwaitableUiHandler)["awaitingActionInput"])) - ) { - const prompt = this.prompts.shift(); - if (prompt?.callback) { - prompt.callback(); + // NB: This has to use an interval to wait for UI prompts to activate. + // TODO: Rework after UI rework + await vi.waitUntil( + async () => { + // If we were interrupted by a UI prompt, we assume that the calling code will queue inputs to + // end the current phase manually, so we just wait for the phase to end from the caller. + if (this.state === "interrupted") { + if (!didLog) { + this.doLog("PhaseInterceptor.to: Waiting for phase to end after being interrupted!"); + didLog = true; } + return false; } - } - }); - } - /** - * Method to add an action to the next prompt. - * @param phaseTarget - The target phase for the prompt. - * @param mode - The mode of the UI. - * @param callback - The callback function to execute. - * @param expireFn - The function to determine if the prompt has expired. - * @param awaitingActionInput - ???; default `false` - */ - addToNextPrompt( - phaseTarget: string, - mode: UiMode, - callback: () => void, - expireFn?: () => void, - awaitingActionInput = false, - ) { - this.prompts.push({ - phaseTarget, - mode, - callback, - expireFn, - awaitingActionInput, - }); - } + currentPhase = pm.getCurrentPhase()!; + // TODO: Remove proof-of-concept error throw after signature update + if (!currentPhase) { + throw new Error("currentPhase is null after being started!"); + } - /** - * Restores the original state of phases and clears intervals. - * - * This function iterates through all phases and resets their `start` method to the original - * function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`. - */ - restoreOg() { - for (const phase of this.PHASES) { - phase.prototype.start = this.phases[phase.name].start; + if (currentPhase.is(this.target)) { + return true; + } + + // Current phase is different; run and wait for it to finish. + await this.run(currentPhase); + return false; + }, + { interval: 0, timeout: 20_000 }, + ); + + // We hit the target; run as applicable and wrap up. + if (!runTarget) { + this.doLog(`PhaseInterceptor.to: Stopping before running ${this.target}`); + return; } - UI.prototype.setMode = this.originalSetMode; - Phase.prototype.end = this.originalSuperEnd; - clearInterval(this.promptInterval); - clearInterval(this.interval); - clearInterval(this.intervalRun); + + await this.run(currentPhase); + this.doLog( + `PhaseInterceptor.to: Stopping ${this.state === "interrupted" ? `after reaching UiMode.${UiMode[this.scene.ui.getMode()]} during` : "on completion of"} ${this.target}`, + ); + } + + /** + * Internal wrapper method to start a phase and wait until it finishes. + * @param currentPhase - The {@linkcode Phase} to run + * @returns A Promise that resolves when the phase has completed running. + */ + private async run(currentPhase: Phase): Promise { + try { + this.state = "running"; + this.logPhase(currentPhase.phaseName); + currentPhase.start(); + await vi.waitUntil( + () => this.state !== "running", + { interval: 50, timeout: 20_000 }, // TODO: Figure out an appropriate timeout for individual phases + ); + } catch (error) { + throw error instanceof Error + ? error + : new Error( + `Unknown error occurred while running phase ${currentPhase.phaseName}!\nError: ${format("%O", error)}`, + ); + } + } + + /** + * If this is at the target phase, unlock the interceptor and + * return control back to the caller once the calling phase has finished. + * @remarks + * This should not be called by anything other than {@linkcode PromptHandler}. + */ + public checkMode(): void { + const currentPhase = this.scene.phaseManager.getCurrentPhase()!; + if (!currentPhase.is(this.target) || this.state === "interrupted") { + // Wrong phase / already interrupted = do nothing + return; + } + + // Interrupt the phase and return control to the caller + this.state = "interrupted"; + } + + /** + * Skip the next upcoming phase. + * @throws Error if currently running a phase. + * @remarks + * This function should be used for skipping phases _not yet started_. + * To end ones already in the process of running, use {@linkcode GameManager.endPhase}. + * @example + * await game.phaseInterceptor.to("LoginPhase", false); + * game.phaseInterceptor.shiftPhase(); + */ + public shiftPhase(): void { + const phaseName = this.scene.phaseManager.getCurrentPhase()!.phaseName; + if (this.state !== "idling") { + throw new Error(`shiftPhase attempted to skip phase ${phaseName} mid-execution!`); + } + this.doLog(`Skipping current phase ${phaseName}`); + this.scene.phaseManager.shiftPhase(); + } + + /** + * Deprecated no-op function. + * + * This was previously used to reset timers created using `setInterval` to wait for phase end + * and undo various method stubs upon a test ending. \ + * However, since we now use {@linkcode vi.waitUntil} and {@linkcode vi.spyOn} to perform these tasks + * respectively, this function has become no longer needed. + * @deprecated This is no longer needed and will be removed in a future PR + */ + public restoreOg() {} + + /** + * Method to log the start of a phase. + * @param phaseName - The name of the phase to log. + */ + private logPhase(phaseName: PhaseString) { + if (!blacklistedPhaseNames.has(phaseName)) { + console.log(`%cStart Phase: ${phaseName}`, "color:green"); + } + this.log.push(phaseName); + } + + /** + * Clear all prior phase logs. + */ + public clearLogs(): void { + this.log = []; + } + + /** + * Wrapper function to add coral coloration to phase logs. + * @param args - Arguments to original logging function. + */ + private doLog(...args: unknown[]): void { + console.log(chalk.hex("#ff7f50")(...args)); } } diff --git a/test/test-utils/tests/helpers/prompt-handler.test.ts b/test/test-utils/tests/helpers/prompt-handler.test.ts new file mode 100644 index 00000000000..4169e946dbe --- /dev/null +++ b/test/test-utils/tests/helpers/prompt-handler.test.ts @@ -0,0 +1,153 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import type { Phase } from "#app/phase"; +import type { AwaitableUiHandler } from "#app/ui/awaitable-ui-handler"; +import { UiMode } from "#enums/ui-mode"; +import type { GameManager } from "#test/test-utils/game-manager"; +import { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; +import type { PhaseInterceptor } from "#test/test-utils/phase-interceptor"; +import type { UI } from "#ui/ui"; +import { beforeAll, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; + +describe("Test Utils - PromptHandler", () => { + let promptHandler: PromptHandler; + let handler: AwaitableUiHandler; + + let callback1: Mock; + let callback2: Mock; + let setModeCallback: Mock; + let checkModeCallback: Mock; + + beforeAll(() => { + setModeCallback = vi.fn(); + checkModeCallback = vi.fn(); + callback1 = vi.fn(() => console.log("callback 1 called!")).mockName("callback 1"); + callback2 = vi.fn(() => console.log("callback 2 called!")).mockName("callback 2"); + }); + + beforeEach(() => { + handler = { + active: true, + show: () => {}, + awaitingActionInput: true, + } as unknown as AwaitableUiHandler; + + promptHandler = new PromptHandler({ + scene: { + ui: { + getHandler: () => handler, + setModeInternal: () => { + setModeCallback(); + return Promise.resolve(); + }, + getMode: () => UiMode.TEST_DIALOGUE, + } as unknown as UI, + phaseManager: { + getCurrentPhase: () => + ({ + phaseName: "CommandPhase", + }) as unknown as Phase, + }, + }, + phaseInterceptor: { + checkMode: () => { + checkModeCallback(); + }, + } as PhaseInterceptor, + } as GameManager); + }); + + function onNextPrompt( + target: string, + mode: UiMode, + callback: () => void, + expireFn?: () => boolean, + awaitingActionInput = false, + ) { + promptHandler.addToNextPrompt(target as unknown as PhaseString, mode, callback, expireFn, awaitingActionInput); + } + + describe("setMode", () => { + it("should wrap and pass along original function arguments from setModeInternal", async () => { + const setModeSpy = vi.spyOn(promptHandler as any, "setMode"); + promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); + + expect(setModeSpy).toHaveBeenCalledExactlyOnceWith([UiMode.PARTY, false, false, false, []]); + expect(setModeCallback).toHaveBeenCalledAfter(setModeSpy); + }); + + it("should call PhaseInterceptor.checkMode if current phase in `endBySetMode`", async () => { + promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); + + expect(checkModeCallback).toHaveBeenCalledOnce(); + }); + }); + + describe("doPromptCheck", () => { + it("should check and remove the first prompt", async () => { + onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1()); + onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback2()); + promptHandler["doPromptCheck"](); + + expect(callback1).toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(promptHandler["prompts"]).toHaveLength(1); + }); + + it.each<{ reason: string; callback: () => void }>([ + { + reason: "wrong UI mode", + callback: () => onNextPrompt("testDialoguePhase", UiMode.ACHIEVEMENTS, () => callback1()), + }, + { + reason: "wrong phase", + callback: () => onNextPrompt("wrong phase", UiMode.TEST_DIALOGUE, () => callback1()), + }, + { + reason: "UI handler is inactive", + callback: () => { + handler.active = false; + onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1()); + }, + }, + { + reason: "UI handler is not awaiting input", + callback: () => { + handler["awaitingActionInput"] = false; + onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1(), undefined, true); + }, + }, + ])("should skip callback and keep in queue if $reason", async ({ callback }) => { + callback(); + onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback2); + promptHandler["doPromptCheck"](); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(promptHandler["prompts"]).toHaveLength(2); + }); + + it("should remove expired prompts without blocking", async () => { + onNextPrompt( + "testDialoguePhase", + UiMode.TEST_DIALOGUE, + () => callback1(), + () => true, + ); + onNextPrompt( + "testDialoguePhase", + UiMode.TEST_DIALOGUE, + () => callback2(), + () => false, + ); + promptHandler["doPromptCheck"](); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(promptHandler["prompts"]).toHaveLength(1); + + promptHandler["doPromptCheck"](); + expect(callback2).toHaveBeenCalledOnce(); + expect(promptHandler["prompts"]).toHaveLength(0); + }); + }); +}); diff --git a/test/test-utils/tests/phase-interceptor/integration.test.ts b/test/test-utils/tests/phase-interceptor/integration.test.ts new file mode 100644 index 00000000000..4e381668001 --- /dev/null +++ b/test/test-utils/tests/phase-interceptor/integration.test.ts @@ -0,0 +1,62 @@ +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { UiMode } from "#enums/ui-mode"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Utils - Phase Interceptor - Integration", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + }); + + it("runToTitle", async () => { + await game.runToTitle(); + + expect(game.scene.ui.getMode()).toBe(UiMode.TITLE); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("TitlePhase"); + }); + + it("runToSummon", async () => { + await game.classicMode.runToSummon([SpeciesId.ABOMASNOW]); + + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("SummonPhase"); + }); + + it("startBattle", async () => { + await game.classicMode.startBattle([SpeciesId.RABOOT]); + + expect(game.scene.ui.getMode()).toBe(UiMode.COMMAND); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); + + it("1 Full Turn", async () => { + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toNextTurn(); + + expect(game.scene.ui.getMode()).toBe(UiMode.COMMAND); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); + + it("should not break when phase ended early via prompt", async () => { + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); + game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { + game.endPhase(); + }); + + game.move.use(MoveId.BOUNCE); + await game.phaseInterceptor.to("EnemyCommandPhase"); + }); +}); diff --git a/test/test-utils/tests/phase-interceptor/unit.test.ts b/test/test-utils/tests/phase-interceptor/unit.test.ts new file mode 100644 index 00000000000..789309baa70 --- /dev/null +++ b/test/test-utils/tests/phase-interceptor/unit.test.ts @@ -0,0 +1,150 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import { globalScene } from "#app/global-scene"; +import type { Phase } from "#app/phase"; +import type { Constructor } from "#app/utils/common"; +import { GameManager } from "#test/test-utils/game-manager"; +import { mockPhase } from "#test/test-utils/mocks/mock-phase"; +import Phaser from "phaser"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +// TODO: Move these to `mock-phase.ts` if/when unit tests for the phase manager are created +class applePhase extends mockPhase { + public readonly phaseName = "applePhase"; +} + +class bananaPhase extends mockPhase { + public readonly phaseName = "bananaPhase"; +} + +class coconutPhase extends mockPhase { + public readonly phaseName = "coconutPhase"; +} + +class oneSecTimerPhase extends mockPhase { + public readonly phaseName = "oneSecTimerPhase"; + start() { + setTimeout(() => { + console.log("1 sec passed!"); + this.end(); + }, 1000); + } +} + +class unshifterPhase extends mockPhase { + public readonly phaseName = "unshifterPhase"; + start() { + globalScene.phaseManager.unshiftPhase(new applePhase() as unknown as Phase); + globalScene.phaseManager.unshiftPhase(new bananaPhase() as unknown as Phase); + globalScene.phaseManager.unshiftPhase(new coconutPhase() as unknown as Phase); + this.end(); + } +} + +describe("Utils - Phase Interceptor - Unit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + setPhases(applePhase, bananaPhase, coconutPhase, bananaPhase, coconutPhase); + }); + + /** + * Helper function to set the phase manager's phases to the specified values and start the first one. + * @param phases - An array of constructors to {@linkcode Phase}s to set. + * Constructors must have no arguments. + */ + function setPhases(phase: Constructor, ...phases: Constructor[]) { + game.scene.phaseManager.clearAllPhases(); + game.scene.phaseManager.phaseQueue = [phase, ...phases].map(m => new m()) as Phase[]; + 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 ?? "no phase"; + } + + /** Wrapper function to make TS not complain about incompatible argument typing on `PhaseString`. */ + function to(phaseName: string, runTarget = true) { + return game.phaseInterceptor.to(phaseName as unknown as PhaseString, runTarget); + } + + describe("to", () => { + it("should start the specified phase and resolve after it ends", async () => { + await to("applePhase"); + + expect(getCurrentPhaseName()).toBe("bananaPhase"); + expect(getQueuedPhases()).toEqual(["coconutPhase", "bananaPhase", "coconutPhase"]); + expect(game.phaseInterceptor.log).toEqual(["applePhase"]); + }); + + it("should run to the specified phase without starting/logging", async () => { + await to("applePhase", false); + + expect(getCurrentPhaseName()).toBe("applePhase"); + expect(getQueuedPhases()).toEqual(["bananaPhase", "coconutPhase", "bananaPhase", "coconutPhase"]); + expect(game.phaseInterceptor.log).toEqual([]); + + await to("applePhase", false); + + // should not do anything + expect(getCurrentPhaseName()).toBe("applePhase"); + expect(getQueuedPhases()).toEqual(["bananaPhase", "coconutPhase", "bananaPhase", "coconutPhase"]); + expect(game.phaseInterceptor.log).toEqual([]); + }); + + it("should run all phases between start and the first instance of target", async () => { + await to("coconutPhase"); + + expect(getCurrentPhaseName()).toBe("bananaPhase"); + expect(getQueuedPhases()).toEqual(["coconutPhase"]); + expect(game.phaseInterceptor.log).toEqual(["applePhase", "bananaPhase", "coconutPhase"]); + }); + + it("should work on newly unshifted phases", async () => { + setPhases(unshifterPhase, coconutPhase); // adds applePhase, bananaPhase and coconutPhase to queue + await to("bananaPhase"); + + expect(getCurrentPhaseName()).toBe("coconutPhase"); + expect(getQueuedPhases()).toEqual(["coconutPhase"]); + expect(game.phaseInterceptor.log).toEqual(["unshifterPhase", "applePhase", "bananaPhase"]); + }); + + it("should wait for asynchronous phases to end", async () => { + setPhases(oneSecTimerPhase, coconutPhase); + const callback = vi.fn(() => console.log("fffffff")); + const spy = vi.spyOn(oneSecTimerPhase.prototype, "end"); + setTimeout(() => { + callback(); + }, 500); + await to("coconutPhase"); + expect(callback).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe("shift", () => { + it("should skip the next phase in line without starting it", async () => { + const startSpy = vi.spyOn(applePhase.prototype, "start"); + + game.phaseInterceptor.shiftPhase(); + + expect(getCurrentPhaseName()).toBe("bananaPhase"); + expect(getQueuedPhases()).toEqual(["coconutPhase", "bananaPhase", "coconutPhase"]); + expect(startSpy).not.toHaveBeenCalled(); + expect(game.phaseInterceptor.log).toEqual([]); + }); + }); +}); diff --git a/test/vitest.setup.ts b/test/vitest.setup.ts index be35e18e2e9..2fdea7ea661 100644 --- a/test/vitest.setup.ts +++ b/test/vitest.setup.ts @@ -1,6 +1,7 @@ import "vitest-canvas-mock"; +import { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; import { initTests } from "#test/test-utils/test-file-initialization"; -import { afterAll, beforeAll, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, vi } from "vitest"; /** Set the timezone to UTC for tests. */ @@ -48,13 +49,28 @@ vi.mock("i18next", async importOriginal => { return await importOriginal(); }); -global.testFailed = false; - beforeAll(() => { initTests(); + + process + .on("uncaughtException", err => { + clearInterval(PromptHandler.runInterval); + PromptHandler.runInterval = undefined; + throw err; + }) + .on("unhandledRejection", err => { + clearInterval(PromptHandler.runInterval); + PromptHandler.runInterval = undefined; + throw err; + }); }); afterAll(() => { global.server.close(); console.log("Closing i18n MSW server!"); }); + +afterEach(() => { + clearInterval(PromptHandler.runInterval); + PromptHandler.runInterval = undefined; +}); From 55c11de14e8557aa63637629385c064e0d2b5146 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 24 Aug 2025 18:27:16 -0400 Subject: [PATCH 02/30] Documentation updates --- src/phase-manager.ts | 1 + test/test-utils/helpers/prompt-handler.ts | 6 +++- test/test-utils/phase-interceptor.ts | 43 ++++++++++++----------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index e971b732e37..dccf04adfd9 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -400,6 +400,7 @@ export class PhaseManager { * As such, **do not remove or split this method** as it will break integration tests. */ private startCurrentPhase(): void { + // TODO: Remove once signature is updated to no longer contain `null` if (!this.currentPhase) { console.warn("Trying to start null phase!"); return; diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index 989c09ff4be..b4e55e05d59 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -41,13 +41,17 @@ const endBySetMode: ReadonlyArray = [ "PostMysteryEncounterPhase", ]; -/** Helper class to handle executing prompts upon UI mode changes. */ +/** + * Helper class to handle executing prompts upon UI mode changes. + * @todo Remove once a UI overhaul + */ export class PromptHandler extends GameManagerHelper { /** An array of {@linkcode UIPrompt | prompts} with associated callbacks. */ private prompts: UIPrompt[] = []; /** The original `setModeInternal` function, stored for use in {@linkcode setMode}. */ private originalSetModeInternal: (typeof this.game.scene.ui)["setModeInternal"]; + /** A {@linkcode NodeJS.Timeout | Timeout} containing an interval used to check prompts. */ public static runInterval?: NodeJS.Timeout; constructor(game: GameManager) { diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 71d0aeee92e..04f481fb556 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,4 +1,4 @@ -import type { PhaseString } from "#app/@types/phase-types"; +import type { PhaseManager, PhaseString } from "#app/@types/phase-types"; import type { BattleScene } from "#app/battle-scene"; import type { Phase } from "#app/phase"; import type { Constructor } from "#app/utils/common"; @@ -7,9 +7,10 @@ import { UiMode } from "#enums/ui-mode"; import type { GameManager } from "#test/test-utils/game-manager"; import type { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; // biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports -import { format } from "util"; +import { inspect } from "util"; import chalk from "chalk"; import { vi } from "vitest"; +import { getEnumStr } from "./string-utils"; /** * A Set containing phase names that will not be shown in the console when started. @@ -57,7 +58,12 @@ export class PhaseInterceptor { constructor(scene: BattleScene) { this.scene = scene; // Mock the private startCurrentPhase method to toggle `isRunning` rather than actually starting anything - vi.spyOn(this.scene.phaseManager as any, "startCurrentPhase").mockImplementation(() => { + vi.spyOn( + this.scene.phaseManager as unknown as typeof PhaseManager & { + startCurrentPhase: PhaseManager["startCurrentPhase"]; + }, + "startCurrentPhase", + ).mockImplementation(() => { this.state = "idling"; }); } @@ -66,12 +72,14 @@ export class PhaseInterceptor { * Method to transition to a target phase. * @param target - The name of the {@linkcode Phase} to transition to * @param runTarget - Whether or not to run the target phase before resolving; default `true` - * @returns A Promise that resolves once {@linkcode target} has been reached. + * @returns A Promise that resolves once `target` has been reached. * @todo remove `Constructor` from type signature in favor of phase strings * @remarks - * This will not resolve for *any* reason until the target phase has been reached. + * This will not resolve for _any_ reason until the target phase has been reached. * @example + * ```ts * await game.phaseInterceptor.to("MoveEffectPhase", false); + * ``` */ public async to(target: PhaseString | Constructor, runTarget = true): Promise { this.target = typeof target === "string" ? target : (target.name as PhaseString); @@ -79,12 +87,13 @@ export class PhaseInterceptor { const pm = this.scene.phaseManager; // TODO: remove bangs once signature is updated - let currentPhase: Phase = pm.getCurrentPhase()!; + let currentPhase = pm.getCurrentPhase()!; let didLog = false; - // NB: This has to use an interval to wait for UI prompts to activate. - // TODO: Rework after UI rework + // NB: This has to use an interval to wait for UI prompts to activate + // since our UI code effectively stalls when waiting for input. + // This entire function can likely be made synchronous once UI code is moved to a separate scene. await vi.waitUntil( async () => { // If we were interrupted by a UI prompt, we assume that the calling code will queue inputs to @@ -98,11 +107,6 @@ export class PhaseInterceptor { } currentPhase = pm.getCurrentPhase()!; - // TODO: Remove proof-of-concept error throw after signature update - if (!currentPhase) { - throw new Error("currentPhase is null after being started!"); - } - if (currentPhase.is(this.target)) { return true; } @@ -122,7 +126,7 @@ export class PhaseInterceptor { await this.run(currentPhase); this.doLog( - `PhaseInterceptor.to: Stopping ${this.state === "interrupted" ? `after reaching UiMode.${UiMode[this.scene.ui.getMode()]} during` : "on completion of"} ${this.target}`, + `PhaseInterceptor.to: Stopping ${this.state === "interrupted" ? `after reaching ${getEnumStr(UiMode, this.scene.ui.getMode())} during` : "on completion of"} ${this.target}`, ); } @@ -143,9 +147,7 @@ export class PhaseInterceptor { } catch (error) { throw error instanceof Error ? error - : new Error( - `Unknown error occurred while running phase ${currentPhase.phaseName}!\nError: ${format("%O", error)}`, - ); + : new Error(`Unknown error occurred while running phase ${currentPhase.phaseName}!\nError: ${inspect(error)}`); } } @@ -188,8 +190,8 @@ export class PhaseInterceptor { /** * Deprecated no-op function. * - * This was previously used to reset timers created using `setInterval` to wait for phase end - * and undo various method stubs upon a test ending. \ + * This was previously used to reset timers created using `setInterval` to wait for phases to end + * and undo various method stubs after each test run. \ * However, since we now use {@linkcode vi.waitUntil} and {@linkcode vi.spyOn} to perform these tasks * respectively, this function has become no longer needed. * @deprecated This is no longer needed and will be removed in a future PR @@ -198,6 +200,7 @@ export class PhaseInterceptor { /** * Method to log the start of a phase. + * Called in place of {@linkcode PhaseManager.startCurrentPhase} to allow for manual intervention. * @param phaseName - The name of the phase to log. */ private logPhase(phaseName: PhaseString) { @@ -216,7 +219,7 @@ export class PhaseInterceptor { /** * Wrapper function to add coral coloration to phase logs. - * @param args - Arguments to original logging function. + * @param args - Arguments to original logging function */ private doLog(...args: unknown[]): void { console.log(chalk.hex("#ff7f50")(...args)); From 4be227418f08987901e9fe9ef37ff940d9c8ca44 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 24 Aug 2025 21:09:59 -0400 Subject: [PATCH 03/30] Moved default setting setting into init because reasons --- test/test-utils/game-manager.ts | 17 +---------- test/test-utils/helpers/settings-helper.ts | 28 +++++++++++++++---- .../tests/helpers/prompt-handler.test.ts | 4 +-- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index a4045c6c2c5..6557860b3be 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -6,11 +6,8 @@ import overrides from "#app/overrides"; import { modifierTypes } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; import { Button } from "#enums/buttons"; -import { ExpGainsSpeed } from "#enums/exp-gains-speed"; -import { ExpNotification } from "#enums/exp-notification"; import { GameModes } from "#enums/game-modes"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { PlayerGender } from "#enums/player-gender"; import type { PokeballType } from "#enums/pokeball"; import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; @@ -196,18 +193,6 @@ export class GameManager { await this.phaseInterceptor.to("LoginPhase", false); this.phaseInterceptor.shiftPhase(); await this.phaseInterceptor.to("TitlePhase"); - - // TODO: This should be moved to a separate initialization method - this.scene.gameSpeed = 5; - this.scene.moveAnimations = false; - this.scene.showLevelUpStats = false; - this.scene.expGainsSpeed = ExpGainsSpeed.SKIP; - this.scene.expParty = ExpNotification.SKIP; - this.scene.hpBarSpeed = 3; - this.scene.enableTutorials = false; - this.scene.gameData.gender = PlayerGender.MALE; // set initial player gender - this.scene.battleStyle = this.settings.battleStyle; - this.scene.fieldVolume = 0; } /** @@ -424,7 +409,7 @@ export class GameManager { * Checks if the current phase matches the target phase. * @param phaseTarget - The target phase. * @returns Whether the current phase matches the target phase - * @deprecated - Use phaseClass + * @deprecated - Use phaseString */ isCurrentPhase(phaseTarget: PhaseClass): boolean; isCurrentPhase(phaseTarget: PhaseString | PhaseClass): boolean { diff --git a/test/test-utils/helpers/settings-helper.ts b/test/test-utils/helpers/settings-helper.ts index a26aa2de33c..e0cf6ff17bf 100644 --- a/test/test-utils/helpers/settings-helper.ts +++ b/test/test-utils/helpers/settings-helper.ts @@ -1,24 +1,40 @@ import { BattleStyle } from "#enums/battle-style"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; +import { ExpNotification } from "#enums/exp-notification"; import { PlayerGender } from "#enums/player-gender"; +import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** * Helper to handle settings for tests + * @todo Why does this exist */ export class SettingsHelper extends GameManagerHelper { - private _battleStyle: BattleStyle = BattleStyle.SET; + constructor(game: GameManager) { + super(game); - get battleStyle(): BattleStyle { - return this._battleStyle; + this.initDefaultSettings(); + } + + private initDefaultSettings(): void { + this.game.scene.gameSpeed = 5; + this.game.scene.moveAnimations = false; + this.game.scene.showLevelUpStats = false; + this.game.scene.expGainsSpeed = ExpGainsSpeed.SKIP; + this.game.scene.expParty = ExpNotification.SKIP; + this.game.scene.hpBarSpeed = 3; + this.game.scene.enableTutorials = false; + this.game.scene.battleStyle = BattleStyle.SET; + this.game.scene.gameData.gender = PlayerGender.MALE; // set initial player gender; + this.game.scene.fieldVolume = 0; } /** * Change the battle style to Switch or Set mode (tests default to {@linkcode BattleStyle.SET}) - * @param mode {@linkcode BattleStyle.SWITCH} or {@linkcode BattleStyle.SET} + * @param mode - The {@linkcode BattleStyle} to set */ - set battleStyle(mode: BattleStyle.SWITCH | BattleStyle.SET) { - this._battleStyle = mode; + battleStyle(mode: BattleStyle) { + this.game.scene.battleStyle = mode; } /** diff --git a/test/test-utils/tests/helpers/prompt-handler.test.ts b/test/test-utils/tests/helpers/prompt-handler.test.ts index 4169e946dbe..b2bf740359b 100644 --- a/test/test-utils/tests/helpers/prompt-handler.test.ts +++ b/test/test-utils/tests/helpers/prompt-handler.test.ts @@ -44,7 +44,7 @@ describe("Test Utils - PromptHandler", () => { phaseManager: { getCurrentPhase: () => ({ - phaseName: "CommandPhase", + phaseName: "testDialoguePhase", }) as unknown as Phase, }, }, @@ -83,7 +83,7 @@ describe("Test Utils - PromptHandler", () => { }); describe("doPromptCheck", () => { - it("should check and remove the first prompt", async () => { + it("should check and remove the first prompt matching criteria", async () => { onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1()); onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback2()); promptHandler["doPromptCheck"](); From 461afb0fde3a7c1f1d6ff8adaccf63ae257ca9ad Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 24 Aug 2025 21:52:16 -0400 Subject: [PATCH 04/30] Fixed a couple tests Currently chipping away at the backlog of "jank and/or stupid tests" --- test/abilities/ability-timing.test.ts | 3 +- test/abilities/screen-cleaner.test.ts | 72 ++++++------------- test/moves/baton-pass.test.ts | 8 --- .../encounters/lost-at-sea-encounter.test.ts | 5 +- test/test-utils/helpers/settings-helper.ts | 29 +++++--- .../tests/helpers/prompt-handler.test.ts | 2 + 6 files changed, 44 insertions(+), 75 deletions(-) diff --git a/test/abilities/ability-timing.test.ts b/test/abilities/ability-timing.test.ts index f5315d2b80e..c2a29176371 100644 --- a/test/abilities/ability-timing.test.ts +++ b/test/abilities/ability-timing.test.ts @@ -34,8 +34,9 @@ describe("Ability Timing", () => { vi.spyOn(i18next, "t"); }); + // TODO: change this to use the `startBattleWithSwitch` util from hotfix branch it("should trigger after switch check", async () => { - game.settings.battleStyle = BattleStyle.SWITCH; + game.settings.battleStyle(BattleStyle.SWITCH); await game.classicMode.runToSummon([SpeciesId.EEVEE, SpeciesId.FEEBAS]); game.onNextPrompt( diff --git a/test/abilities/screen-cleaner.test.ts b/test/abilities/screen-cleaner.test.ts index 50a854adeb1..8acba6a140c 100644 --- a/test/abilities/screen-cleaner.test.ts +++ b/test/abilities/screen-cleaner.test.ts @@ -1,9 +1,8 @@ import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; -import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { PostSummonPhase } from "#phases/post-summon-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; +import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -24,57 +23,26 @@ describe("Abilities - Screen Cleaner", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single").ability(AbilityId.SCREEN_CLEANER).enemySpecies(SpeciesId.SHUCKLE); + game.override + .battleStyle("single") + .ability(AbilityId.SCREEN_CLEANER) + .enemySpecies(SpeciesId.SHUCKLE) + .weather(WeatherType.HAIL); }); - it("removes Aurora Veil", async () => { - game.override.enemyMoveset(MoveId.AURORA_VEIL); + // TODO: Screen cleaner doesn't remove both sides' tags if both players have them + it.todo.each([ + { name: "Reflect", tagType: ArenaTagType.REFLECT }, + { name: "Light Screen", tagType: ArenaTagType.LIGHT_SCREEN }, + { name: "Aurora Veil", tagType: ArenaTagType.AURORA_VEIL }, + ])("should remove $name from both sides of the field on entrance", async ({ tagType }) => { + game.scene.arena.addTag(tagType, 0, 0, 0, ArenaTagSide.PLAYER); + game.scene.arena.addTag(tagType, 0, 0, 0, ArenaTagSide.ENEMY); + expect(game).toHaveArenaTag(tagType); + await game.classicMode.startBattle([SpeciesId.SLOWKING]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); - - game.move.use(MoveId.HAIL); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.scene.arena.getTag(ArenaTagType.AURORA_VEIL)).toBeDefined(); - - await game.toNextTurn(); - game.doSwitchPokemon(1); - await game.phaseInterceptor.to(PostSummonPhase); - - expect(game.scene.arena.getTag(ArenaTagType.AURORA_VEIL)).toBeUndefined(); - }); - - it("removes Light Screen", async () => { - game.override.enemyMoveset(MoveId.LIGHT_SCREEN); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); - - game.move.use(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.scene.arena.getTag(ArenaTagType.LIGHT_SCREEN)).toBeDefined(); - - await game.toNextTurn(); - game.doSwitchPokemon(1); - await game.phaseInterceptor.to(PostSummonPhase); - - expect(game.scene.arena.getTag(ArenaTagType.LIGHT_SCREEN)).toBeUndefined(); - }); - - it("removes Reflect", async () => { - game.override.enemyMoveset(MoveId.REFLECT); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); - - game.move.use(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.scene.arena.getTag(ArenaTagType.REFLECT)).toBeDefined(); - - await game.toNextTurn(); - game.doSwitchPokemon(1); - await game.phaseInterceptor.to(PostSummonPhase); - - expect(game.scene.arena.getTag(ArenaTagType.REFLECT)).toBeUndefined(); + const slowking = game.field.getPlayerPokemon(); + expect(slowking).toHaveAbilityApplied(AbilityId.SCREEN_CLEANER); + expect(game).not.toHaveArenaTag(tagType); }); }); diff --git a/test/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts index f9bd92a63cd..a155bc11b2a 100644 --- a/test/moves/baton-pass.test.ts +++ b/test/moves/baton-pass.test.ts @@ -74,14 +74,6 @@ describe("Moves - Baton Pass", () => { // check buffs are still there expect(game.field.getEnemyPokemon().getStatStage(Stat.SPATK)).toEqual(2); - // confirm that a switch actually happened. can't use species because I - // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.phaseInterceptor.log.slice(-4)).toEqual([ - "MoveEffectPhase", - "SwitchSummonPhase", - "SummonPhase", - "PostSummonPhase", - ]); }); it("doesn't transfer effects that aren't transferrable", async () => { diff --git a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 73134381553..f31f2a7530a 100644 --- a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -8,7 +8,6 @@ import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils" import { LostAtSeaEncounter } from "#mystery-encounters/lost-at-sea-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { PartyExpPhase } from "#phases/party-exp-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, @@ -119,7 +118,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const expBefore = blastoise!.exp; await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(PartyExpPhase); + await game.phaseInterceptor.to("ShowPartyExpBarPhase"); expect(blastoise?.exp).toBe(expBefore + Math.floor((laprasSpecies.baseExp * defaultWave) / 5 + 1)); }); @@ -184,7 +183,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const expBefore = pidgeot!.exp; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(PartyExpPhase); + await game.phaseInterceptor.to("ShowPartyExpBarPhase"); expect(pidgeot!.exp).toBe(expBefore + Math.floor((laprasBaseExp * defaultWave) / 5 + 1)); }); diff --git a/test/test-utils/helpers/settings-helper.ts b/test/test-utils/helpers/settings-helper.ts index e0cf6ff17bf..0094c4ad8d0 100644 --- a/test/test-utils/helpers/settings-helper.ts +++ b/test/test-utils/helpers/settings-helper.ts @@ -4,6 +4,8 @@ import { ExpNotification } from "#enums/exp-notification"; import { PlayerGender } from "#enums/player-gender"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import chalk from "chalk"; +import { getEnumStr } from "../string-utils"; /** * Helper to handle settings for tests @@ -31,40 +33,45 @@ export class SettingsHelper extends GameManagerHelper { /** * Change the battle style to Switch or Set mode (tests default to {@linkcode BattleStyle.SET}) - * @param mode - The {@linkcode BattleStyle} to set + * @param style - The {@linkcode BattleStyle} to set */ - battleStyle(mode: BattleStyle) { - this.game.scene.battleStyle = mode; + battleStyle(style: BattleStyle): this { + this.game.scene.battleStyle = style; + this.log(`Battle Style set to BattleStyle.${getEnumStr(BattleStyle, style)}!`); + return this; } /** * Disable/Enable type hints settings - * @param enable true to enabled, false to disabled + * @param enable - Whether to enable or disable type hints. + * @returns `this` */ - typeHints(enable: boolean): void { + typeHints(enable: boolean): this { this.game.scene.typeHints = enable; - this.log(`Type Hints ${enable ? "enabled" : "disabled"}`); + this.log(`Type Hints ${enable ? "enabled" : "disabled"}!`); + return this; } /** * Change the player gender - * @param gender the {@linkcode PlayerGender} to set + * @param gender - The {@linkcode PlayerGender} to set */ playerGender(gender: PlayerGender) { this.game.scene.gameData.gender = gender; - this.log(`Gender set to: ${PlayerGender[gender]} (=${gender})`); + this.log(`Gender set to PlayerGender.${getEnumStr(PlayerGender, gender)}!`); } /** * Change the exp gains speed - * @param speed the {@linkcode ExpGainsSpeed} to set + * @param speed - the {@linkcode ExpGainsSpeed} to set */ expGainsSpeed(speed: ExpGainsSpeed) { this.game.scene.expGainsSpeed = speed; - this.log(`Exp Gains Speed set to: ${ExpGainsSpeed[speed]} (=${speed})`); + this.log(`Exp Gains Speed set to ExpGainsSpeed.${getEnumStr(ExpGainsSpeed, speed)}!`); + return this; } private log(...params: any[]) { - console.log("Settings:", ...params); + console.log(chalk.hex("#FFAFFA")(params)); } } diff --git a/test/test-utils/tests/helpers/prompt-handler.test.ts b/test/test-utils/tests/helpers/prompt-handler.test.ts index b2bf740359b..2fd45d599ad 100644 --- a/test/test-utils/tests/helpers/prompt-handler.test.ts +++ b/test/test-utils/tests/helpers/prompt-handler.test.ts @@ -76,6 +76,8 @@ describe("Test Utils - PromptHandler", () => { }); it("should call PhaseInterceptor.checkMode if current phase in `endBySetMode`", async () => { + promptHandler["game"]["scene"]["phaseManager"]["getCurrentPhase"] = () => + ({ phaseName: "CommandPhase" }) as Phase; promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); expect(checkModeCallback).toHaveBeenCalledOnce(); From a41bf4940af36949e0d5254ca5d135eecc4a90f9 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 28 Aug 2025 00:21:41 -0400 Subject: [PATCH 05/30] Nearly fixed the rest of the tests --- src/ui/ui.ts | 1 + test/moves/focus-punch.test.ts | 6 +-- test/test-utils/helpers/prompt-handler.ts | 25 ++++----- test/ui/battle-info.test.ts | 6 +-- test/ui/item-manage-button.test.ts | 65 ++++++++++------------- 5 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/ui/ui.ts b/src/ui/ui.ts index e381d205b78..11f11973c91 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -518,6 +518,7 @@ export class UI extends Phaser.GameObjects.Container { } private setModeInternal( + this: UI, mode: UiMode, clear: boolean, forceTransition: boolean, diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts index 9a76dbec0db..d7b40569aaa 100644 --- a/test/moves/focus-punch.test.ts +++ b/test/moves/focus-punch.test.ts @@ -9,7 +9,7 @@ import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import i18next from "i18next"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Focus Punch", () => { let phaserGame: Phaser.Game; @@ -125,8 +125,8 @@ describe("Moves - Focus Punch", () => { game.move.select(MoveId.FOCUS_PUNCH); await game.phaseInterceptor.to("MoveEndPhase", true); await game.phaseInterceptor.to("MessagePhase", false); - const consoleSpy = vi.spyOn(console, "log"); await game.phaseInterceptor.to("MoveEndPhase", true); - expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" })); + expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" })); + expect(game.textInterceptor.logs).not.toContain(i18next.t("battle:attackFailed")); }); }); diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index b4e55e05d59..e54de9cf767 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -2,9 +2,11 @@ import type { AwaitableUiHandler } from "#app/ui/awaitable-ui-handler"; import { UiMode } from "#enums/ui-mode"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import { getEnumStr } from "#test/test-utils/string-utils"; import type { PhaseString } from "#types/phase-types"; +import type { UI } from "#ui/ui"; import chalk from "chalk"; -import { type MockInstance, vi } from "vitest"; +import { vi } from "vitest"; interface UIPrompt { /** The {@linkcode PhaseString | name} of the Phase during which to execute the callback. */ @@ -56,12 +58,14 @@ export class PromptHandler extends GameManagerHelper { constructor(game: GameManager) { super(game); + this.originalSetModeInternal = this.game.scene.ui["setModeInternal"]; // `any` assertion needed as we are mocking private property - ( - vi.spyOn(this.game.scene.ui as any, "setModeInternal") as MockInstance< - (typeof this.game.scene.ui)["setModeInternal"] - > + vi.spyOn( + this.game.scene.ui as unknown as { + setModeInternal: UI["setModeInternal"]; + }, + "setModeInternal", ).mockImplementation((...args) => this.setMode(args)); // Set an interval to repeatedly check the current prompt. @@ -79,10 +83,8 @@ export class PromptHandler extends GameManagerHelper { private setMode(args: Parameters) { const mode = args[0]; - this.doLog(`UI mode changed to ${UiMode[mode]} (=${mode})!`); - const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args) as ReturnType< - typeof this.originalSetModeInternal - >; + this.doLog(`UI mode changed to ${getEnumStr(UiMode, mode)}!`); + const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args); const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName!; if (endBySetMode.includes(currentPhase)) { @@ -93,7 +95,6 @@ export class PromptHandler extends GameManagerHelper { /** * Method to perform prompt handling every so often. - * @param uiMode - The {@linkcode UiMode} being set */ private doPromptCheck(): void { if (this.prompts.length === 0) { @@ -154,10 +155,10 @@ export class PromptHandler extends GameManagerHelper { } /** - * Wrapper function to add green coloration to phase logs. + * Wrapper function to add coloration to phase logs. * @param args - Arguments to original logging function. */ private doLog(...args: unknown[]): void { - console.log(chalk.hex("#ffa500")(...args)); + console.log(chalk.hex("#008B8B")(...args)); } } diff --git a/test/ui/battle-info.test.ts b/test/ui/battle-info.test.ts index 8bdd61e05b0..6588064d63f 100644 --- a/test/ui/battle-info.test.ts +++ b/test/ui/battle-info.test.ts @@ -2,7 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { ExpPhase } from "#phases/exp-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -14,7 +13,8 @@ vi.mock("../data/exp", ({}) => { }; }); -describe("UI - Battle Info", () => { +// TODO: These are jank and need to be redone +describe.todo("UI - Battle Info", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -48,7 +48,7 @@ describe("UI - Battle Info", () => { game.move.select(MoveId.SPLASH); await game.doKillOpponents(); - await game.phaseInterceptor.to(ExpPhase, true); + await game.phaseInterceptor.to("ExpPhase", true); expect(Math.pow).not.toHaveBeenCalledWith(2, expGainsSpeed); }, diff --git a/test/ui/item-manage-button.test.ts b/test/ui/item-manage-button.test.ts index c28cd9e802e..ba1e576df07 100644 --- a/test/ui/item-manage-button.test.ts +++ b/test/ui/item-manage-button.test.ts @@ -3,14 +3,13 @@ import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import type { Pokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { type PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("UI - Transfer Items", () => { +describe("UI - Item Manage Button", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -40,11 +39,14 @@ describe("UI - Transfer Items", () => { await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA]); game.move.use(MoveId.DRAGON_CLAW); - - await game.phaseInterceptor.to("SelectModifierPhase"); }); + it("foo", () => { + expect(1).toBe(1); + }); it("manage button exists in the proper screen", async () => { + await game.phaseInterceptor.to("SelectModifierPhase"); + let handlerLength: Phaser.GameObjects.GameObject[] | undefined; await new Promise(resolve => { @@ -76,6 +78,7 @@ describe("UI - Transfer Items", () => { }); it("manage button doesn't exist in the other screens", async () => { + await game.phaseInterceptor.to("SelectModifierPhase"); let handlerLength: Phaser.GameObjects.GameObject[] | undefined; await new Promise(resolve => { @@ -108,7 +111,8 @@ describe("UI - Transfer Items", () => { // Test that the manage button actually discards items, needs proofreading it("should discard items when button is selected", async () => { - let pokemon: Pokemon | undefined; + await game.phaseInterceptor.to("SelectModifierPhase"); + const pokemon = game.field.getPlayerPokemon(); await new Promise(resolve => { game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => { @@ -128,17 +132,13 @@ describe("UI - Transfer Items", () => { handler.processInput(Button.ACTION); handler.setCursor(0); handler.processInput(Button.ACTION); - pokemon = game.field.getPlayerPokemon(); resolve(); }); }); - expect(pokemon).toBeDefined(); - if (pokemon) { - expect(pokemon.getHeldItems()).toHaveLength(3); - expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([1, 2, 2]); - } + expect(pokemon.getHeldItems()).toHaveLength(3); + expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([1, 2, 2]); await new Promise(resolve => { game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => { @@ -155,25 +155,18 @@ describe("UI - Transfer Items", () => { const handler = game.scene.ui.getHandler() as PartyUiHandler; handler.processInput(Button.ACTION); - pokemon = game.field.getPlayerPokemon(); - handler.processInput(Button.CANCEL); resolve(); }); }); - expect(pokemon).toBeDefined(); - if (pokemon) { - // Sitrus berry was discarded, leaving 2 stacks of 2 berries behind - expect(pokemon.getHeldItems()).toHaveLength(2); - expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([2, 2]); - } + // Sitrus berry was discarded, leaving 2 stacks of 2 berries behind + expect(pokemon.getHeldItems()).toHaveLength(2); + expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([2, 2]); }); // TODO: This test breaks when running all tests on github. Fix this once hotfix period is over. it.todo("should not allow changing to discard mode when transfering items", async () => { - let handler: PartyUiHandler | undefined; - const { resolve, promise } = Promise.withResolvers(); game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => { @@ -187,7 +180,7 @@ describe("UI - Transfer Items", () => { game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => { await new Promise(r => setTimeout(r, 100)); - handler = game.scene.ui.getHandler() as PartyUiHandler; + const handler = game.scene.ui.getHandler() as PartyUiHandler; handler.setCursor(0); handler.processInput(Button.ACTION); @@ -199,21 +192,21 @@ describe("UI - Transfer Items", () => { }); await promise; - expect(handler).toBeDefined(); - if (handler) { - const partyMode = handler["partyUiMode"]; - expect(partyMode).toBe(PartyUiMode.MODIFIER_TRANSFER); - handler.setCursor(7); - handler.processInput(Button.ACTION); - // Should not change mode to discard - expect(handler["partyUiMode"]).toBe(PartyUiMode.MODIFIER_TRANSFER); + const handler = game.scene.ui.getHandler() as PartyUiHandler; - handler.processInput(Button.CANCEL); - handler.setCursor(7); - handler.processInput(Button.ACTION); - // Should change mode to discard - expect(handler["partyUiMode"]).toBe(PartyUiMode.DISCARD); - } + const partyMode = handler["partyUiMode"]; + expect(partyMode).toBe(PartyUiMode.MODIFIER_TRANSFER); + + handler.setCursor(7); + handler.processInput(Button.ACTION); + // Should not change mode to discard + expect(handler["partyUiMode"]).toBe(PartyUiMode.MODIFIER_TRANSFER); + + handler.processInput(Button.CANCEL); + handler.setCursor(7); + handler.processInput(Button.ACTION); + // Should change mode to discard + expect(handler["partyUiMode"]).toBe(PartyUiMode.DISCARD); }); }); From 61327263429b44a08e1442cbe38ed671f8734e5c Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 3 Sep 2025 20:29:40 -0400 Subject: [PATCH 06/30] un-reverted stuff --- src/battle-scene.ts | 9 +++- test/test-utils/game-manager.ts | 10 +++- test/test-utils/game-wrapper.ts | 17 ++++-- test/test-utils/helpers/prompt-handler.ts | 11 ++-- test/test-utils/mocks/mock-phase.ts | 3 +- test/ui/item-manage-button.test.ts | 66 +++++++++++++---------- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 78ed1150dfc..7c07c9f7e18 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -360,7 +360,11 @@ export class BattleScene extends SceneBase { ); } - async preload() { + /** + * Load game assets necessary for the scene to run. + * Called by Phaser on new game start. + */ + public async preload(): Promise { if (DEBUG_RNG) { const originalRealInRange = Phaser.Math.RND.realInRange; Phaser.Math.RND.realInRange = function (min: number, max: number): number { @@ -395,7 +399,7 @@ export class BattleScene extends SceneBase { * Create game objects with loaded assets. * Called by Phaser on new game start. */ - create(): void { + public create(): void { this.scene.remove(LoadingScene.KEY); initGameSpeed.apply(this); this.inputController = new InputsController(); @@ -1264,6 +1268,7 @@ export class BattleScene extends SceneBase { this.uiContainer.remove(this.ui, true); this.uiContainer.destroy(); this.children.removeAll(true); + // TODO: Do we even need this? this.game.domContainer.innerHTML = ""; // TODO: `launchBattle` calls `reset(false, false, true)` this.launchBattle(); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 6557860b3be..abd790b8e58 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -117,8 +117,14 @@ export class GameManager { global.fetch = vi.fn(MockFetch) as any; } - /** Reset a prior `BattleScene` instance to the proper initial state. */ + /** + * Reset a prior `BattleScene` instance to the proper initial state. + * @todo Review why our UI doesn't reset between runs and why we need to do it manually + */ private resetScene(): void { + // NB: We can't pass `clearScene=true` to `reset` as it will only launch the battle after a fadeout tween + // (along with initializing a bunch of sprites we don't really care about) + this.scene.reset(false, true); (this.scene.ui.handlers[UiMode.STARTER_SELECT] as StarterSelectUiHandler).clearStarterPreferences(); @@ -131,7 +137,7 @@ export class GameManager { /** * Initialize various default overrides for starting tests, typically to alleviate randomness. */ - // TODO: This should not be here + // TODO: Move this to overrides-helper.ts private initDefaultOverrides(): void { // Disables Mystery Encounters on all tests (can be overridden at test level) this.override.mysteryEncounterChance(0); diff --git a/test/test-utils/game-wrapper.ts b/test/test-utils/game-wrapper.ts index 8069da027ef..f0dc1e23d39 100644 --- a/test/test-utils/game-wrapper.ts +++ b/test/test-utils/game-wrapper.ts @@ -77,16 +77,22 @@ export class GameWrapper { * Initialize the given {@linkcode BattleScene} and override various properties to avoid crashes with headless games. * @param scene - The {@linkcode BattleScene} to initialize * @returns A Promise that resolves once the initialization process has completed. + * @todo Is loading files actually necessary for a headless renderer? */ - // TODO: is asset loading & method overriding actually needed for a headless renderer? - async setScene(scene: BattleScene): Promise { + public async setScene(scene: BattleScene): Promise { this.scene = scene; this.injectMandatory(); + this.scene.preload(); this.scene.create(); } - injectMandatory() { + /** + * Override this scene and stub out various properties to avoid crashes with headless games. + * @todo Review what parts of this are actually NEEDED + * @todo Overhaul this to work with a multi-scene project + */ + private injectMandatory(): void { this.game.config = { seed: ["test"], gameVersion: version, @@ -160,9 +166,12 @@ export class GameWrapper { this.scene.scale = this.game.scale; this.scene.textures = this.game.textures; this.scene.events = this.game.events; + // TODO: Why is this needed? The `manager` property isn't used anywhere this.scene.manager = new InputManager(this.game, {}); this.scene.manager.keyboard = new KeyboardManager(this.scene); this.scene.pluginEvents = new EventEmitter(); + this.game.domContainer = {} as HTMLDivElement; + // TODO: scenes don't have dom containers this.scene.domContainer = {} as HTMLDivElement; this.scene.spritePipeline = {}; this.scene.fieldSpritePipeline = {}; @@ -204,7 +213,7 @@ export class GameWrapper { this.scene.sys.updateList = new UpdateList(this.scene); this.scene.systems = this.scene.sys; this.scene.input = this.game.input; - this.scene.scene = this.scene; + this.scene.scene = this.scene; // TODO: This seems wacky this.scene.input.keyboard = new KeyboardPlugin(this.scene); this.scene.input.gamepad = new GamepadPlugin(this.scene); this.scene.cachedFetch = (url, _init) => { diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index e54de9cf767..41255b8360b 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -62,9 +62,7 @@ export class PromptHandler extends GameManagerHelper { this.originalSetModeInternal = this.game.scene.ui["setModeInternal"]; // `any` assertion needed as we are mocking private property vi.spyOn( - this.game.scene.ui as unknown as { - setModeInternal: UI["setModeInternal"]; - }, + this.game.scene.ui as UI & Pick<{ setModeInternal: UI["setModeInternal"] }, "setModeInternal">, "setModeInternal", ).mockImplementation((...args) => this.setMode(args)); @@ -83,7 +81,9 @@ export class PromptHandler extends GameManagerHelper { private setMode(args: Parameters) { const mode = args[0]; - this.doLog(`UI mode changed to ${getEnumStr(UiMode, mode)}!`); + this.doLog( + `UI mode changed from ${getEnumStr(UiMode, this.game.scene.ui.getMode())} to ${getEnumStr(UiMode, mode)}!`, + ); const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args); const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName!; @@ -156,8 +156,9 @@ export class PromptHandler extends GameManagerHelper { /** * Wrapper function to add coloration to phase logs. - * @param args - Arguments to original logging function. + * @param args - Arguments to original logging function */ + // TODO: Move this to colors.ts & change color after mock console PR private doLog(...args: unknown[]): void { console.log(chalk.hex("#008B8B")(...args)); } diff --git a/test/test-utils/mocks/mock-phase.ts b/test/test-utils/mocks/mock-phase.ts index 3d4e4870cd5..6a64c7472e5 100644 --- a/test/test-utils/mocks/mock-phase.ts +++ b/test/test-utils/mocks/mock-phase.ts @@ -1,6 +1,7 @@ import { Phase } from "#app/phase"; + /** - * A rudimentary mock of a phase. + * A rudimentary mock of a phase used for unit tests. * Ends upon starting by default. */ export abstract class mockPhase extends Phase { diff --git a/test/ui/item-manage-button.test.ts b/test/ui/item-manage-button.test.ts index ba1e576df07..c02b718d836 100644 --- a/test/ui/item-manage-button.test.ts +++ b/test/ui/item-manage-button.test.ts @@ -3,13 +3,15 @@ import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; +import type { Pokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { type PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("UI - Item Manage Button", () => { +// TODO: Resolve issues with UI test state corruption +describe.todo("UI - Transfer Items", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -39,14 +41,11 @@ describe("UI - Item Manage Button", () => { await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA]); game.move.use(MoveId.DRAGON_CLAW); - }); - it("foo", () => { - expect(1).toBe(1); - }); - it("manage button exists in the proper screen", async () => { await game.phaseInterceptor.to("SelectModifierPhase"); + }); + it("manage button exists in the proper screen", async () => { let handlerLength: Phaser.GameObjects.GameObject[] | undefined; await new Promise(resolve => { @@ -78,7 +77,6 @@ describe("UI - Item Manage Button", () => { }); it("manage button doesn't exist in the other screens", async () => { - await game.phaseInterceptor.to("SelectModifierPhase"); let handlerLength: Phaser.GameObjects.GameObject[] | undefined; await new Promise(resolve => { @@ -111,8 +109,7 @@ describe("UI - Item Manage Button", () => { // Test that the manage button actually discards items, needs proofreading it("should discard items when button is selected", async () => { - await game.phaseInterceptor.to("SelectModifierPhase"); - const pokemon = game.field.getPlayerPokemon(); + let pokemon: Pokemon | undefined; await new Promise(resolve => { game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => { @@ -132,13 +129,17 @@ describe("UI - Item Manage Button", () => { handler.processInput(Button.ACTION); handler.setCursor(0); handler.processInput(Button.ACTION); + pokemon = game.field.getPlayerPokemon(); resolve(); }); }); - expect(pokemon.getHeldItems()).toHaveLength(3); - expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([1, 2, 2]); + expect(pokemon).toBeDefined(); + if (pokemon) { + expect(pokemon.getHeldItems()).toHaveLength(3); + expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([1, 2, 2]); + } await new Promise(resolve => { game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => { @@ -155,18 +156,25 @@ describe("UI - Item Manage Button", () => { const handler = game.scene.ui.getHandler() as PartyUiHandler; handler.processInput(Button.ACTION); + pokemon = game.field.getPlayerPokemon(); + handler.processInput(Button.CANCEL); resolve(); }); }); - // Sitrus berry was discarded, leaving 2 stacks of 2 berries behind - expect(pokemon.getHeldItems()).toHaveLength(2); - expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([2, 2]); + expect(pokemon).toBeDefined(); + if (pokemon) { + // Sitrus berry was discarded, leaving 2 stacks of 2 berries behind + expect(pokemon.getHeldItems()).toHaveLength(2); + expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([2, 2]); + } }); // TODO: This test breaks when running all tests on github. Fix this once hotfix period is over. it.todo("should not allow changing to discard mode when transfering items", async () => { + let handler: PartyUiHandler | undefined; + const { resolve, promise } = Promise.withResolvers(); game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => { @@ -180,7 +188,7 @@ describe("UI - Item Manage Button", () => { game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => { await new Promise(r => setTimeout(r, 100)); - const handler = game.scene.ui.getHandler() as PartyUiHandler; + handler = game.scene.ui.getHandler() as PartyUiHandler; handler.setCursor(0); handler.processInput(Button.ACTION); @@ -192,21 +200,21 @@ describe("UI - Item Manage Button", () => { }); await promise; + expect(handler).toBeDefined(); + if (handler) { + const partyMode = handler["partyUiMode"]; + expect(partyMode).toBe(PartyUiMode.MODIFIER_TRANSFER); - const handler = game.scene.ui.getHandler() as PartyUiHandler; + handler.setCursor(7); + handler.processInput(Button.ACTION); + // Should not change mode to discard + expect(handler["partyUiMode"]).toBe(PartyUiMode.MODIFIER_TRANSFER); - const partyMode = handler["partyUiMode"]; - expect(partyMode).toBe(PartyUiMode.MODIFIER_TRANSFER); - - handler.setCursor(7); - handler.processInput(Button.ACTION); - // Should not change mode to discard - expect(handler["partyUiMode"]).toBe(PartyUiMode.MODIFIER_TRANSFER); - - handler.processInput(Button.CANCEL); - handler.setCursor(7); - handler.processInput(Button.ACTION); - // Should change mode to discard - expect(handler["partyUiMode"]).toBe(PartyUiMode.DISCARD); + handler.processInput(Button.CANCEL); + handler.setCursor(7); + handler.processInput(Button.ACTION); + // Should change mode to discard + expect(handler["partyUiMode"]).toBe(PartyUiMode.DISCARD); + } }); }); From 72e3f0670919e24a4baa5c63bd345b796af80054 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 3 Sep 2025 20:42:28 -0400 Subject: [PATCH 07/30] un reverted more --- src/phases/title-phase.ts | 28 ++++++++++++++------------- test/abilities/ability-timing.test.ts | 18 +---------------- test/vitest.setup.ts | 12 ------------ 3 files changed, 16 insertions(+), 42 deletions(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 11a2ccc482c..15d92ba2812 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -168,22 +168,24 @@ export class TitlePhase extends Phase { globalScene.ui.setMode(UiMode.TITLE, config); } - async loadSaveSlot(slotId: number): Promise { + loadSaveSlot(slotId: number): void { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - try { - const success = await globalScene.gameData.loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined); - if (success) { - this.loaded = true; - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - } catch (err) { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - } + globalScene.gameData + .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) + .then((success: boolean) => { + if (success) { + this.loaded = true; + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + }) + .catch(err => { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + }); } initDailyRun(): void { diff --git a/test/abilities/ability-timing.test.ts b/test/abilities/ability-timing.test.ts index c2a29176371..11048bca573 100644 --- a/test/abilities/ability-timing.test.ts +++ b/test/abilities/ability-timing.test.ts @@ -1,9 +1,5 @@ import { AbilityId } from "#enums/ability-id"; -import { BattleStyle } from "#enums/battle-style"; import { SpeciesId } from "#enums/species-id"; -import { UiMode } from "#enums/ui-mode"; -import { CommandPhase } from "#phases/command-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import i18next from "#plugins/i18n"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; @@ -34,22 +30,10 @@ describe("Ability Timing", () => { vi.spyOn(i18next, "t"); }); - // TODO: change this to use the `startBattleWithSwitch` util from hotfix branch it("should trigger after switch check", async () => { - game.settings.battleStyle(BattleStyle.SWITCH); await game.classicMode.runToSummon([SpeciesId.EEVEE, SpeciesId.FEEBAS]); + await game.classicMode.startBattleWithSwitch(1); - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase), - ); - - await game.phaseInterceptor.to("MessagePhase"); expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 })); }); }); diff --git a/test/vitest.setup.ts b/test/vitest.setup.ts index 2fdea7ea661..855068b83f9 100644 --- a/test/vitest.setup.ts +++ b/test/vitest.setup.ts @@ -51,18 +51,6 @@ vi.mock("i18next", async importOriginal => { beforeAll(() => { initTests(); - - process - .on("uncaughtException", err => { - clearInterval(PromptHandler.runInterval); - PromptHandler.runInterval = undefined; - throw err; - }) - .on("unhandledRejection", err => { - clearInterval(PromptHandler.runInterval); - PromptHandler.runInterval = undefined; - throw err; - }); }); afterAll(() => { From 272760dcd99a254eabd4ab72eecb2e3095c0e65f Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 3 Sep 2025 20:49:00 -0400 Subject: [PATCH 08/30] Tried reverting my revert --- src/phases/title-phase.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 15d92ba2812..11a2ccc482c 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -168,24 +168,22 @@ export class TitlePhase extends Phase { globalScene.ui.setMode(UiMode.TITLE, config); } - loadSaveSlot(slotId: number): void { + async loadSaveSlot(slotId: number): Promise { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - globalScene.gameData - .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) - .then((success: boolean) => { - if (success) { - this.loaded = true; - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - }) - .catch(err => { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); + try { + const success = await globalScene.gameData.loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined); + if (success) { + this.loaded = true; + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + } catch (err) { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + } } initDailyRun(): void { From 54836ccd4a077098059c0306260980cff7f4cc83 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 3 Sep 2025 20:54:45 -0400 Subject: [PATCH 09/30] Marked starter select test as TODO to avoid corruption --- src/phases/title-phase.ts | 1 + test/test-utils/helpers/prompt-handler.ts | 4 ++-- test/ui/starter-select.test.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 11a2ccc482c..dcc8557a65f 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -168,6 +168,7 @@ export class TitlePhase extends Phase { globalScene.ui.setMode(UiMode.TITLE, config); } + // TODO: Make callers actually wait for the damn save slot to load async loadSaveSlot(slotId: number): Promise { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index 41255b8360b..4b16636f4db 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -78,13 +78,13 @@ export class PromptHandler extends GameManagerHelper { * @param args - Arguments being passed to the original method * @returns The original return value. */ - private setMode(args: Parameters) { + private async setMode(args: Parameters) { const mode = args[0]; this.doLog( `UI mode changed from ${getEnumStr(UiMode, this.game.scene.ui.getMode())} to ${getEnumStr(UiMode, mode)}!`, ); - const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args); + const ret = await this.originalSetModeInternal.apply(this.game.scene.ui, args); const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName!; if (endBySetMode.includes(currentPhase)) { diff --git a/test/ui/starter-select.test.ts b/test/ui/starter-select.test.ts index 397f3d6086f..73c49037750 100644 --- a/test/ui/starter-select.test.ts +++ b/test/ui/starter-select.test.ts @@ -16,7 +16,8 @@ import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("UI - Starter select", () => { +// TODO: Resolve issues with UI test state corruption +describe.todo("UI - Starter select", () => { let phaserGame: Phaser.Game; let game: GameManager; From 9f185cd8a8def1a838ed6c3842e3f210675bf206 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 3 Sep 2025 21:03:11 -0400 Subject: [PATCH 10/30] fixed the bug, ruined my sanity --- test/test-utils/helpers/prompt-handler.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index 4b16636f4db..c727fa87616 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -77,14 +77,18 @@ export class PromptHandler extends GameManagerHelper { * Helper method to wrap UI mode changing. * @param args - Arguments being passed to the original method * @returns The original return value. + * @todo Make this wait for the actual UI mode setting */ - private async setMode(args: Parameters) { + private setMode( + args: Parameters, + ): ReturnType { const mode = args[0]; this.doLog( `UI mode changed from ${getEnumStr(UiMode, this.game.scene.ui.getMode())} to ${getEnumStr(UiMode, mode)}!`, ); - const ret = await this.originalSetModeInternal.apply(this.game.scene.ui, args); + // TODO: Add `await` to this + const ret = this.originalSetModeInternal.apply(this.game.scene.ui, args); const currentPhase = this.game.scene.phaseManager.getCurrentPhase()?.phaseName!; if (endBySetMode.includes(currentPhase)) { From 8bbe706f3f04ca4cb58ff79b4f0711077256ef25 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 7 Sep 2025 13:46:35 -0400 Subject: [PATCH 11/30] Added toggle to optionally ignore phase blacklist --- src/phase-manager.ts | 1 + test/test-utils/phase-interceptor.ts | 52 ++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 2308698efbc..fb30d078267 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -1,3 +1,4 @@ +import { PHASE_START_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue"; diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index d10a633d024..ac3061da774 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,5 +1,6 @@ import type { PhaseManager, PhaseString } from "#app/@types/phase-types"; import type { BattleScene } from "#app/battle-scene"; +import { PHASE_INTERCEPTOR_COLOR, PHASE_START_COLOR } from "#app/constants/colors"; import type { Phase } from "#app/phase"; import type { Constructor } from "#app/utils/common"; import { UiMode } from "#enums/ui-mode"; @@ -11,7 +12,6 @@ import { inspect } from "util"; import chalk from "chalk"; import { vi } from "vitest"; import { getEnumStr } from "./string-utils"; -import { PHASE_INTERCEPTOR_COLOR, PHASE_START_COLOR } from "#app/constants/colors"; /** * A {@linkcode ReadonlySet} containing phase names that will not be shown in the console when started. @@ -47,22 +47,35 @@ export class PhaseInterceptor { * - `running`: The interceptor is currently running a phase. * - `interrupted`: The interceptor has been interrupted by a UI prompt and is waiting for the caller to end it. * - `idling`: The interceptor is not currently running a phase and is ready to start a new one. + * @defaultValue `idling` */ private state: StateType = "idling"; - + /** The current target that is being ran to. */ private target: PhaseString; + /** + * Whether to respect {@linkcode blacklistedPhaseNames} during logging. + * @defaultValue `true` + */ + private respectBlacklist = true; /** - * Constructor to initialize the scene and properties, and to start the phase handling. + * Initialize a new PhaseInterceptor. * @param scene - The scene to be managed + * @todo This should take a GameManager instance once multi scene stuff becomes a reality + * @remarks + * This overrides {@linkcode PhaseManager.startCurrentPhase} to toggle the interceptor's state + * instead of immediately starting the next phase. */ constructor(scene: BattleScene) { this.scene = scene; - // Mock the private startCurrentPhase method to toggle `isRunning` rather than actually starting anything vi.spyOn( - this.scene.phaseManager as unknown as typeof PhaseManager & { - startCurrentPhase: PhaseManager["startCurrentPhase"]; - }, + this.scene.phaseManager as PhaseManager & + Pick< + { + startCurrentPhase: PhaseManager["startCurrentPhase"]; + }, + "startCurrentPhase" + >, "startCurrentPhase", ).mockImplementation(() => { this.state = "idling"; @@ -88,7 +101,6 @@ export class PhaseInterceptor { const pm = this.scene.phaseManager; let currentPhase = pm.getCurrentPhase(); - let didLog = false; // NB: This has to use an interval to wait for UI prompts to activate @@ -176,14 +188,14 @@ export class PhaseInterceptor { * To end ones already in the process of running, use {@linkcode GameManager.endPhase}. * @example * await game.phaseInterceptor.to("LoginPhase", false); - * game.phaseInterceptor.shiftPhase(); + * game.phaseInterceptor.shiftPhase(); // skips LoginPhase without starting it */ public shiftPhase(): void { - const phaseName = this.scene.phaseManager.getCurrentPhase()!.phaseName; + const phaseName = this.scene.phaseManager.getCurrentPhase().phaseName; if (this.state !== "idling") { - throw new Error(`shiftPhase attempted to skip phase ${phaseName} mid-execution!`); + throw new Error(`PhaseInterceptor.shiftPhase attempted to skip phase ${phaseName} mid-execution!`); } - this.doLog(`Skipping current phase ${phaseName}`); + this.doLog(`Skipping current phase: ${phaseName}`); this.scene.phaseManager.shiftPhase(); } @@ -201,10 +213,10 @@ export class PhaseInterceptor { /** * Method to log the start of a phase. * Called in place of {@linkcode PhaseManager.startCurrentPhase} to allow for manual intervention. - * @param phaseName - The name of the phase to log. + * @param phaseName - The name of the phase to log */ - private logPhase(phaseName: PhaseString) { - if (!blacklistedPhaseNames.has(phaseName)) { + private logPhase(phaseName: PhaseString): void { + if (this.respectBlacklist && !blacklistedPhaseNames.has(phaseName)) { console.log(`%cStart Phase: ${phaseName}`, `color:${PHASE_START_COLOR}`); } this.log.push(phaseName); @@ -217,6 +229,16 @@ export class PhaseInterceptor { this.log = []; } + /** + * Toggle the Interceptor's logging blacklist, enabling or disabling logging all phases in + * {@linkcode blacklistedPhaseNames}. + * @param state - Whether the blacklist should work; default `false` (disable) + */ + public toggleBlacklist(state = false): void { + this.respectBlacklist = state; + this.doLog(`Phase blacklist logging ${state ? "disabled" : "enabled"}!`); + } + /** * Wrapper function to add coral coloration to phase logs. * @param args - Arguments to original logging function From 72e9c153d0c9161d47ae101f49e601624d222f75 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 7 Sep 2025 13:48:41 -0400 Subject: [PATCH 12/30] fixed doc --- test/test-utils/phase-interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index ac3061da774..2107eb65108 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -232,7 +232,7 @@ export class PhaseInterceptor { /** * Toggle the Interceptor's logging blacklist, enabling or disabling logging all phases in * {@linkcode blacklistedPhaseNames}. - * @param state - Whether the blacklist should work; default `false` (disable) + * @param state - Whether the blacklist should be enabled; default `false` (disable) */ public toggleBlacklist(state = false): void { this.respectBlacklist = state; From ba489231f92b97f3a2145e24de976ccc29769c89 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 10 Sep 2025 10:35:49 -0400 Subject: [PATCH 13/30] Reverted title phase async change --- src/phases/title-phase.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 54f0093b959..bb1cf6a9619 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -173,23 +173,21 @@ export class TitlePhase extends Phase { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - globalScene.gameData - .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) - .then((success: boolean) => { - if (success) { - this.loaded = true; - if (loggedInUser) { - loggedInUser.lastSessionSlot = slotId; - } - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); + try { + const success = await globalScene.gameData.loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined); + if (success) { + this.loaded = true; + if (loggedInUser) { + loggedInUser.lastSessionSlot = slotId; } - }) - .catch(err => { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + } catch (err) { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + } } initDailyRun(): void { From 64d1ff7946163a5c153b6a9a7176210063a103c3 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 10 Sep 2025 10:45:44 -0400 Subject: [PATCH 14/30] Made `TitlePhase.loadSession` private --- src/phases/title-phase.ts | 2 +- test/test-utils/game-manager.ts | 12 ++++++------ test/test-utils/helpers/reload-helper.ts | 10 ++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index bb1cf6a9619..986c8bdb1f6 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -169,7 +169,7 @@ export class TitlePhase extends Phase { } // TODO: Make callers actually wait for the damn save slot to load - async loadSaveSlot(slotId: number): Promise { + private async loadSaveSlot(slotId: number): Promise { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f3745a2199f..2823e033623 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -416,7 +416,7 @@ export class GameManager { * Checks if the current phase matches the target phase. * @param phaseTarget - The target phase. * @returns Whether the current phase matches the target phase - * @deprecated - Use phaseString + * @deprecated - Use `PhaseString` instead */ isCurrentPhase(phaseTarget: PhaseClass): boolean; isCurrentPhase(phaseTarget: PhaseString | PhaseClass): boolean { @@ -425,12 +425,12 @@ export class GameManager { } /** - * Checks if the current mode matches the target mode. + * Check if the current `UiMode` matches the target mode. * @param mode - The target {@linkcode UiMode} to check. * @returns Whether the current mode matches the target mode. */ - isCurrentMode(mode: UiMode) { - return this.scene.ui?.getMode() === mode; + isCurrentMode(mode: UiMode): boolean { + return this.scene.ui.getMode() === mode; } /** @@ -448,10 +448,10 @@ export class GameManager { /** * Imports game data from a file. - * @param path - The path to the data file. + * @param path - The path to the data file * @returns A promise that resolves with a tuple containing a boolean indicating success and an integer status code. */ - async importData(path): Promise<[boolean, number]> { + async importData(path: string): Promise<[boolean, number]> { const saveKey = "x0i2O7WRiANTqPmZ"; const dataRaw = fs.readFileSync(path, { encoding: "utf8", flag: "r" }); let dataStr = AES.decrypt(dataRaw, saveKey).toString(enc.Utf8); diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index c92af218b50..835e8d5f58e 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -18,11 +18,9 @@ export class ReloadHelper extends GameManagerHelper { super(game); // Whenever the game saves the session, save it to the reloadHelper instead - vi.spyOn(game.scene.gameData, "saveAll").mockImplementation(() => { - return new Promise((resolve, _reject) => { - this.sessionData = game.scene.gameData.getSessionSaveData(); - resolve(true); - }); + vi.spyOn(game.scene.gameData, "saveAll").mockImplementation(async () => { + this.sessionData = game.scene.gameData.getSessionSaveData(); + return true; }); } @@ -52,7 +50,7 @@ export class ReloadHelper extends GameManagerHelper { ); this.game.scene.modifiers = []; } - await titlePhase.loadSaveSlot(-1); // Load the desired session data + await titlePhase["loadSaveSlot"](-1); // Load the desired session data // Run through prompts for switching Pokemon, copied from classicModeHelper.ts if (this.game.scene.battleStyle === BattleStyle.SWITCH) { From 7d8beea5170cf44a808dc9c8cc82600b5648a318 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 10 Sep 2025 10:51:11 -0400 Subject: [PATCH 15/30] Ran biome --- test/phases/mystery-encounter-phase.test.ts | 7 +------ test/test-utils/helpers/prompt-handler.ts | 8 ++++---- test/test-utils/helpers/settings-helper.ts | 2 +- .../tests/helpers/prompt-handler.test.ts | 19 ++++++++++--------- .../tests/phase-interceptor/unit.test.ts | 5 ++--- test/vitest.setup.ts | 6 +++--- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/test/phases/mystery-encounter-phase.test.ts b/test/phases/mystery-encounter-phase.test.ts index e073cfca586..28fcf7c0b98 100644 --- a/test/phases/mystery-encounter-phase.test.ts +++ b/test/phases/mystery-encounter-phase.test.ts @@ -3,7 +3,6 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { MysteryEncounterOptionSelectedPhase } from "#phases/mystery-encounter-phases"; import { GameManager } from "#test/test-utils/game-manager"; import type { MessageUiHandler } from "#ui/handlers/message-ui-handler"; import type { MysteryEncounterUiHandler } from "#ui/handlers/mystery-encounter-ui-handler"; @@ -83,11 +82,7 @@ describe("Mystery Encounter Phases", () => { handler.processInput(Button.ACTION); // Waitfor required so that option select messages and preOptionPhase logic are handled - await vi.waitFor(() => - expect(game).toBeAtPhase( - "MysteryEncounterOptionSelectedPhase", - ), - ); + await vi.waitFor(() => expect(game).toBeAtPhase("MysteryEncounterOptionSelectedPhase")); expect(ui.getMode()).toBe(UiMode.MESSAGE); expect(ui.showDialogue).toHaveBeenCalledTimes(1); expect(ui.showText).toHaveBeenCalledTimes(2); diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index c727fa87616..fac7067341c 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -120,10 +120,10 @@ export class PromptHandler extends GameManagerHelper { // If the current mode, phase, and handler match the expected values, execute the callback and continue. // If not, leave it there. if ( - mode === prompt.mode && - currentPhase === prompt.phaseTarget && - currentHandler.active && - !(prompt.awaitingActionInput && !(currentHandler as AwaitableUiHandler)["awaitingActionInput"]) + mode === prompt.mode + && currentPhase === prompt.phaseTarget + && currentHandler.active + && !(prompt.awaitingActionInput && !(currentHandler as AwaitableUiHandler)["awaitingActionInput"]) ) { prompt.callback(); this.prompts.shift(); diff --git a/test/test-utils/helpers/settings-helper.ts b/test/test-utils/helpers/settings-helper.ts index ffacd408492..d948a9d5d98 100644 --- a/test/test-utils/helpers/settings-helper.ts +++ b/test/test-utils/helpers/settings-helper.ts @@ -5,8 +5,8 @@ import { ExpNotification } from "#enums/exp-notification"; import { PlayerGender } from "#enums/player-gender"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; -import chalk from "chalk"; import { getEnumStr } from "#test/test-utils/string-utils"; +import chalk from "chalk"; /** * Helper to handle settings for tests diff --git a/test/test-utils/tests/helpers/prompt-handler.test.ts b/test/test-utils/tests/helpers/prompt-handler.test.ts index 2fd45d599ad..965be24fbe4 100644 --- a/test/test-utils/tests/helpers/prompt-handler.test.ts +++ b/test/test-utils/tests/helpers/prompt-handler.test.ts @@ -1,10 +1,10 @@ -import type { PhaseString } from "#app/@types/phase-types"; import type { Phase } from "#app/phase"; -import type { AwaitableUiHandler } from "#app/ui/awaitable-ui-handler"; import { UiMode } from "#enums/ui-mode"; import type { GameManager } from "#test/test-utils/game-manager"; import { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; import type { PhaseInterceptor } from "#test/test-utils/phase-interceptor"; +import type { PhaseString } from "#types/phase-types"; +import type { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; import type { UI } from "#ui/ui"; import { beforeAll, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; @@ -35,12 +35,12 @@ describe("Test Utils - PromptHandler", () => { scene: { ui: { getHandler: () => handler, - setModeInternal: () => { + setModeInternal: (): Promise => { setModeCallback(); return Promise.resolve(); }, getMode: () => UiMode.TEST_DIALOGUE, - } as unknown as UI, + } as UI, phaseManager: { getCurrentPhase: () => ({ @@ -56,6 +56,7 @@ describe("Test Utils - PromptHandler", () => { } as GameManager); }); + // Wrapper func to ignore incorrect typing on `PhaseString` function onNextPrompt( target: string, mode: UiMode, @@ -69,7 +70,7 @@ describe("Test Utils - PromptHandler", () => { describe("setMode", () => { it("should wrap and pass along original function arguments from setModeInternal", async () => { const setModeSpy = vi.spyOn(promptHandler as any, "setMode"); - promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); + await promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); expect(setModeSpy).toHaveBeenCalledExactlyOnceWith([UiMode.PARTY, false, false, false, []]); expect(setModeCallback).toHaveBeenCalledAfter(setModeSpy); @@ -78,14 +79,14 @@ describe("Test Utils - PromptHandler", () => { it("should call PhaseInterceptor.checkMode if current phase in `endBySetMode`", async () => { promptHandler["game"]["scene"]["phaseManager"]["getCurrentPhase"] = () => ({ phaseName: "CommandPhase" }) as Phase; - promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); + await promptHandler["game"].scene.ui["setModeInternal"](UiMode.PARTY, false, false, false, []); expect(checkModeCallback).toHaveBeenCalledOnce(); }); }); describe("doPromptCheck", () => { - it("should check and remove the first prompt matching criteria", async () => { + it("should check and remove the first prompt matching criteria", () => { onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1()); onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback2()); promptHandler["doPromptCheck"](); @@ -118,7 +119,7 @@ describe("Test Utils - PromptHandler", () => { onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback1(), undefined, true); }, }, - ])("should skip callback and keep in queue if $reason", async ({ callback }) => { + ])("should skip callback and keep in queue if $reason", ({ callback }) => { callback(); onNextPrompt("testDialoguePhase", UiMode.TEST_DIALOGUE, () => callback2); promptHandler["doPromptCheck"](); @@ -128,7 +129,7 @@ describe("Test Utils - PromptHandler", () => { expect(promptHandler["prompts"]).toHaveLength(2); }); - it("should remove expired prompts without blocking", async () => { + it("should remove expired prompts without blocking", () => { onNextPrompt( "testDialoguePhase", UiMode.TEST_DIALOGUE, diff --git a/test/test-utils/tests/phase-interceptor/unit.test.ts b/test/test-utils/tests/phase-interceptor/unit.test.ts index 5813d9f8f15..de4283c3af4 100644 --- a/test/test-utils/tests/phase-interceptor/unit.test.ts +++ b/test/test-utils/tests/phase-interceptor/unit.test.ts @@ -1,9 +1,9 @@ -import type { PhaseString } from "#app/@types/phase-types"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; import type { Constructor } from "#app/utils/common"; import { GameManager } from "#test/test-utils/game-manager"; import { mockPhase } from "#test/test-utils/mocks/mock-phase"; +import type { PhaseString } from "#types/phase-types"; import Phaser from "phaser"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -73,7 +73,7 @@ describe("Utils - Phase Interceptor - Unit", () => { } function expectAtPhase(phaseName: string) { - expect(game).toBeAtPhase(phaseName as PhaseString) + expect(game).toBeAtPhase(phaseName as PhaseString); } /** Wrapper function to make TS not complain about incompatible argument typing on `PhaseString`. */ @@ -146,6 +146,5 @@ describe("Utils - Phase Interceptor - Unit", () => { expect(startSpy).not.toHaveBeenCalled(); expect(game.phaseInterceptor.log).toEqual([]); }); - }); }); diff --git a/test/vitest.setup.ts b/test/vitest.setup.ts index 575678d6980..84d3ccb7e5f 100644 --- a/test/vitest.setup.ts +++ b/test/vitest.setup.ts @@ -1,10 +1,10 @@ import "vitest-canvas-mock"; import { PromptHandler } from "#test/test-utils/helpers/prompt-handler"; -import { initTests } from "#test/test-utils/test-file-initialization"; -import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest"; import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console"; -import { logTestStart, logTestEnd } from "#test/test-utils/setup/test-end-log"; +import { logTestEnd, logTestStart } from "#test/test-utils/setup/test-end-log"; +import { initTests } from "#test/test-utils/test-file-initialization"; import chalk from "chalk"; +import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest"; //#region Mocking From c082b0da6d7e9fcaee19657b9990cfc49fc93f26 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 10 Sep 2025 13:56:23 -0400 Subject: [PATCH 16/30] Fixed typecheck errors --- test/test-utils/tests/helpers/prompt-handler.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-utils/tests/helpers/prompt-handler.test.ts b/test/test-utils/tests/helpers/prompt-handler.test.ts index 965be24fbe4..b6dd46ff3af 100644 --- a/test/test-utils/tests/helpers/prompt-handler.test.ts +++ b/test/test-utils/tests/helpers/prompt-handler.test.ts @@ -35,12 +35,12 @@ describe("Test Utils - PromptHandler", () => { scene: { ui: { getHandler: () => handler, - setModeInternal: (): Promise => { + setModeInternal: () => { setModeCallback(); return Promise.resolve(); }, getMode: () => UiMode.TEST_DIALOGUE, - } as UI, + } as unknown as UI, phaseManager: { getCurrentPhase: () => ({ From aa5c55bda6408ecc89d678accb1d739ad6d64334 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 10 Sep 2025 13:58:44 -0400 Subject: [PATCH 17/30] Fixed import --- test/test-utils/helpers/prompt-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index fac7067341c..dadc9314c1b 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -1,9 +1,9 @@ -import type { AwaitableUiHandler } from "#app/ui/awaitable-ui-handler"; import { UiMode } from "#enums/ui-mode"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; import { getEnumStr } from "#test/test-utils/string-utils"; import type { PhaseString } from "#types/phase-types"; +import type { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; import type { UI } from "#ui/ui"; import chalk from "chalk"; import { vi } from "vitest"; From 76ae331a641cf14fceca7ff686935ab306281efa Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:40:01 -0400 Subject: [PATCH 18/30] Update title-phase.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/phases/title-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 6a3a331048f..fece05aa03c 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -168,7 +168,7 @@ export class TitlePhase extends Phase { globalScene.ui.setMode(UiMode.TITLE, config); } - // TODO: Make callers actually wait for the damn save slot to load + // TODO: Make callers actually wait for the save slot to load private async loadSaveSlot(slotId: number): Promise { globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; globalScene.ui.setMode(UiMode.MESSAGE); From 6d2bef5a7181382bf726ec098dd99dbbab72028c Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:41:17 -0400 Subject: [PATCH 19/30] Made comment use `privateRemarks` --- src/phase-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 5e542cc499d..337750d322d 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -395,7 +395,7 @@ export class PhaseManager { /** * Helper method to start and log the current phase. * - * @remarks + * @privateRemarkd * This is disabled during tests by `phase-interceptor.ts` to allow for pausing execution at specific phases. * As such, **do not remove or split this method** as it will break integration tests. */ From 9b4fcc04b79705e78d5b4b190461cf6efcfc3089 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:55:54 -0400 Subject: [PATCH 20/30] Fixed comment typo `@privateRemarkd` lmao --- src/phase-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 337750d322d..1ca0838376c 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -395,7 +395,7 @@ export class PhaseManager { /** * Helper method to start and log the current phase. * - * @privateRemarkd + * @privateRemarks * This is disabled during tests by `phase-interceptor.ts` to allow for pausing execution at specific phases. * As such, **do not remove or split this method** as it will break integration tests. */ From 756bf2504f627b954f0b24c06b344d50ca0aee97 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:14:19 -0400 Subject: [PATCH 21/30] Update game-manager.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/game-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index e4603e82c63..6cf7f440027 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -416,7 +416,7 @@ export class GameManager { * Checks if the current phase matches the target phase. * @param phaseTarget - The target phase. * @returns Whether the current phase matches the target phase - * @deprecated - Use `PhaseString` instead + * @deprecated Use `PhaseString` instead */ isCurrentPhase(phaseTarget: PhaseClass): boolean; isCurrentPhase(phaseTarget: PhaseString | PhaseClass): boolean { From 3aaf2184c3ea393b524f03678b74faddb4558fb0 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:14:24 -0400 Subject: [PATCH 22/30] Update game-manager.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/game-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 6cf7f440027..8959e0d30ad 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -178,7 +178,7 @@ export class GameManager { * @param expireFn - Optional function to determine if the prompt has expired. * @param awaitingActionInput - If true, will prevent the prompt from activating until the current {@linkcode AwaitableUiHandler} * is awaiting input; default `false` - * @todo Remove in favor of {@linkcode promptHandler.addToNextPrompt} + * @deprecated Remove in favor of {@linkcode promptHandler.addToNextPrompt} */ onNextPrompt( phaseTarget: PhaseString, From ff2c706dd4591a478640583ad6a0eee528fba534 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 12 Sep 2025 23:59:46 -0400 Subject: [PATCH 23/30] Applied various review comments 0.5 --- src/battle-scene.ts | 1 + src/enums/exp-gains-speed.ts | 2 +- test/abilities/mimicry.test.ts | 4 +-- test/abilities/screen-cleaner.test.ts | 25 ++++++++++++-- test/test-utils/helpers/settings-helper.ts | 38 +++++++++++++--------- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7f050f4aad6..ed8acdb6b5d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -173,6 +173,7 @@ export class BattleScene extends SceneBase { public sessionPlayTime: number | null = null; public lastSavePlayTime: number | null = null; + // TODO: move these settings into a settings helper object public masterVolume = 0.5; public bgmVolume = 1; public fieldVolume = 1; diff --git a/src/enums/exp-gains-speed.ts b/src/enums/exp-gains-speed.ts index b98345552ae..55ea82e6766 100644 --- a/src/enums/exp-gains-speed.ts +++ b/src/enums/exp-gains-speed.ts @@ -1,4 +1,4 @@ -/** Defines the speed of gaining experience. */ +/** Enum regulating the speed of EXP bar animations. */ export enum ExpGainsSpeed { /** The normal speed. */ DEFAULT, diff --git a/test/abilities/mimicry.test.ts b/test/abilities/mimicry.test.ts index 44416387f6e..dec570f19a8 100644 --- a/test/abilities/mimicry.test.ts +++ b/test/abilities/mimicry.test.ts @@ -58,9 +58,7 @@ describe("Abilities - Mimicry", () => { expect(playerPokemon.getTypes()).toEqual([PokemonType.PSYCHIC]); - if (game.scene.arena.terrain) { - game.scene.arena.terrain.turnsLeft = 1; - } + game.scene.arena.terrain!.turnsLeft = 1; game.move.use(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH); diff --git a/test/abilities/screen-cleaner.test.ts b/test/abilities/screen-cleaner.test.ts index 8acba6a140c..52501d371bb 100644 --- a/test/abilities/screen-cleaner.test.ts +++ b/test/abilities/screen-cleaner.test.ts @@ -27,22 +27,41 @@ describe("Abilities - Screen Cleaner", () => { .battleStyle("single") .ability(AbilityId.SCREEN_CLEANER) .enemySpecies(SpeciesId.SHUCKLE) - .weather(WeatherType.HAIL); + .weather(WeatherType.SNOW); }); - // TODO: Screen cleaner doesn't remove both sides' tags if both players have them + // TODO: Screen cleaner doesn't remove both sides' tags if both players have them (as do a LOT of other things) it.todo.each([ { name: "Reflect", tagType: ArenaTagType.REFLECT }, { name: "Light Screen", tagType: ArenaTagType.LIGHT_SCREEN }, { name: "Aurora Veil", tagType: ArenaTagType.AURORA_VEIL }, - ])("should remove $name from both sides of the field on entrance", async ({ tagType }) => { + ])("should remove all instances of $name on entrance", async ({ tagType }) => { game.scene.arena.addTag(tagType, 0, 0, 0, ArenaTagSide.PLAYER); game.scene.arena.addTag(tagType, 0, 0, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(tagType, 0, 0, 0, ArenaTagSide.BOTH); expect(game).toHaveArenaTag(tagType); + await game.classicMode.startBattle([SpeciesId.SLOWKING]); const slowking = game.field.getPlayerPokemon(); expect(slowking).toHaveAbilityApplied(AbilityId.SCREEN_CLEANER); expect(game).not.toHaveArenaTag(tagType); }); + + it("should remove all tag types at once", async () => { + game.scene.arena.addTag(ArenaTagType.REFLECT, 0, 0, 0); + game.scene.arena.addTag(ArenaTagType.LIGHT_SCREEN, 0, 0, 0); + game.scene.arena.addTag(ArenaTagType.AURORA_VEIL, 0, 0, 0); + expect(game).toHaveArenaTag(ArenaTagType.REFLECT); + expect(game).toHaveArenaTag(ArenaTagType.LIGHT_SCREEN); + expect(game).toHaveArenaTag(ArenaTagType.AURORA_VEIL); + + await game.classicMode.startBattle([SpeciesId.SLOWKING]); + + const slowking = game.field.getPlayerPokemon(); + expect(slowking).toHaveAbilityApplied(AbilityId.SCREEN_CLEANER); + expect(game).not.toHaveArenaTag(ArenaTagType.REFLECT); + expect(game).not.toHaveArenaTag(ArenaTagType.LIGHT_SCREEN); + expect(game).not.toHaveArenaTag(ArenaTagType.AURORA_VEIL); + }); }); diff --git a/test/test-utils/helpers/settings-helper.ts b/test/test-utils/helpers/settings-helper.ts index d948a9d5d98..987e883f12d 100644 --- a/test/test-utils/helpers/settings-helper.ts +++ b/test/test-utils/helpers/settings-helper.ts @@ -9,8 +9,7 @@ import { getEnumStr } from "#test/test-utils/string-utils"; import chalk from "chalk"; /** - * Helper to handle settings for tests - * @todo Why does this exist + * Helper to handle changing game settings for tests. */ export class SettingsHelper extends GameManagerHelper { constructor(game: GameManager) { @@ -19,6 +18,9 @@ export class SettingsHelper extends GameManagerHelper { this.initDefaultSettings(); } + /** + * Initialize default settings upon starting a new test case. + */ private initDefaultSettings(): void { this.game.scene.gameSpeed = 5; this.game.scene.moveAnimations = false; @@ -33,42 +35,46 @@ export class SettingsHelper extends GameManagerHelper { } /** - * Change the battle style to Switch or Set mode (tests default to {@linkcode BattleStyle.SET}) - * @param style - The {@linkcode BattleStyle} to set + * Change the current {@linkcode BattleStyle}. + * @param style - The `BattleStyle` to set + * @returns `this` */ - battleStyle(style: BattleStyle): this { + public battleStyle(style: BattleStyle): this { this.game.scene.battleStyle = style; - this.log(`Battle Style set to BattleStyle.${getEnumStr(BattleStyle, style)}!`); + this.log(`Battle Style set to ${getEnumStr(BattleStyle, style)}!`); return this; } /** - * Disable/Enable type hints settings - * @param enable - Whether to enable or disable type hints. + * Toggle the availability of type hints. + * @param enable - Whether to enable or disable type hints * @returns `this` */ - typeHints(enable: boolean): this { + public typeHints(enable: boolean): this { this.game.scene.typeHints = enable; this.log(`Type Hints ${enable ? "enabled" : "disabled"}!`); return this; } /** - * Change the player gender + * Change the player character's selected gender. * @param gender - The {@linkcode PlayerGender} to set + * @returns `this` */ - playerGender(gender: PlayerGender) { + public playerGender(gender: PlayerGender): this { this.game.scene.gameData.gender = gender; - this.log(`Gender set to PlayerGender.${getEnumStr(PlayerGender, gender)}!`); + this.log(`Gender set to ${getEnumStr(PlayerGender, gender)}!`); + return this; } /** - * Change the exp gains speed - * @param speed - the {@linkcode ExpGainsSpeed} to set + * Change the current {@linkcode ExpGainsSpeed}. + * @param speed - The speed to set + * @returns `this` */ - expGainsSpeed(speed: ExpGainsSpeed) { + public expGainsSpeed(speed: ExpGainsSpeed): this { this.game.scene.expGainsSpeed = speed; - this.log(`Exp Gains Speed set to ExpGainsSpeed.${getEnumStr(ExpGainsSpeed, speed)}!`); + this.log(`EXP Gain bar speed set to ${getEnumStr(ExpGainsSpeed, speed)}!`); return this; } From ae6bab28e0b56100726bd9c9602ebdb696ae7d0a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 13 Sep 2025 00:31:28 -0400 Subject: [PATCH 24/30] Added requisite prompt cancellation stuff to ensure prompts are all cleared by the time ME command phase rolls around As a matter of fact, the whole "skip battle, egg dialogues, etc" was never actually triggering (and would 110% break a lot of things) due to there being 3+ option selection/dialogue queueing pormpts ahead blocking the queue --- .../mystery-encounter/encounter-test-utils.ts | 73 +++++-------------- .../bug-type-superfan-encounter.test.ts | 4 +- test/test-utils/game-manager.ts | 19 ++--- 3 files changed, 29 insertions(+), 67 deletions(-) diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 7b2dbfc9aeb..8a7dd8ff809 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -4,13 +4,8 @@ import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: Necessary for mocks import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { CommandPhase } from "#phases/command-phase"; import { MessagePhase } from "#phases/message-phase"; -import { - MysteryEncounterBattlePhase, - MysteryEncounterOptionSelectedPhase, - MysteryEncounterRewardsPhase, -} from "#phases/mystery-encounter-phases"; +import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; import { VictoryPhase } from "#phases/victory-phase"; import type { GameManager } from "#test/test-utils/game-manager"; import type { MessageUiHandler } from "#ui/message-ui-handler"; @@ -47,51 +42,19 @@ export async function runMysteryEncounterToEnd( () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase), ); - if (isBattle) { - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase), - ); - - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.MESSAGE, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase), - ); - - // If a battle is started, fast forward to end of the battle - game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { - game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); - game.scene.phaseManager.unshiftPhase(new VictoryPhase(0)); - game.endPhase(); - }); - - // Handle end of battle trainer messages - game.onNextPrompt("TrainerVictoryPhase", UiMode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - // Handle egg hatch dialogue - game.onNextPrompt("EggLapsePhase", UiMode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - await game.toNextTurn(); - } else { - await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); + if (!isBattle) { + return await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); } + game.onNextPrompt( + "CheckSwitchPhase", + UiMode.CONFIRM, + () => { + game.setMode(UiMode.MESSAGE); + game.endPhase(); + }, + () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase"), + ); + await game.toNextTurn(); } export async function runSelectMysteryEncounterOption( @@ -107,7 +70,7 @@ export async function runSelectMysteryEncounterOption( const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); }, - () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase), + () => game.isCurrentPhase("MysteryEncounterOptionSelectedPhase", "CommandPhase", "TurnInitPhase"), ); if (game.isCurrentPhase(MessagePhase)) { @@ -122,7 +85,7 @@ export async function runSelectMysteryEncounterOption( const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); }, - () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase), + () => game.isCurrentPhase("MysteryEncounterOptionSelectedPhase", "CommandPhase", "TurnInitPhase"), ); await game.phaseInterceptor.to("MysteryEncounterPhase", true); @@ -147,10 +110,10 @@ export async function runSelectMysteryEncounterOption( break; } - if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { - await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo); - } else { + if (secondaryOptionSelect?.pokemonNo == null) { uiHandler.processInput(Button.ACTION); + } else { + await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo); } } diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index f74779f601f..67bff488f37 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -364,9 +364,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await skipBattleRunMysteryEncounterRewardsPhase(game, false); expect(game).toBeAtPhase("MysteryEncounterRewardsPhase"); - // Clear out prompt handlers - // TODO: Is this even needed? - game.promptHandler["prompts"] = []; game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { game.endPhase(); }); @@ -374,6 +371,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(selectOptionSpy).toHaveBeenCalledTimes(1); const optionData = selectOptionSpy.mock.calls[0][0]; + // TODO: This is a bad way to check moves tests expect(PHYSICAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[0].label)).toBe(true); expect(SPECIAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[1].label)).toBe(true); expect(STATUS_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[2].label)).toBe(true); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 8959e0d30ad..6ddc0f0d4b6 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -178,7 +178,7 @@ export class GameManager { * @param expireFn - Optional function to determine if the prompt has expired. * @param awaitingActionInput - If true, will prevent the prompt from activating until the current {@linkcode AwaitableUiHandler} * is awaiting input; default `false` - * @deprecated Remove in favor of {@linkcode promptHandler.addToNextPrompt} + * @deprecated Remove in favor of {@linkcode PromptHandler.addToNextPrompt} */ onNextPrompt( phaseTarget: PhaseString, @@ -407,21 +407,22 @@ export class GameManager { /** * Checks if the current phase matches the target phase. - * @param phaseTarget - The target phase. - * @returns Whether the current phase matches the target phase + * @param phaseTargets - The target phase(s) to check + * @returns Whether the current phase matches any of the target phases * @todo Remove `phaseClass` from signature + * @todo Convert existing calls of `game.isCurrentPhase(A) || game.isCurrentPhase(B)` to pass them together in 1 call */ - isCurrentPhase(phaseTarget: PhaseString): boolean; + public isCurrentPhase(...phaseTargets: [PhaseString, ...PhaseString[]]): boolean; /** * Checks if the current phase matches the target phase. - * @param phaseTarget - The target phase. + * @param phaseTargets - The target phase to check * @returns Whether the current phase matches the target phase * @deprecated Use `PhaseString` instead */ - isCurrentPhase(phaseTarget: PhaseClass): boolean; - isCurrentPhase(phaseTarget: PhaseString | PhaseClass): boolean { - const targetName = typeof phaseTarget === "string" ? phaseTarget : (phaseTarget.name as PhaseString); - return this.scene.phaseManager.getCurrentPhase().is(targetName); + public isCurrentPhase(phaseTargets: PhaseClass): boolean; + public isCurrentPhase(...phaseTargets: (PhaseString | PhaseClass)[]): boolean { + const pName = this.scene.phaseManager.getCurrentPhase().phaseName; + return phaseTargets.some(p => (typeof p === "string" ? p : (p.name as PhaseString) === pName)); } /** From e1d99e5426439689bc1e37b5ac1ad0d132da745e Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 15 Sep 2025 16:28:17 -0400 Subject: [PATCH 25/30] Fixed stupid merge missing brace --- test/mystery-encounter/encounter-test-utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index e22e3c7acdb..f7cdd5b4cc6 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -114,6 +114,7 @@ export async function runSelectMysteryEncounterOption( } else { uiHandler.processInput(Button.ACTION); } +} async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo?: number) { // Handle secondary option selections From 9b02a82e12d703348679643ab606a0976586d879 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 12:19:04 -0400 Subject: [PATCH 26/30] Fixed issues with switch phase jank 0.95 --- .../mystery-encounter/encounter-test-utils.ts | 15 ++++------ .../bug-type-superfan-encounter.test.ts | 1 + test/test-utils/game-manager.ts | 16 ++-------- .../test-utils/helpers/classic-mode-helper.ts | 8 +++-- test/test-utils/helpers/prompt-handler.ts | 3 +- test/test-utils/helpers/reload-helper.ts | 29 +------------------ 6 files changed, 18 insertions(+), 54 deletions(-) diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index f7cdd5b4cc6..6a4b7025f1c 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -1,4 +1,5 @@ import { Status } from "#data/status-effect"; +import { BattleStyle } from "#enums/battle-style"; import { Button } from "#enums/buttons"; import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; @@ -8,6 +9,7 @@ import { MessagePhase } from "#phases/message-phase"; import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; import { VictoryPhase } from "#phases/victory-phase"; import type { GameManager } from "#test/test-utils/game-manager"; +import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console"; import type { MessageUiHandler } from "#ui/message-ui-handler"; import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; @@ -44,15 +46,10 @@ export async function runMysteryEncounterToEnd( if (!isBattle) { return await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); } - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase"), - ); + if (game.scene.battleStyle === BattleStyle.SWITCH) { + MockConsole.queuePostTestWarning("BattleStyle.SWITCH was used during a test case, swapping to set mode..."); + game.settings.battleStyle(BattleStyle.SET); + } await game.toNextTurn(); } diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 67bff488f37..3faef27e685 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -362,6 +362,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game, false); + console.log(game.promptHandler["prompts"]); expect(game).toBeAtPhase("MysteryEncounterRewardsPhase"); game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index db46c3591dd..d3a09fda02b 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -23,7 +23,6 @@ import { NewBattlePhase } from "#phases/new-battle-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import type { SelectTargetPhase } from "#phases/select-target-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { generateStarter } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; @@ -380,17 +379,6 @@ export class GameManager { async toNextWave(): Promise { this.doSelectModifier(); - // forcibly end the message box for switching pokemon - this.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - this.setMode(UiMode.MESSAGE); - this.endPhase(); - }, - () => this.isCurrentPhase(TurnInitPhase), - ); - await this.phaseInterceptor.to("TurnInitPhase"); await this.phaseInterceptor.to("CommandPhase"); console.log("==================[New Wave]=================="); @@ -420,8 +408,8 @@ export class GameManager { */ public isCurrentPhase(phaseTargets: PhaseClass): boolean; public isCurrentPhase(...phaseTargets: (PhaseString | PhaseClass)[]): boolean { - const pName = this.scene.phaseManager.getCurrentPhase().phaseName; - return phaseTargets.some(p => (typeof p === "string" ? p : (p.name as PhaseString) === pName)); + const phase = this.scene.phaseManager.getCurrentPhase(); + return phaseTargets.some(p => phase.is(typeof p === "string" ? p : (p.name as PhaseString))); } /** diff --git a/test/test-utils/helpers/classic-mode-helper.ts b/test/test-utils/helpers/classic-mode-helper.ts index f813a8f797e..15547a221ed 100644 --- a/test/test-utils/helpers/classic-mode-helper.ts +++ b/test/test-utils/helpers/classic-mode-helper.ts @@ -110,7 +110,10 @@ export class ClassicModeHelper extends GameManagerHelper { * Queue inputs to switch at the start of the next battle, and then start it. * @param pokemonIndex - The 0-indexed position of the party pokemon to switch to. * Should never be called with 0 as that will select the currently active pokemon and freeze - * @returns A Promise that resolves once the battle has been started and the switch prompt resolved + * @returns A Promise that resolves once the battle has been started and the switch prompt resolved. + * @remarks + * This will temporarily set the current {@linkcode BattleStyle} to `SWITCH` for the duration + * of the `CheckSwitchPhase`. * @todo Make this work for double battles * @example * ```ts @@ -119,7 +122,7 @@ export class ClassicModeHelper extends GameManagerHelper { * ``` */ public async startBattleWithSwitch(pokemonIndex: number): Promise { - this.game.scene.battleStyle = BattleStyle.SWITCH; + this.game.settings.battleStyle(BattleStyle.SWITCH); this.game.onNextPrompt( "CheckSwitchPhase", UiMode.CONFIRM, @@ -133,5 +136,6 @@ export class ClassicModeHelper extends GameManagerHelper { await this.game.phaseInterceptor.to("CommandPhase"); console.log("==================[New Battle (Initial Switch)]=================="); + this.game.settings.battleStyle(BattleStyle.SET); } } diff --git a/test/test-utils/helpers/prompt-handler.ts b/test/test-utils/helpers/prompt-handler.ts index dadc9314c1b..4d9ef8eef05 100644 --- a/test/test-utils/helpers/prompt-handler.ts +++ b/test/test-utils/helpers/prompt-handler.ts @@ -45,7 +45,8 @@ const endBySetMode: ReadonlyArray = [ /** * Helper class to handle executing prompts upon UI mode changes. - * @todo Remove once a UI overhaul + * @todo Remove once a UI overhaul occurs - + * using this correctly effectively requires one to know the entire phase heiarchy */ export class PromptHandler extends GameManagerHelper { /** An array of {@linkcode UIPrompt | prompts} with associated callbacks. */ diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index 30ff34ae559..728fe556a3a 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -1,8 +1,4 @@ -import { BattleStyle } from "#enums/battle-style"; -import { UiMode } from "#enums/ui-mode"; -import { CommandPhase } from "#phases/command-phase"; import { TitlePhase } from "#phases/title-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; import type { SessionSaveData } from "#types/save-data"; @@ -52,30 +48,7 @@ export class ReloadHelper extends GameManagerHelper { } await titlePhase["loadSaveSlot"](-1); // Load the desired session data - // Run through prompts for switching Pokemon, copied from classicModeHelper.ts - if (this.game.scene.battleStyle === BattleStyle.SWITCH) { - this.game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - this.game.setMode(UiMode.MESSAGE); - this.game.endPhase(); - }, - () => this.game.isCurrentPhase(CommandPhase) || this.game.isCurrentPhase(TurnInitPhase), - ); - - this.game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - this.game.setMode(UiMode.MESSAGE); - this.game.endPhase(); - }, - () => this.game.isCurrentPhase(CommandPhase) || this.game.isCurrentPhase(TurnInitPhase), - ); - } - - await this.game.phaseInterceptor.to(CommandPhase); + await this.game.phaseInterceptor.to("CommandPhase"); console.log("==================[New Turn (Reloaded)]=================="); } } From 831922c247b0a5f5b5ba53d27192f0a3d7b09fcc Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 14:23:54 -0400 Subject: [PATCH 27/30] Fixed callers directly passing `true` to `to` --- test/abilities/good-as-gold.test.ts | 2 +- test/moves/fissure.test.ts | 3 +-- test/moves/focus-punch.test.ts | 5 ++--- test/mystery-encounter/encounter-test-utils.ts | 13 +++++++------ test/test-utils/mocks/mock-phase.ts | 2 +- test/test-utils/phase-interceptor.ts | 6 +++++- .../test-utils/tests/phase-interceptor/unit.test.ts | 6 +++--- test/ui/battle-info.test.ts | 2 +- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/test/abilities/good-as-gold.test.ts b/test/abilities/good-as-gold.test.ts index c6b6faf8349..39c5045505f 100644 --- a/test/abilities/good-as-gold.test.ts +++ b/test/abilities/good-as-gold.test.ts @@ -102,7 +102,7 @@ describe("Abilities - Good As Gold", () => { game.move.select(MoveId.HELPING_HAND, 0); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase"); expect(game.scene.getPlayerField()[1].getTag(BattlerTagType.HELPING_HAND)).toBeUndefined(); }); diff --git a/test/moves/fissure.test.ts b/test/moves/fissure.test.ts index b5255d75d73..06c64621dc6 100644 --- a/test/moves/fissure.test.ts +++ b/test/moves/fissure.test.ts @@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon"; -import { DamageAnimPhase } from "#phases/damage-anim-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; @@ -50,7 +49,7 @@ describe("Moves - Fissure", () => { game.override.ability(AbilityId.NO_GUARD).enemyAbility(AbilityId.FUR_COAT); game.move.select(MoveId.FISSURE); - await game.phaseInterceptor.to(DamageAnimPhase, true); + await game.phaseInterceptor.to("DamageAnimPhase"); expect(enemyPokemon.isFainted()).toBe(true); }); diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts index d7b40569aaa..202cab51294 100644 --- a/test/moves/focus-punch.test.ts +++ b/test/moves/focus-punch.test.ts @@ -123,9 +123,8 @@ describe("Moves - Focus Punch", () => { await game.classicMode.startBattle([SpeciesId.CHARIZARD]); game.move.select(MoveId.FOCUS_PUNCH); - await game.phaseInterceptor.to("MoveEndPhase", true); - await game.phaseInterceptor.to("MessagePhase", false); - await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to("MoveEndPhase"); expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" })); expect(game.textInterceptor.logs).not.toContain(i18next.t("battle:attackFailed")); }); diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 6a4b7025f1c..884a91a3d2a 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -5,11 +5,9 @@ import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: Necessary for mocks import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { MessagePhase } from "#phases/message-phase"; import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; import { VictoryPhase } from "#phases/victory-phase"; import type { GameManager } from "#test/test-utils/game-manager"; -import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console"; import type { MessageUiHandler } from "#ui/message-ui-handler"; import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; @@ -47,7 +45,7 @@ export async function runMysteryEncounterToEnd( return await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); } if (game.scene.battleStyle === BattleStyle.SWITCH) { - MockConsole.queuePostTestWarning("BattleStyle.SWITCH was used during a test case, swapping to set mode..."); + console.warn("BattleStyle.SWITCH was used during ME battle, swapping to set mode..."); game.settings.battleStyle(BattleStyle.SET); } await game.toNextTurn(); @@ -69,7 +67,7 @@ export async function runSelectMysteryEncounterOption( () => game.isCurrentPhase("MysteryEncounterOptionSelectedPhase", "CommandPhase", "TurnInitPhase"), ); - if (game.isCurrentPhase(MessagePhase)) { + if (game.isCurrentPhase("MessagePhase")) { await game.phaseInterceptor.to("MessagePhase"); } @@ -84,7 +82,7 @@ export async function runSelectMysteryEncounterOption( () => game.isCurrentPhase("MysteryEncounterOptionSelectedPhase", "CommandPhase", "TurnInitPhase"), ); - await game.phaseInterceptor.to("MysteryEncounterPhase", true); + await game.phaseInterceptor.to("MysteryEncounterPhase"); // select the desired option const uiHandler = game.scene.ui.getHandler(); @@ -154,7 +152,10 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, * @param game * @param runRewardsPhase */ -export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) { +export async function skipBattleRunMysteryEncounterRewardsPhase( + game: GameManager, + runRewardsPhase?: false | undefined, +) { game.scene.phaseManager.clearPhaseQueue(); game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.getEnemyParty().forEach(p => { diff --git a/test/test-utils/mocks/mock-phase.ts b/test/test-utils/mocks/mock-phase.ts index 6a64c7472e5..a970356b3d5 100644 --- a/test/test-utils/mocks/mock-phase.ts +++ b/test/test-utils/mocks/mock-phase.ts @@ -6,7 +6,7 @@ import { Phase } from "#app/phase"; */ export abstract class mockPhase extends Phase { public phaseName: any; - start() { + public override start() { this.end(); } } diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 2107eb65108..0194cbf5fd4 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -87,7 +87,6 @@ export class PhaseInterceptor { * @param target - The name of the {@linkcode Phase} to transition to * @param runTarget - Whether or not to run the target phase before resolving; default `true` * @returns A Promise that resolves once `target` has been reached. - * @todo remove `Constructor` from type signature in favor of phase strings * @remarks * This will not resolve for _any_ reason until the target phase has been reached. * @example @@ -95,6 +94,11 @@ export class PhaseInterceptor { * await game.phaseInterceptor.to("MoveEffectPhase", false); * ``` */ + public async to(target: PhaseString, runTarget?: boolean): Promise; + /** + * @deprecated Use `PhaseString` instead for `target` + */ + public async to(target: Constructor, runTarget?: boolean): Promise; public async to(target: PhaseString | Constructor, runTarget = true): Promise { this.target = typeof target === "string" ? target : (target.name as PhaseString); diff --git a/test/test-utils/tests/phase-interceptor/unit.test.ts b/test/test-utils/tests/phase-interceptor/unit.test.ts index de4283c3af4..8c44206488c 100644 --- a/test/test-utils/tests/phase-interceptor/unit.test.ts +++ b/test/test-utils/tests/phase-interceptor/unit.test.ts @@ -60,9 +60,9 @@ describe("Utils - Phase Interceptor - Unit", () => { * @param phases - An array of constructors to {@linkcode Phase}s to set. * Constructors must have no arguments. */ - function setPhases(phase: Constructor, ...phases: Constructor[]) { + function setPhases(...phases: [Constructor, ...Constructor[]]) { game.scene.phaseManager.clearAllPhases(); - game.scene.phaseManager.phaseQueue = [phase, ...phases].map(m => new m()) as Phase[]; + game.scene.phaseManager.phaseQueue = phases.map(m => new m()) as Phase[]; game.scene.phaseManager.shiftPhase(); // start the thing going } @@ -77,7 +77,7 @@ describe("Utils - Phase Interceptor - Unit", () => { } /** Wrapper function to make TS not complain about incompatible argument typing on `PhaseString`. */ - function to(phaseName: string, runTarget = true) { + function to(phaseName: string, runTarget?: false): Promise { return game.phaseInterceptor.to(phaseName as unknown as PhaseString, runTarget); } diff --git a/test/ui/battle-info.test.ts b/test/ui/battle-info.test.ts index 6588064d63f..4e77b880e42 100644 --- a/test/ui/battle-info.test.ts +++ b/test/ui/battle-info.test.ts @@ -48,7 +48,7 @@ describe.todo("UI - Battle Info", () => { game.move.select(MoveId.SPLASH); await game.doKillOpponents(); - await game.phaseInterceptor.to("ExpPhase", true); + await game.phaseInterceptor.to("ExpPhase"); expect(Math.pow).not.toHaveBeenCalledWith(2, expGainsSpeed); }, From 70ca72aad50d7a5782290dcfa60bcd8fc9ca6acf Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:56:05 -0400 Subject: [PATCH 28/30] Removed leftover logging statement in bug type superfan test --- .../encounters/bug-type-superfan-encounter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 3faef27e685..67bff488f37 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -362,7 +362,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game, false); - console.log(game.promptHandler["prompts"]); expect(game).toBeAtPhase("MysteryEncounterRewardsPhase"); game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { From 55782e5dbc31dd5ca708334311a2a9ac574f44ee Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 18:58:16 -0400 Subject: [PATCH 29/30] Fixed unit tests to not explode in dramatic fashion --- test/test-utils/tests/phase-interceptor/unit.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test-utils/tests/phase-interceptor/unit.test.ts b/test/test-utils/tests/phase-interceptor/unit.test.ts index 8c44206488c..1b6daf838aa 100644 --- a/test/test-utils/tests/phase-interceptor/unit.test.ts +++ b/test/test-utils/tests/phase-interceptor/unit.test.ts @@ -62,14 +62,14 @@ describe("Utils - Phase Interceptor - Unit", () => { */ function setPhases(...phases: [Constructor, ...Constructor[]]) { game.scene.phaseManager.clearAllPhases(); - game.scene.phaseManager.phaseQueue = phases.map(m => new m()) as Phase[]; + for (const phase of phases) { + game.scene.phaseManager.unshiftPhase(new phase()); + } 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); + return game.scene.phaseManager["phaseQueue"]["levels"].flat(2).map(p => p.phaseName); } function expectAtPhase(phaseName: string) { From b5b3f677c46a0b8d30ce059024a6591b95fd1a35 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 19:50:34 -0400 Subject: [PATCH 30/30] Removed phase log blacklist --- test/test-utils/phase-interceptor.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 0194cbf5fd4..74ea58e396f 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -13,13 +13,6 @@ import chalk from "chalk"; import { vi } from "vitest"; import { getEnumStr } from "./string-utils"; -/** - * A {@linkcode ReadonlySet} containing phase names that will not be shown in the console when started. - * - * Used to reduce console noise from very repetitive phases. - */ -const blacklistedPhaseNames: ReadonlySet = new Set(["ActivatePriorityQueuePhase"]); - /** * The interceptor's current state. * Possible values are the following: @@ -52,11 +45,6 @@ export class PhaseInterceptor { private state: StateType = "idling"; /** The current target that is being ran to. */ private target: PhaseString; - /** - * Whether to respect {@linkcode blacklistedPhaseNames} during logging. - * @defaultValue `true` - */ - private respectBlacklist = true; /** * Initialize a new PhaseInterceptor. @@ -220,9 +208,7 @@ export class PhaseInterceptor { * @param phaseName - The name of the phase to log */ private logPhase(phaseName: PhaseString): void { - if (this.respectBlacklist && !blacklistedPhaseNames.has(phaseName)) { - console.log(`%cStart Phase: ${phaseName}`, `color:${PHASE_START_COLOR}`); - } + console.log(`%cStart Phase: ${phaseName}`, `color:${PHASE_START_COLOR}`); this.log.push(phaseName); } @@ -233,16 +219,6 @@ export class PhaseInterceptor { this.log = []; } - /** - * Toggle the Interceptor's logging blacklist, enabling or disabling logging all phases in - * {@linkcode blacklistedPhaseNames}. - * @param state - Whether the blacklist should be enabled; default `false` (disable) - */ - public toggleBlacklist(state = false): void { - this.respectBlacklist = state; - this.doLog(`Phase blacklist logging ${state ? "disabled" : "enabled"}!`); - } - /** * Wrapper function to add coral coloration to phase logs. * @param args - Arguments to original logging function