[Test] Address flaky tests and add Metronome mock helper (#6093)

- Fix `copycat`, `first-attack-double-power` and `ability-ignore-moves`

* Add utility method for forcing metronome move

* Stop bolt beak / fiscious rend flakiness
This commit is contained in:
Sirz Benjie 2025-07-13 23:20:09 -06:00 committed by GitHub
parent 22e399be8b
commit de8491505b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 14 deletions

View File

@ -2,10 +2,9 @@ import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { RandomMoveAttr } from "#moves/move";
import { GameManager } from "#test/testUtils/gameManager";
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 - Ability-Ignoring Moves", () => {
let phaserGame: Phaser.Game;
@ -55,10 +54,9 @@ describe("Moves - Ability-Ignoring Moves", () => {
expect(enemy.isFainted()).toBe(true);
});
// TODO: figure out why this test sometimes fails (cross-test game state pollution?)
it.todo("should not ignore enemy abilities when called by Metronome", async () => {
it("should not ignore enemy abilities when called by Metronome", async () => {
await game.classicMode.startBattle([SpeciesId.MILOTIC]);
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.PHOTON_GEYSER);
game.move.forceMetronomeMove(MoveId.PHOTON_GEYSER, true);
const enemy = game.field.getEnemyPokemon();
game.move.select(MoveId.METRONOME);

View File

@ -5,10 +5,9 @@ import { MoveResult } from "#enums/move-result";
import { MoveUseMode } from "#enums/move-use-mode";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import { RandomMoveAttr } from "#moves/move";
import { GameManager } from "#test/testUtils/gameManager";
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 - Copycat", () => {
let phaserGame: Phaser.Game;
@ -65,7 +64,7 @@ describe("Moves - Copycat", () => {
it("should copy the called move when the last move successfully calls another", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.METRONOME]).enemyMoveset(MoveId.COPYCAT);
await game.classicMode.startBattle([SpeciesId.DRAMPA]);
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.SWORDS_DANCE);
game.move.forceMetronomeMove(MoveId.SWORDS_DANCE, true);
game.move.select(MoveId.METRONOME);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first so enemy can copy Swords Dance

View File

@ -7,12 +7,11 @@ import { MoveUseMode } from "#enums/move-use-mode";
import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Fishious Rend & Bolt Beak", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let powerSpy: MockInstance;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -35,15 +34,13 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
.enemySpecies(SpeciesId.DRACOVISH)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH);
powerSpy = vi.spyOn(allMoves[MoveId.BOLT_BEAK], "calculateBattlePower");
});
it.each<{ name: string; move: MoveId }>([
{ name: "Bolt Beak", move: MoveId.BOLT_BEAK },
{ name: "Fishious Rend", move: MoveId.FISHIOUS_REND },
])("$name should double power if the user moves before the target", async ({ move }) => {
powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
// turn 1: enemy, then player (no boost)
@ -63,6 +60,7 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
it("should only consider the selected target in Double Battles", async () => {
game.override.battleStyle("double");
const powerSpy = vi.spyOn(allMoves[MoveId.BOLT_BEAK], "calculateBattlePower");
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
// Use move after everyone but P1 and enemy 1 have already moved
@ -76,6 +74,7 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
it("should double power on the turn the target switches in", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const powerSpy = vi.spyOn(allMoves[MoveId.BOLT_BEAK], "calculateBattlePower");
game.move.use(MoveId.BOLT_BEAK);
game.forceEnemyToSwitch();
@ -86,6 +85,7 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
it("should double power on forced switch-induced sendouts", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const powerSpy = vi.spyOn(allMoves[MoveId.BOLT_BEAK], "calculateBattlePower");
game.move.use(MoveId.BOLT_BEAK);
await game.move.forceEnemyMove(MoveId.U_TURN);
@ -100,7 +100,7 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
{ type: "an Instructed", allyMove: MoveId.INSTRUCT },
])("should double power if $type move is used as the target's first action that turn", async ({ allyMove }) => {
game.override.battleStyle("double").enemyAbility(AbilityId.DANCER);
powerSpy = vi.spyOn(allMoves[MoveId.FISHIOUS_REND], "calculateBattlePower");
const powerSpy = vi.spyOn(allMoves[MoveId.FISHIOUS_REND], "calculateBattlePower");
await game.classicMode.startBattle([SpeciesId.DRACOVISH, SpeciesId.ARCTOZOLT]);
// Simulate enemy having used splash last turn to allow Instruct to copy it

View File

@ -1,4 +1,5 @@
import Overrides from "#app/overrides";
import { allMoves } from "#data/data-lists";
import { BattlerIndex } from "#enums/battler-index";
import { Command } from "#enums/command";
import { MoveId } from "#enums/move-id";
@ -12,6 +13,7 @@ import type { EnemyCommandPhase } from "#phases/enemy-command-phase";
import { MoveEffectPhase } from "#phases/move-effect-phase";
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
import { coerceArray, toReadableString } from "#utils/common";
import type { MockInstance } from "vitest";
import { expect, vi } from "vitest";
/**
@ -305,4 +307,20 @@ export class MoveHelper extends GameManagerHelper {
*/
await this.game.phaseInterceptor.to("EnemyCommandPhase");
}
/**
* Force the move used by Metronome to be a specific move.
* @param move - The move to force metronome to use
* @param once - If `true`, uses {@linkcode MockInstance#mockReturnValueOnce} when mocking, else uses {@linkcode MockInstance#mockReturnValue}.
* @returns The spy that for Metronome that was mocked (Usually unneeded).
*/
public forceMetronomeMove(move: MoveId, once = false): MockInstance {
const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride");
if (once) {
spy.mockReturnValueOnce(move);
} else {
spy.mockReturnValue(move);
}
return spy;
}
}