From c301a5403974af7888fcc31a3196a4ac7ff9d1ad Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Sat, 8 Jun 2024 21:54:20 +0200 Subject: [PATCH] [Improvement] Refactored Test Phase Management with Error Handling (#1962) * better phase management in tests * cleanup runFrom/to * first step of async error handling * second step of async error handling, halt await, still an issue with select-starter test * added back whenAboutToRun * added back full run suite for starter-select.test.ts --- src/test/abilities/intrepid_sword.test.ts | 8 +- src/test/battle/battle-order.test.ts | 16 +- src/test/battle/battle.test.ts | 223 +++++++------- src/test/battle/error-handling.test.ts | 39 +++ src/test/phases/phases.test.ts | 13 +- src/test/ui/starter-select.test.ts | 346 ++++++++++------------ src/test/utils/errorInterceptor.ts | 50 ++++ src/test/utils/gameManager.ts | 38 +-- src/test/utils/gameWrapper.ts | 1 - src/test/utils/phaseInterceptor.ts | 215 ++++++++------ src/test/vitest.setup.ts | 2 + 11 files changed, 529 insertions(+), 422 deletions(-) create mode 100644 src/test/battle/error-handling.test.ts create mode 100644 src/test/utils/errorInterceptor.ts diff --git a/src/test/abilities/intrepid_sword.test.ts b/src/test/abilities/intrepid_sword.test.ts index f2e8eeb12cb..da2beb9c029 100644 --- a/src/test/abilities/intrepid_sword.test.ts +++ b/src/test/abilities/intrepid_sword.test.ts @@ -44,8 +44,8 @@ describe("Abilities - Intrepid Sword", () => { expect(game.scene.getParty()[0].summonData).not.toBeUndefined(); let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - await game.phaseInterceptor.mustRun(ShowAbilityPhase).catch((error) => expect(error).toBe(ShowAbilityPhase)); - await game.phaseInterceptor.mustRun(StatChangePhase).catch((error) => expect(error).toBe(StatChangePhase)); + await game.phaseInterceptor.run(ShowAbilityPhase); + await game.phaseInterceptor.run(StatChangePhase); battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); }, 20000); @@ -57,8 +57,8 @@ describe("Abilities - Intrepid Sword", () => { let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); await game.phaseInterceptor.runFrom(PostSummonPhase).to(ToggleDoublePositionPhase); - await game.phaseInterceptor.mustRun(StatChangePhase).catch((error) => expect(error).toBe(StatChangePhase)); - await game.phaseInterceptor.whenAboutToRun(MessagePhase); + await game.phaseInterceptor.run(StatChangePhase); + await game.phaseInterceptor.run(MessagePhase); battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(1); }, 20000); diff --git a/src/test/battle/battle-order.test.ts b/src/test/battle/battle-order.test.ts index e481150fafe..7ace7dae224 100644 --- a/src/test/battle/battle-order.test.ts +++ b/src/test/battle/battle-order.test.ts @@ -5,7 +5,7 @@ import * as overrides from "#app/overrides"; import {Abilities} from "#app/data/enums/abilities"; import {Species} from "#app/data/enums/species"; import { - CommandPhase, EnemyCommandPhase, + CommandPhase, EnemyCommandPhase, SelectTargetPhase, TurnStartPhase } from "#app/phases"; import {Mode} from "#app/ui/ui"; @@ -55,7 +55,6 @@ describe("Battle order", () => { (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.whenAboutToRun(TurnStartPhase); const phase = game.scene.getCurrentPhase() as TurnStartPhase; const order = phase.getOrder(); expect(order[0]).toBe(2); @@ -77,7 +76,6 @@ describe("Battle order", () => { (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.whenAboutToRun(TurnStartPhase); const phase = game.scene.getCurrentPhase() as TurnStartPhase; const order = phase.getOrder(); expect(order[0]).toBe(0); @@ -118,9 +116,7 @@ describe("Battle order", () => { const handler = game.scene.ui.getHandler() as TargetSelectUiHandler; handler.processInput(Button.ACTION); }); - await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.whenAboutToRun(TurnStartPhase); + await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); const phase = game.scene.getCurrentPhase() as TurnStartPhase; const order = phase.getOrder(); expect(order.indexOf(0)).toBeGreaterThan(order.indexOf(2)); @@ -163,9 +159,7 @@ describe("Battle order", () => { const handler = game.scene.ui.getHandler() as TargetSelectUiHandler; handler.processInput(Button.ACTION); }); - await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.whenAboutToRun(TurnStartPhase); + await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); const phase = game.scene.getCurrentPhase() as TurnStartPhase; const order = phase.getOrder(); expect(order.indexOf(3)).toBeLessThan(order.indexOf(0)); @@ -207,9 +201,7 @@ describe("Battle order", () => { const handler = game.scene.ui.getHandler() as TargetSelectUiHandler; handler.processInput(Button.ACTION); }); - await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.whenAboutToRun(TurnStartPhase); + await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); const phase = game.scene.getCurrentPhase() as TurnStartPhase; const order = phase.getOrder(); expect(order.indexOf(1)).toBeLessThan(order.indexOf(0)); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 5a747eb8749..28f98d936ce 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -1,34 +1,21 @@ import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; -import {generateStarter, getMovePosition, waitUntil,} from "#app/test/utils/gameManagerUtils"; +import {generateStarter, getMovePosition,} from "#app/test/utils/gameManagerUtils"; import {Mode} from "#app/ui/ui"; import {GameModes} from "#app/game-mode"; import {Species} from "#app/data/enums/species"; import * as overrides from "../../overrides"; import {Command} from "#app/ui/command-ui-handler"; import { - BattleEndPhase, - BerryPhase, CommandPhase, - DamagePhase, - EggLapsePhase, EncounterPhase, EnemyCommandPhase, - FaintPhase, LoginPhase, - MessagePhase, - MoveEffectPhase, - MoveEndPhase, - MovePhase, - PostSummonPhase, SelectGenderPhase, SelectModifierPhase, SelectStarterPhase, - StatChangePhase, + SummonPhase, TitlePhase, - TurnEndPhase, TurnInitPhase, - TurnStartPhase, - VictoryPhase, } from "#app/phases"; import {Moves} from "#app/data/enums/moves"; import GameManager from "#app/test/utils/gameManager"; @@ -36,6 +23,7 @@ import Phaser from "phaser"; import {allSpecies} from "#app/data/pokemon-species"; import {PlayerGender} from "#app/data/enums/player-gender"; import { getGameMode } from "#app/game-mode.js"; +import {Abilities} from "#app/data/enums/abilities"; describe("Test Battle Phase", () => { let phaserGame: Phaser.Game; @@ -55,22 +43,6 @@ describe("Test Battle Phase", () => { game = new GameManager(phaserGame); }); - it("test phase interceptor with remove", async() => { - await game.phaseInterceptor.run(LoginPhase); - - await game.phaseInterceptor.run(LoginPhase, () => { - return game.phaseInterceptor.log.includes("LoginPhase"); - }); - - game.scene.gameData.gender = PlayerGender.MALE; - await game.phaseInterceptor.remove(SelectGenderPhase, () => game.isCurrentPhase(TitlePhase)); - - await game.phaseInterceptor.run(TitlePhase); - await waitUntil(() => game.scene.ui?.getMode() === Mode.TITLE); - - expect(game.scene.ui?.getMode()).toBe(Mode.TITLE); - }, 100000); - it("test phase interceptor with prompt", async() => { await game.phaseInterceptor.run(LoginPhase); @@ -87,7 +59,7 @@ describe("Test Battle Phase", () => { expect(game.scene.ui?.getMode()).toBe(Mode.TITLE); expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }, 100000); + }, 20000); it("test phase interceptor with prompt with preparation for a future prompt", async() => { await game.phaseInterceptor.run(LoginPhase); @@ -109,13 +81,13 @@ describe("Test Battle Phase", () => { expect(game.scene.ui?.getMode()).toBe(Mode.TITLE); expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }, 100000); + }, 20000); it("newGame one-liner", async() => { await game.startBattle(); expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); - }, 100000); + }, 20000); it("do attack wave 3 - single battle - regular - OHKO", async() => { vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); @@ -123,6 +95,8 @@ describe("Test Battle Phase", () => { vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); await game.startBattle(); game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { @@ -132,28 +106,10 @@ describe("Test Battle Phase", () => { const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE); (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.run(TurnStartPhase); - - await game.phaseInterceptor.run(MovePhase); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(MoveEffectPhase); - await game.phaseInterceptor.run(DamagePhase); - await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(FaintPhase)); - await game.phaseInterceptor.run(FaintPhase); - await game.phaseInterceptor.run(MessagePhase); - - await game.phaseInterceptor.run(VictoryPhase); - await game.phaseInterceptor.run(MoveEndPhase); - await game.phaseInterceptor.run(MovePhase); - await game.phaseInterceptor.run(BerryPhase); - await game.phaseInterceptor.run(TurnEndPhase); - await game.phaseInterceptor.run(BattleEndPhase); - await game.phaseInterceptor.run(EggLapsePhase); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(SelectModifierPhase); expect(game.scene.ui?.getMode()).toBe(Mode.MODIFIER_SELECT); expect(game.scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); - }, 100000); + }, 20000); it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async() => { vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); @@ -161,7 +117,8 @@ describe("Test Battle Phase", () => { vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5); vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); - vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TAIL_WHIP]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TAIL_WHIP, Moves.TAIL_WHIP, Moves.TAIL_WHIP, Moves.TAIL_WHIP]); vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); await game.startBattle(); game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { @@ -171,35 +128,8 @@ describe("Test Battle Phase", () => { const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE); (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.run(TurnStartPhase); - - await game.phaseInterceptor.run(MovePhase); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(MoveEffectPhase); - await game.phaseInterceptor.run(DamagePhase); - await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEndPhase)); - await game.phaseInterceptor.run(MoveEndPhase); - - await game.phaseInterceptor.run(MovePhase); - await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEffectPhase)); - await game.phaseInterceptor.run(MoveEffectPhase); - game.scene.moveAnimations = null; // Mandatory to avoid the crash - await game.phaseInterceptor.run(StatChangePhase, () => game.isCurrentPhase(MessagePhase) || game.isCurrentPhase(MoveEndPhase) || game.isCurrentPhase(DamagePhase)); - await game.phaseInterceptor.run(DamagePhase, () => game.isCurrentPhase(MessagePhase) || game.isCurrentPhase(MoveEndPhase)); - await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEndPhase)); - await game.phaseInterceptor.run(MoveEndPhase); - - await game.phaseInterceptor.run(BerryPhase); - await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(TurnEndPhase)); - await game.phaseInterceptor.run(TurnEndPhase); - - await game.phaseInterceptor.run(TurnInitPhase); - await game.phaseInterceptor.run(CommandPhase); - await waitUntil(() => game.scene.ui?.getMode() === Mode.COMMAND); - expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); - expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); - }, 100000); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); + }, 20000); it("load 100% data file", async() => { await game.importData("src/test/utils/saves/everything.prsv"); @@ -208,7 +138,7 @@ describe("Test Battle Phase", () => { return species.caughtAttr !== 0n; }).length; expect(caughtCount).toBe(Object.keys(allSpecies).length); - }, 50000); + }, 20000); it("start battle with selected team", async() => { await game.startBattle([ @@ -219,26 +149,7 @@ describe("Test Battle Phase", () => { expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CHARIZARD); expect(game.scene.getParty()[1].species.speciesId).toBe(Species.CHANSEY); expect(game.scene.getParty()[2].species.speciesId).toBe(Species.MEW); - }, 50000); - - it("assert next phase", async() => { - await game.phaseInterceptor.run(LoginPhase); - game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }, () => game.isCurrentPhase(TitlePhase)); - await game.phaseInterceptor.mustRun(SelectGenderPhase).catch((error) => expect(error).toBe(SelectGenderPhase)); - await game.phaseInterceptor.mustRun(TitlePhase).catch((error) => expect(error).toBe(TitlePhase)); - game.onNextPrompt("TitlePhase", Mode.TITLE, () => { - game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(game.scene); - const selectStarterPhase = new SelectStarterPhase(game.scene); - game.scene.pushPhase(new EncounterPhase(game.scene, false)); - selectStarterPhase.initBattle(starters); - }); - await game.phaseInterceptor.mustRun(EncounterPhase).catch((error) => expect(error).toBe(EncounterPhase)); - await game.phaseInterceptor.mustRun(PostSummonPhase).catch((error) => expect(error).toBe(PostSummonPhase)); - }, 50000); + }, 20000); it("test remove random battle seed int", async() => { for (let i=0; i<10; i++) { @@ -246,5 +157,107 @@ describe("Test Battle Phase", () => { expect(rand).toBe(14); } }); + + it("wrong phase", async() => { + await game.phaseInterceptor.run(LoginPhase); + await game.phaseInterceptor.run(LoginPhase).catch((e) => { + expect(e).toBe("Wrong phase: this is SelectGenderPhase and not LoginPhase"); + }); + }, 20000); + + it("wrong phase but skip", async() => { + await game.phaseInterceptor.run(LoginPhase); + await game.phaseInterceptor.run(LoginPhase, () => game.isCurrentPhase(SelectGenderPhase)); + }, 20000); + + it("good run", async() => { + await game.phaseInterceptor.run(LoginPhase); + game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { + game.scene.gameData.gender = PlayerGender.MALE; + game.endPhase(); + }, () => game.isCurrentPhase(TitlePhase)); + await game.phaseInterceptor.run(SelectGenderPhase, () => game.isCurrentPhase(TitlePhase)); + await game.phaseInterceptor.run(TitlePhase); + }, 20000); + + it("good run from select gender to title", async() => { + await game.phaseInterceptor.run(LoginPhase); + game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { + game.scene.gameData.gender = PlayerGender.MALE; + game.endPhase(); + }, () => game.isCurrentPhase(TitlePhase)); + await game.phaseInterceptor.runFrom(SelectGenderPhase).to(TitlePhase); + }, 20000); + + it("good run to SummonPhase phase", async() => { + await game.phaseInterceptor.run(LoginPhase); + game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { + game.scene.gameData.gender = PlayerGender.MALE; + game.endPhase(); + }, () => game.isCurrentPhase(TitlePhase)); + game.onNextPrompt("TitlePhase", Mode.TITLE, () => { + game.scene.gameMode = getGameMode(GameModes.CLASSIC); + const starters = generateStarter(game.scene); + const selectStarterPhase = new SelectStarterPhase(game.scene); + game.scene.pushPhase(new EncounterPhase(game.scene, false)); + selectStarterPhase.initBattle(starters); + }); + await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase); + }, 20000); + + it("2vs1", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("1vs1", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + await game.startBattle([ + Species.BLASTOISE, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("2vs2", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("4vs2", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + Species.DARKRAI, + Species.GABITE, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); }); diff --git a/src/test/battle/error-handling.test.ts b/src/test/battle/error-handling.test.ts new file mode 100644 index 00000000000..2cb29aab1e7 --- /dev/null +++ b/src/test/battle/error-handling.test.ts @@ -0,0 +1,39 @@ +import {afterEach, beforeAll, beforeEach, describe, it, vi} from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import * as overrides from "#app/overrides"; +import {Species} from "#app/data/enums/species"; +import {Moves} from "#app/data/enums/moves"; +import {Abilities} from "#app/data/enums/abilities"; + +describe("Test Battle Phase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + }); + + it("should start phase", async() => { + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + await game.startBattle(); + }, 100000); +}); + diff --git a/src/test/phases/phases.test.ts b/src/test/phases/phases.test.ts index abb7ac2c975..009526ebe41 100644 --- a/src/test/phases/phases.test.ts +++ b/src/test/phases/phases.test.ts @@ -28,7 +28,8 @@ describe("Phases", () => { describe("LoginPhase", () => { it("should start the login phase", async () => { const loginPhase = new LoginPhase(scene); - loginPhase.start(); + scene.pushPhase(loginPhase); + await game.phaseInterceptor.run(LoginPhase); expect(scene.ui.getMode()).to.equal(Mode.MESSAGE); }); }); @@ -36,16 +37,18 @@ describe("Phases", () => { describe("TitlePhase", () => { it("should start the title phase", async () => { const titlePhase = new TitlePhase(scene); - titlePhase.start(); - expect(scene.ui.getMode()).to.equal(Mode.MESSAGE); + scene.pushPhase(titlePhase); + await game.phaseInterceptor.run(TitlePhase); + expect(scene.ui.getMode()).to.equal(Mode.TITLE); }); }); describe("UnavailablePhase", () => { it("should start the unavailable phase", async () => { const unavailablePhase = new UnavailablePhase(scene); - unavailablePhase.start(); + scene.pushPhase(unavailablePhase); + await game.phaseInterceptor.run(UnavailablePhase); expect(scene.ui.getMode()).to.equal(Mode.UNAVAILABLE); - }); + }, 20000); }); }); diff --git a/src/test/ui/starter-select.test.ts b/src/test/ui/starter-select.test.ts index 802542da259..9f323a2839c 100644 --- a/src/test/ui/starter-select.test.ts +++ b/src/test/ui/starter-select.test.ts @@ -15,9 +15,9 @@ import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import SaveSlotSelectUiHandler from "#app/ui/save-slot-select-ui-handler"; import {OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler"; import {Gender} from "#app/data/gender"; +import {allSpecies} from "#app/data/pokemon-species"; import {Nature} from "#app/data/nature"; import {Abilities} from "#app/data/enums/abilities"; -import {allSpecies} from "#app/data/pokemon-species"; describe("UI - Starter select", () => { @@ -51,12 +51,13 @@ describe("UI - Starter select", () => { currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -73,21 +74,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -99,19 +99,25 @@ describe("UI - Starter select", () => { it("Bulbasaur - shiny - variant 2 female hardy overgrow", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); handler.processInput(Button.CYCLE_GENDER); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -128,21 +134,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -153,15 +158,19 @@ describe("UI - Starter select", () => { expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.OVERGROW); }, 20000); - it("Bulbasaur - shiny - variant 2 female lonely cholorophyl", async() => { + it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); @@ -169,7 +178,9 @@ describe("UI - Starter select", () => { handler.processInput(Button.CYCLE_NATURE); handler.processInput(Button.CYCLE_ABILITY); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -186,21 +197,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -211,21 +221,27 @@ describe("UI - Starter select", () => { expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL); }, 20000); - it("Bulbasaur - shiny - variant 2 female", async() => { + it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); handler.processInput(Button.CYCLE_GENDER); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -242,21 +258,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -268,19 +283,25 @@ describe("UI - Starter select", () => { it("Bulbasaur - not shiny", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); handler.processInput(Button.CYCLE_SHINY); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -297,21 +318,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -320,76 +340,28 @@ describe("UI - Starter select", () => { expect(game.scene.getParty()[0].variant).toBe(0); }, 20000); - it("Bulbasaur - shiny - variant 0", async() => { - await game.importData("src/test/utils/saves/everything.prsv"); - await game.runToTitle(); - game.onNextPrompt("TitlePhase", Mode.TITLE, () => { - const currentPhase = game.scene.getCurrentPhase() as TitlePhase; - currentPhase.gameMode = GameModes.CLASSIC; - currentPhase.end(); - }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.RIGHT); - handler.processInput(Button.V); - handler.processInput(Button.ACTION); - }); - let options: OptionSelectItem[]; - let optionSelectUiHandler: OptionSelectUiHandler; - await new Promise((resolve) => { - game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => { - optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler; - options = optionSelectUiHandler.getOptionsWithScroll(); - resolve(); - }); - }); - expect(options.some(option => option.label === "Add to Party")).toBe(true); - expect(options.some(option => option.label === "Toggle IVs")).toBe(true); - expect(options.some(option => option.label === "Manage Moves")).toBe(true); - expect(options.some(option => option.label === "Use Candies")).toBe(true); - expect(options.some(option => option.label === "Cancel")).toBe(true); - optionSelectUiHandler.processInput(Button.ACTION); - - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(0); - }, 20000); - it("Bulbasaur - shiny - variant 1", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); handler.processInput(Button.V); handler.processInput(Button.V); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -406,21 +378,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -429,15 +400,19 @@ describe("UI - Starter select", () => { expect(game.scene.getParty()[0].variant).toBe(1); }, 20000); - it("Bulbasaur - shiny - variant 1", async() => { + it("Bulbasaur - shiny - variant 2", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); @@ -445,7 +420,9 @@ describe("UI - Starter select", () => { handler.processInput(Button.V); handler.processInput(Button.V); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -462,21 +439,20 @@ describe("UI - Starter select", () => { expect(options.some(option => option.label === "Cancel")).toBe(true); optionSelectUiHandler.processInput(Button.ACTION); - game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.SUBMIT); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { - const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; - saveSlotSelectUiHandler.processInput(Button.ACTION); - }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); + await new Promise((resolve) => { + game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.SUBMIT); + }); + game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { + const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; + handler.processInput(Button.ACTION); + }); + game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => { + const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; + saveSlotSelectUiHandler.processInput(Button.ACTION); + resolve(); + }); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); @@ -485,15 +461,19 @@ describe("UI - Starter select", () => { expect(game.scene.getParty()[0].variant).toBe(2); }, 20000); - it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column ", async() => { + it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); @@ -501,7 +481,9 @@ describe("UI - Starter select", () => { handler.processInput(Button.RIGHT); handler.processInput(Button.RIGHT); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -526,6 +508,7 @@ describe("UI - Starter select", () => { resolve(); }); }); + expect(starterSelectUiHandler.starterGens[0]).toBe(0); expect(starterSelectUiHandler.starterCursors[0]).toBe(3); expect(starterSelectUiHandler.cursorObj.x).toBe(132 + 4 * 18); @@ -539,23 +522,23 @@ describe("UI - Starter select", () => { const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; saveSlotSelectUiHandler.processInput(Button.ACTION); }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE); }, 20000); - it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1) ", async() => { + it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1)", async() => { await game.importData("src/test/utils/saves/everything.prsv"); + const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => { + const species = game.scene.gameData.dexData[key]; + return species.caughtAttr !== 0n; + }).length; + expect(caughtCount).toBe(Object.keys(allSpecies).length); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { const currentPhase = game.scene.getCurrentPhase() as TitlePhase; currentPhase.gameMode = GameModes.CLASSIC; currentPhase.end(); }); - await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase)); game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => { const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; handler.processInput(Button.RIGHT); @@ -564,7 +547,9 @@ describe("UI - Starter select", () => { handler.processInput(Button.RIGHT); handler.processInput(Button.DOWN); handler.processInput(Button.ACTION); + game.phaseInterceptor.unlock(); }); + await game.phaseInterceptor.run(SelectStarterPhase); let options: OptionSelectItem[]; let optionSelectUiHandler: OptionSelectUiHandler; await new Promise((resolve) => { @@ -589,6 +574,7 @@ describe("UI - Starter select", () => { resolve(); }); }); + expect(starterSelectUiHandler.starterGens[0]).toBe(0); expect(starterSelectUiHandler.starterCursors[0]).toBe(12); expect(starterSelectUiHandler.cursorObj.x).toBe(132 + 4 * 18); @@ -602,10 +588,6 @@ describe("UI - Starter select", () => { const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; saveSlotSelectUiHandler.processInput(Button.ACTION); }); - game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => { - const handler = game.scene.ui.getHandler() as StarterSelectUiHandler; - handler.processInput(Button.ACTION); - }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M); }, 20000); diff --git a/src/test/utils/errorInterceptor.ts b/src/test/utils/errorInterceptor.ts new file mode 100644 index 00000000000..7f06c47fd39 --- /dev/null +++ b/src/test/utils/errorInterceptor.ts @@ -0,0 +1,50 @@ +export default 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/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cf38fbe01bb..dc1991d5659 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -2,21 +2,17 @@ import GameWrapper from "#app/test/utils/gameWrapper"; import {Mode} from "#app/ui/ui"; import {generateStarter, waitUntil} from "#app/test/utils/gameManagerUtils"; import { - CheckSwitchPhase, CommandPhase, EncounterPhase, LoginPhase, PostSummonPhase, SelectGenderPhase, SelectStarterPhase, - SummonPhase, TitlePhase, - ToggleDoublePositionPhase, } from "#app/phases"; import BattleScene from "#app/battle-scene.js"; import PhaseInterceptor from "#app/test/utils/phaseInterceptor"; import TextInterceptor from "#app/test/utils/TextInterceptor"; -import {expect} from "vitest"; import {GameModes, getGameMode} from "#app/game-mode"; import fs from "fs"; import { AES, enc } from "crypto-js"; @@ -26,6 +22,7 @@ import {PlayerGender} from "#app/data/enums/player-gender"; import {GameDataType} from "#app/data/enums/game-data-type"; import InputsHandler from "#app/test/utils/inputsHandler"; import {ExpNotification} from "#app/enums/exp-notification"; +import ErrorInterceptor from "#app/test/utils/errorInterceptor"; /** * Class to manage the game state and transitions between phases. @@ -43,6 +40,8 @@ export default class GameManager { * @param bypassLogin - Whether to bypass the login phase. */ constructor(phaserGame: Phaser.Game, bypassLogin: boolean = true) { + localStorage.clear(); + ErrorInterceptor.getInstance().clear(); BattleScene.prototype.randBattleSeedInt = (arg) => arg-1; this.gameWrapper = new GameWrapper(phaserGame, bypassLogin); this.scene = new BattleScene(); @@ -94,14 +93,14 @@ export default class GameManager { * @returns A promise that resolves when the title phase is reached. */ runToTitle(): Promise { - return new Promise(async(resolve) => { - await this.phaseInterceptor.run(LoginPhase); + return new Promise(async(resolve, reject) => { + await this.phaseInterceptor.run(LoginPhase).catch((e) => reject(e)); this.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { this.scene.gameData.gender = PlayerGender.MALE; this.endPhase(); }, () => this.isCurrentPhase(TitlePhase)); - await this.phaseInterceptor.run(SelectGenderPhase, () => this.isCurrentPhase(TitlePhase)); - await this.phaseInterceptor.run(TitlePhase); + await this.phaseInterceptor.run(SelectGenderPhase, () => this.isCurrentPhase(TitlePhase)).catch((e) => reject(e)); + await this.phaseInterceptor.run(TitlePhase).catch((e) => reject(e)); this.scene.gameSpeed = 5; this.scene.moveAnimations = false; this.scene.showLevelUpStats = false; @@ -118,8 +117,8 @@ export default class GameManager { * @returns A promise that resolves when the summon phase is reached. */ runToSummon(species?: Species[]): Promise { - return new Promise(async(resolve) => { - await this.runToTitle(); + return new Promise(async(resolve, reject) => { + await this.runToTitle().catch((e) => reject(e)); this.onNextPrompt("TitlePhase", Mode.TITLE, () => { this.scene.gameMode = getGameMode(GameModes.CLASSIC); const starters = generateStarter(this.scene, species); @@ -127,7 +126,7 @@ export default class GameManager { this.scene.pushPhase(new EncounterPhase(this.scene, false)); selectStarterPhase.initBattle(starters); }); - await this.phaseInterceptor.run(EncounterPhase); + await this.phaseInterceptor.run(EncounterPhase).catch((e) => reject(e)); resolve(); }); } @@ -138,25 +137,18 @@ export default class GameManager { * @returns A promise that resolves when the battle is started. */ startBattle(species?: Species[]): Promise { - return new Promise(async(resolve) => { - await this.runToSummon(species); - await this.phaseInterceptor.runFrom(PostSummonPhase).to(ToggleDoublePositionPhase); - await this.phaseInterceptor.run(SummonPhase, () => this.isCurrentPhase(CheckSwitchPhase) || this.isCurrentPhase(PostSummonPhase)); + return new Promise(async(resolve, reject) => { + await this.runToSummon(species).catch((e) => reject(e)); this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { this.setMode(Mode.MESSAGE); this.endPhase(); - }, () => this.isCurrentPhase(PostSummonPhase)); + }, () => this.isCurrentPhase(CommandPhase)); this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { this.setMode(Mode.MESSAGE); this.endPhase(); - }, () => this.isCurrentPhase(PostSummonPhase)); - await this.phaseInterceptor.run(CheckSwitchPhase, () => this.isCurrentPhase(PostSummonPhase)); - await this.phaseInterceptor.run(CheckSwitchPhase, () => this.isCurrentPhase(PostSummonPhase)); - await this.phaseInterceptor.runFrom(PostSummonPhase).to(CommandPhase); - await waitUntil(() => this.scene.ui?.getMode() === Mode.COMMAND); + }, () => this.isCurrentPhase(CommandPhase)); + await this.phaseInterceptor.runFrom(PostSummonPhase).to(CommandPhase).catch((e) => reject(e)); console.log("==================[New Turn]=================="); - expect(this.scene.ui?.getMode()).toBe(Mode.COMMAND); - expect(this.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); return resolve(); }); } diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index c9aed9865db..7d0e4110351 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -86,7 +86,6 @@ export default class GameWrapper { frames: {}, }); Pokemon.prototype.enableMask = () => null; - localStorage.clear(); } setScene(scene: BattleScene) { diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 2915f2e614b..49e67e8448c 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -6,10 +6,12 @@ import { LoginPhase, MessagePhase, MoveEffectPhase, MoveEndPhase, MovePhase, NewBattlePhase, NextEncounterPhase, PostSummonPhase, SelectGenderPhase, SelectModifierPhase, - SelectStarterPhase, ShinySparklePhase, ShowAbilityPhase, StatChangePhase, SummonPhase, - TitlePhase, ToggleDoublePositionPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase, VictoryPhase + SelectStarterPhase, SelectTargetPhase, ShinySparklePhase, ShowAbilityPhase, StatChangePhase, SummonPhase, + TitlePhase, ToggleDoublePositionPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase, UnavailablePhase, VictoryPhase } from "#app/phases"; -import {Mode} from "#app/ui/ui"; +import UI, {Mode} from "#app/ui/ui"; +import {Phase} from "#app/phase"; +import ErrorInterceptor from "#app/test/utils/errorInterceptor"; export default class PhaseInterceptor { public scene; @@ -21,6 +23,9 @@ export default class PhaseInterceptor { private intervalRun; private prompts; private phaseFrom; + private inProgress; + private originalSetMode; + private originalSuperEnd; /** * List of phases with their corresponding start methods. @@ -56,6 +61,12 @@ export default class PhaseInterceptor { [MoveEndPhase, this.startPhase], [StatChangePhase, this.startPhase], [ShinySparklePhase, this.startPhase], + [SelectTargetPhase, this.startPhase], + [UnavailablePhase, this.startPhase], + ]; + + private endBySetMode = [ + TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase ]; /** @@ -71,6 +82,15 @@ export default class PhaseInterceptor { this.startPromptHander(); } + rejectAll(error) { + if (this.inProgress) { + clearInterval(this.promptInterval); + clearInterval(this.interval); + clearInterval(this.intervalRun); + this.inProgress.onError(error); + } + } + /** * Method to set the starting phase. * @param phaseFrom - The phase to start from. @@ -86,20 +106,31 @@ export default class PhaseInterceptor { * @param phaseTo - The phase to transition to. * @returns A promise that resolves when the transition is complete. */ - async to(phaseTo): Promise { - return new Promise(async (resolve) => { - await this.run(this.phaseFrom); + async to(phaseTo, runTarget: boolean = true): Promise { + return new Promise(async (resolve, reject) => { + ErrorInterceptor.getInstance().add(this); + await this.run(this.phaseFrom).catch((e) => reject(e)); this.phaseFrom = null; const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name; - this.intervalRun = setInterval(async () => { + this.intervalRun = setInterval(async() => { const currentPhase = this.onHold?.length && this.onHold[0]; - if (currentPhase && currentPhase.name !== targetName) { - await this.run(currentPhase.name); - } else if (currentPhase.name === targetName) { - await this.run(currentPhase.name); + if (currentPhase && currentPhase.name === targetName) { clearInterval(this.intervalRun); + if (!runTarget) { + return resolve(); + } + await this.run(currentPhase).catch((e) => { + clearInterval(this.intervalRun); + return reject(e); + }); return resolve(); } + if (currentPhase && currentPhase.name !== targetName) { + await this.run(currentPhase).catch((e) => { + clearInterval(this.intervalRun); + return reject(e); + }); + } }); }); } @@ -111,92 +142,53 @@ export default class PhaseInterceptor { * @returns A promise that resolves when the phase is run. */ run(phaseTarget, skipFn?): Promise { - this.scene.moveAnimations = null; // Mandatory to avoid crash - return new Promise(async (resolve) => { - this.waitUntil(phaseTarget, skipFn).then(() => { - const currentPhase = this.onHold.shift(); - currentPhase.call(); - resolve(); - }).catch(() => { - resolve(); - }); - }); - } - - /** - * Method to ensure a phase is run, to throw error on test if not. - * @param phaseTarget - The phase to run. - * @returns A promise that resolves when the phase is run. - */ - mustRun(phaseTarget): Promise { const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; this.scene.moveAnimations = null; // Mandatory to avoid crash return new Promise(async (resolve, reject) => { + ErrorInterceptor.getInstance().add(this); const interval = setInterval(async () => { - const currentPhase = this.onHold?.length && this.onHold[0]; - if (currentPhase && currentPhase.name !== targetName) { - reject(currentPhase); - } else if (currentPhase && currentPhase.name === targetName) { + const currentPhase = this.onHold.shift(); + if (currentPhase) { + if (currentPhase.name !== targetName) { + clearInterval(interval); + const skip = skipFn && skipFn(currentPhase.name); + if (skip) { + this.onHold.unshift(currentPhase); + ErrorInterceptor.getInstance().remove(this); + return resolve(); + } + clearInterval(interval); + return reject(`Wrong phase: this is ${currentPhase.name} and not ${targetName}`); + } clearInterval(interval); - await this.run(phaseTarget); - resolve(); + this.inProgress = { + name: currentPhase.name, + callback: () => { + ErrorInterceptor.getInstance().remove(this); + resolve(); + }, + onError: (error) => reject(error), + }; + currentPhase.call(); } }); }); } - /** - * Method to execute actions when about to run a phase. Does not run the phase, stop right before. - * @param phaseTarget - The phase to run. - * @param skipFn - Optional skip function. - * @returns A promise that resolves when the phase is about to run. - */ whenAboutToRun(phaseTarget, skipFn?): Promise { - return new Promise(async (resolve) => { - this.waitUntil(phaseTarget, skipFn).then(() => { - resolve(); - }).catch(() => { - resolve(); - }); - }); - } - - /** - * Method to remove a phase from the list. - * @param phaseTarget - The phase to remove. - * @param skipFn - Optional skip function. - * @returns A promise that resolves when the phase is removed. - */ - remove(phaseTarget, skipFn?): Promise { - return new Promise(async (resolve) => { - this.waitUntil(phaseTarget, skipFn).then(() => { - this.onHold.shift(); - this.scene.getCurrentPhase().end(); - resolve(); - }).catch(() => { - resolve(); - }); - }); - } - - /** - * Method to wait until a specific phase is reached. - * @param phaseTarget - The phase to wait for. - * @param skipFn - Optional skip function. - * @returns A promise that resolves when the phase is reached. - */ - waitUntil(phaseTarget, skipFn?): Promise { const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - return new Promise((resolve, reject) => { - this.interval = setInterval(() => { - const currentPhase = this.onHold?.length && this.onHold[0] && this.onHold[0].name; - // if the currentPhase here is not filled, it means it's a phase we haven't added to the list - if (currentPhase === targetName) { - clearInterval(this.interval); - return resolve(); - } else if (skipFn && skipFn()) { - clearInterval(this.interval); - return reject("Skipped phase"); + this.scene.moveAnimations = null; // Mandatory to avoid crash + return new Promise(async (resolve, reject) => { + ErrorInterceptor.getInstance().add(this); + const interval = setInterval(async () => { + const currentPhase = this.onHold.shift(); + if (currentPhase) { + if (currentPhase.name !== targetName) { + this.onHold.unshift(currentPhase); + } else { + clearInterval(interval); + resolve(); + } } }); }); @@ -206,10 +198,17 @@ export default class PhaseInterceptor { * Method to initialize phases and their corresponding methods. */ initPhases() { - for (const [phase, method] of this.PHASES) { + 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, methodStart] of this.PHASES) { const originalStart = phase.prototype.start; - this.phases[phase.name] = originalStart; - phase.prototype.start = () => method.call(this, phase); + this.phases[phase.name] = { + start: originalStart, + endBySetMode: this.endBySetMode.some((elm) => elm.name === phase.name), + }; + phase.prototype.start = () => methodStart.call(this, phase); } } @@ -223,11 +222,44 @@ export default class PhaseInterceptor { this.onHold.push({ name: phase.name, call: () => { - this.phases[phase.name].apply(instance); + this.phases[phase.name].start.apply(instance); } }); } + unlock() { + this.inProgress?.callback(); + this.inProgress = undefined; + } + + /** + * Method to end a phase and log it. + * @param phase - The phase to start. + */ + superEndPhase() { + const instance = this.scene.getCurrentPhase(); + console.log(`%c INTERCEPTED Super End Phase ${instance.constructor.name}`, "color:red;"); + this.originalSuperEnd.apply(instance); + this.inProgress?.callback(); + this.inProgress = undefined; + } + + /** + * m2m to set mode. + * @param phase - The phase to start. + */ + setMode(mode: Mode, ...args: any[]): Promise { + const currentPhase = this.scene.getCurrentPhase(); + const instance = this.scene.ui; + console.log("setMode", mode, args); + const ret = this.originalSetMode.apply(instance, [mode, ...args]); + if (this.phases[currentPhase.constructor.name].endBySetMode) { + this.inProgress?.callback(); + this.inProgress = undefined; + } + return ret; + } + /** * Method to start the prompt handler. */ @@ -271,9 +303,12 @@ export default class PhaseInterceptor { */ restoreOg() { for (const [phase] of this.PHASES) { - phase.prototype.start = this.phases[phase.name]; + phase.prototype.start = this.phases[phase.name].start; } + UI.prototype.setMode = this.originalSetMode; + Phase.prototype.end = this.originalSuperEnd; clearInterval(this.promptInterval); clearInterval(this.interval); + clearInterval(this.intervalRun); } } diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index 7c1bbcbeabe..8dac707255a 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -21,3 +21,5 @@ initPokemonForms(); initSpecies(); initMoves(); initAbilities(); + +global.testFailed = false;