From 49825a672993fce2751dca22b82f303a5f31e709 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 23:52:07 -0400 Subject: [PATCH 01/14] [Test] Added `toHaveArenaTagMatcher` + fixed prior matchers --- test/@types/test-helpers.ts | 27 +++++++ test/@types/vitest.d.ts | 30 ++++++- test/matchers.setup.ts | 2 + test/test-utils/game-manager-utils.ts | 3 +- test/test-utils/matchers/to-have-arena-tag.ts | 80 +++++++++++++++++++ .../matchers/to-have-status-effect.ts | 6 +- 6 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 test/@types/test-helpers.ts create mode 100644 test/test-utils/matchers/to-have-arena-tag.ts diff --git a/test/@types/test-helpers.ts b/test/@types/test-helpers.ts new file mode 100644 index 00000000000..4db85edcb5d --- /dev/null +++ b/test/@types/test-helpers.ts @@ -0,0 +1,27 @@ +import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers"; + +/** + * Helper type to admit an object containing the given properties + * _and at least 1 other non-function property_. + * @example + * ```ts + * type foo = { + * qux: 1 | 2 | 3, + * bar: number, + * baz: string + * quux: () => void; // ignored! + * } + * + * type quxAndSomethingElse = OneOther + * + * const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK! + * const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK! + * const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required + * const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required + * ``` + * @typeParam O - The object to source keys from + * @typeParam K - One or more of O's keys to render mandatory + */ +export type OneOther = AtLeastOne, K>> & { + [key in K]: O[K]; +}; diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 7b756c45a57..5ddc8771df5 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -3,18 +3,25 @@ import type { AbilityId } from "#enums/ability-id"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { expect } from "vitest"; +import type { Arena } from "#field/arena"; +import type { toDmgValue } from "utils/common"; +import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; +import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; +import { ArenaTagType } from "#enums/arena-tag-type"; import type { BattleStat, EffectiveStat, Stat } from "#enums/stat"; import type { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; -import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; -import type { expect } from "vitest"; +import "vitest"; import type Overrides from "#app/overrides"; import type { PokemonMove } from "#moves/pokemon-move"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { OneOther } from "#test/@types/test-helpers"; declare module "vitest" { interface Assertion { @@ -35,6 +42,7 @@ declare module "vitest" { * @param expected - The expected types (in any order) * @param options - The options passed to the matcher */ + toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; /** @@ -79,6 +87,24 @@ declare module "vitest" { */ toHaveTerrain(expectedTerrainType: TerrainType): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * + * @param expectedType - A partially-filled {@linkcode ArenaTag} containing the desired properties + */ + toHaveArenaTag( + expectedType: OneOther & { tagType: T }, // intersection required bc this doesn't preserve T + ): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides; + * default `ArenaTagSide.BOTH` + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + /** * Check whether a {@linkcode Pokemon} is at full HP. */ diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index 03b29302916..447911c1d87 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -1,5 +1,6 @@ import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; +import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat"; import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; @@ -28,6 +29,7 @@ expect.extend({ toHaveTakenDamage, toHaveWeather, toHaveTerrain, + toHaveArenaTag, toHaveFullHp, toHaveStatusEffect, toHaveStatStage, diff --git a/test/test-utils/game-manager-utils.ts b/test/test-utils/game-manager-utils.ts index 89e352cdbdc..7bb8ea57469 100644 --- a/test/test-utils/game-manager-utils.ts +++ b/test/test-utils/game-manager-utils.ts @@ -3,6 +3,7 @@ import type { BattleScene } from "#app/battle-scene"; import { getGameMode } from "#app/game-mode"; import { getDailyRunStarters } from "#data/daily-run"; import { Gender } from "#data/gender"; +import { getPokemonSpeciesForm } from "#data/pokemon-species"; import { BattleType } from "#enums/battle-type"; import { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; @@ -10,7 +11,7 @@ import type { SpeciesId } from "#enums/species-id"; import { PlayerPokemon } from "#field/pokemon"; import type { StarterMoveset } from "#system/game-data"; import type { Starter } from "#ui/starter-select-ui-handler"; -import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; /** Function to convert Blob to string */ export function blobToString(blob) { diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts new file mode 100644 index 00000000000..fd1a696c4df --- /dev/null +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -0,0 +1,80 @@ +import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; +import type { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import type { OneOther } from "#test/@types/test-helpers"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { getEnumStr, getOnelineDiffStr, stringifyEnumArray } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import type { NonFunctionPropertiesRecursive } from "#types/type-helpers"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +export type toHaveArenaTagOptions = OneOther; + +/** + * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag, or a partially-filled object + * containing the desired properties + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides + * @returns The result of the matching + */ +export function toHaveArenaTag( + this: MatcherState, + received: unknown, + // simplified types used for brevity; full overloads are in `vitest.d.ts` + expectedType: T | (Partial> & { tagType: T; side: ArenaTagSide }), + side?: ArenaTagSide, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena) { + return { + pass: false, + message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + }; + } + + if (typeof expectedType === "string") { + // Coerce lone `tagType`s into objects + // Bangs are ok as we enforce safety via overloads + expectedType = { tagType: expectedType, side: side! }; + } + + // We need to get all tags for the case of checking properties of a tag present on both sides of the arena + const tags = received.scene.arena.findTagsOnSide(t => t.tagType === expectedType.tagType, expectedType.side); + if (!tags.length) { + const expectedStr = getEnumStr(ArenaTagType, expectedType.tagType); + return { + pass: false, + message: () => `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: getEnumStr(ArenaTagType, expectedType.tagType), + actual: stringifyEnumArray( + ArenaTagType, + received.scene.arena.tags.map(t => t.tagType), + ), + }; + } + + // Pass if any of the matching tags meet our criteria + const pass = tags.some(tag => + this.equals(tag, expectedType, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), + ); + + const expectedStr = getOnelineDiffStr.call(this, expectedType); + return { + pass, + message: () => + pass + ? `Expected the arena to NOT have a tag matching ${expectedStr}, but it did!` + : `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: expectedType, + actual: tags, + }; +} diff --git a/test/test-utils/matchers/to-have-status-effect.ts b/test/test-utils/matchers/to-have-status-effect.ts index a46800632f3..9bcc0b5097f 100644 --- a/test/test-utils/matchers/to-have-status-effect.ts +++ b/test/test-utils/matchers/to-have-status-effect.ts @@ -37,10 +37,8 @@ export function toHaveStatusEffect( const actualEffect = received.status?.effect ?? StatusEffect.NONE; // Check exclusively effect equality first, coercing non-matching status effects to numbers. - if (actualEffect !== (expectedStatus as Exclude)?.effect) { - // This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed, - // which will never match actualEffect by definition - expectedStatus = (expectedStatus as Exclude).effect; + if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) { + expectedStatus = expectedStatus.effect; } if (typeof expectedStatus === "number") { From f5154179b347eedc34cecc5c13c5405ad8195e1a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 11:12:19 -0400 Subject: [PATCH 02/14] Fixed imports and stufff --- test/@types/vitest.d.ts | 16 +- test/moves/entry-hazards.test.ts | 234 ++++++++++++++++++++++++++ test/test-utils/game-manager-utils.ts | 3 +- 3 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 test/moves/entry-hazards.test.ts diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 5ddc8771df5..409a874ba84 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,27 +1,27 @@ import type { TerrainType } from "#app/data/terrain"; +import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; import type { AbilityId } from "#enums/ability-id"; +import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; -import type { expect } from "vitest"; -import type { Arena } from "#field/arena"; -import type { toDmgValue } from "utils/common"; -import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; -import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; -import { ArenaTagType } from "#enums/arena-tag-type"; import type { BattleStat, EffectiveStat, Stat } from "#enums/stat"; import type { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; +import type { Arena } from "#field/arena"; import type { Pokemon } from "#field/pokemon"; import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; +import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; +import type { toDmgValue } from "utils/common"; +import type { expect } from "vitest"; import "vitest"; import type Overrides from "#app/overrides"; +import type { ArenaTagSide } from "#enums/arena-tag-side"; import type { PokemonMove } from "#moves/pokemon-move"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { OneOther } from "#test/@types/test-helpers"; +import type { OneOther } from "#test/@types/test-helpers"; declare module "vitest" { interface Assertion { diff --git a/test/moves/entry-hazards.test.ts b/test/moves/entry-hazards.test.ts new file mode 100644 index 00000000000..d546e6176d1 --- /dev/null +++ b/test/moves/entry-hazards.test.ts @@ -0,0 +1,234 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import { ArenaTrapTag } from "#data/arena-tag"; +import { allMoves } from "#data/data-lists"; +import type { TypeDamageMultiplier } from "#data/type"; +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattleType } from "#enums/battle-type"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import type { ArenaTrapTagType } from "#types/arena-tags"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Entry Hazards", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.BLISSEY) + .startingLevel(100) + .enemyLevel(100) + .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .battleType(BattleType.TRAINER); + }); + + describe.each<{ name: string; move: MoveId; tagType: ArenaTrapTagType }>([ + { name: "Spikes", move: MoveId.SPIKES, tagType: ArenaTagType.SPIKES }, + { + name: "Toxic Spikes", + move: MoveId.TOXIC_SPIKES, + tagType: ArenaTagType.TOXIC_SPIKES, + }, + { + name: "Stealth Rock", + move: MoveId.STEALTH_ROCK, + tagType: ArenaTagType.STEALTH_ROCK, + }, + { + name: "Sticky Web", + move: MoveId.STICKY_WEB, + tagType: ArenaTagType.STICKY_WEB, + }, + ])("General checks - $name", ({ move, tagType }) => { + it("should add a persistent tag to the opposing side of the field", async () => { + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + expect(game).not.toHaveArenaTag(tagType); + + game.move.use(move); + await game.toNextTurn(); + + // Tag should've been added to the opposing side of the field + expect(game).not.toHaveArenaTag(tagType, ArenaTagSide.PLAYER); + expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY); + }); + + // TODO: re-enable after re-fixing hazards moves + it.todo("should work when all targets fainted", async () => { + game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000); + await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]); + + const [enemy1, enemy2] = game.scene.getEnemyField(); + + game.move.use(MoveId.HYPER_VOICE, BattlerIndex.PLAYER); + game.move.use(MoveId.SPIKES, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + expect(enemy1.isFainted()).toBe(true); + expect(enemy2.isFainted()).toBe(true); + expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined(); + }); + + const maxLayers = tagType === ArenaTagType.SPIKES ? 3 : tagType === ArenaTagType.TOXIC_SPIKES ? 2 : 1; + const msgText = + maxLayers === 1 + ? "should fail if added while already present" + : `can be added up to ${maxLayers} times in a row before failing`; + + it(msgText, async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + + // set up hazards until at max layers + for (let i = 0; i < maxLayers; i++) { + game.move.use(move); + await game.toNextTurn(); + + expect(feebas).toHaveUsedMove({ move, result: MoveResult.SUCCESS }); + expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: i + 1 }); + } + + game.move.use(move); + await game.toNextTurn(); + + expect(feebas).toHaveUsedMove({ move, result: MoveResult.FAIL }); + expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: maxLayers }); + }); + }); + + describe("Spikes", () => { + it.each<{ layers: number; damage: number }>([ + { layers: 1, damage: 12.5 }, + { layers: 2, damage: 100 / 6 }, + { layers: 3, damage: 25 }, + ])("should play message and deal $damage% of the target's max HP at $layers", async ({ layers, damage }) => { + for (let i = 0; i < layers; i++) { + game.scene.arena.addTag(ArenaTagType.SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + } + + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveTakenDamage((enemy.getMaxHp() * damage) / 100); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:spikesActivateTrap", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemy), + }), + ); + }); + }); + + describe("Toxic Spikes", () => { + it.each<{ name: string; layers: number; status: StatusEffect }>([ + { name: "Poison", layers: 1, status: StatusEffect.POISON }, + { name: "Toxic", layers: 2, status: StatusEffect.TOXIC }, + ])("should apply $name at $layers without displaying neutralization msg", async ({ layers, status }) => { + for (let i = 0; i < layers; i++) { + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + } + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveStatusEffect(status); + // shoudl + expect(game.textInterceptor.logs).not.toContain( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemy), + moveName: allMoves[MoveId.TOXIC_SPIKES].name, + }), + ); + }); + }); + + it("should be removed without triggering upon a grounded Poison-type switching in", async () => { + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + const ekans = game.field.getPlayerPokemon(); + expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER); + expect(game.textInterceptor.logs).not.toContain( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(ekans), + moveName: allMoves[MoveId.TOXIC_SPIKES].name, + }), + ); + expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON); + }); + + describe("Stealth Rock", () => { + it.each<{ multi: TypeDamageMultiplier; species: SpeciesId }>([ + { multi: 0.25, species: SpeciesId.LUCARIO }, + { multi: 0.5, species: SpeciesId.DURALUDON }, + { multi: 1, species: SpeciesId.LICKILICKY }, + { multi: 2, species: SpeciesId.DARMANITAN }, + { multi: 4, species: SpeciesId.ARTICUNO }, + ])("should deal damage based on the target's weakness to Rock - $multi", async ({ multi, species }) => { + game.override.enemySpecies(species); + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true)).toBe(multi); + expect(enemy).toHaveTakenDamage(enemy.getMaxHp() * 0.125 * multi); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:stealthRockActivateTrap", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemy), + }), + ); + }); + + it("should ignore strong winds for type effectiveness", async () => { + game.override.enemyAbility(AbilityId.DELTA_STREAM).enemySpecies(SpeciesId.RAYQUAZA); + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const rayquaza = game.field.getEnemyPokemon(); + // took 25% damage despite strong winds halving effectiveness + expect(rayquaza).toHaveTakenDamage(rayquaza.getMaxHp() * 0.25); + }); + }); + + describe("Sticky Web", () => { + it("should lower the target's speed by 1 stage on entry", async () => { + game.scene.arena.addTag(ArenaTagType.STICKY_WEB, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveStatStage(Stat.SPD, -1); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:stickyWebActivateTrap", { + pokemonName: enemy.getNameToRender(), + }), + ); + }); + }); +}); diff --git a/test/test-utils/game-manager-utils.ts b/test/test-utils/game-manager-utils.ts index 7bb8ea57469..89e352cdbdc 100644 --- a/test/test-utils/game-manager-utils.ts +++ b/test/test-utils/game-manager-utils.ts @@ -3,7 +3,6 @@ import type { BattleScene } from "#app/battle-scene"; import { getGameMode } from "#app/game-mode"; import { getDailyRunStarters } from "#data/daily-run"; import { Gender } from "#data/gender"; -import { getPokemonSpeciesForm } from "#data/pokemon-species"; import { BattleType } from "#enums/battle-type"; import { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; @@ -11,7 +10,7 @@ import type { SpeciesId } from "#enums/species-id"; import { PlayerPokemon } from "#field/pokemon"; import type { StarterMoveset } from "#system/game-data"; import type { Starter } from "#ui/starter-select-ui-handler"; -import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** Function to convert Blob to string */ export function blobToString(blob) { From 641f5f5b971c5a018ff9de5bd95ccfcbe804362f Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 11:16:01 -0400 Subject: [PATCH 03/14] Removed accidental test file addition --- test/moves/entry-hazards.test.ts | 234 ------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 test/moves/entry-hazards.test.ts diff --git a/test/moves/entry-hazards.test.ts b/test/moves/entry-hazards.test.ts deleted file mode 100644 index d546e6176d1..00000000000 --- a/test/moves/entry-hazards.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { getPokemonNameWithAffix } from "#app/messages"; -import { ArenaTrapTag } from "#data/arena-tag"; -import { allMoves } from "#data/data-lists"; -import type { TypeDamageMultiplier } from "#data/type"; -import { AbilityId } from "#enums/ability-id"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattleType } from "#enums/battle-type"; -import { BattlerIndex } from "#enums/battler-index"; -import { MoveId } from "#enums/move-id"; -import { MoveResult } from "#enums/move-result"; -import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; -import { Stat } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import type { ArenaTrapTagType } from "#types/arena-tags"; -import i18next from "i18next"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Entry Hazards", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.BLISSEY) - .startingLevel(100) - .enemyLevel(100) - .enemyAbility(AbilityId.BALL_FETCH) - .ability(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .battleType(BattleType.TRAINER); - }); - - describe.each<{ name: string; move: MoveId; tagType: ArenaTrapTagType }>([ - { name: "Spikes", move: MoveId.SPIKES, tagType: ArenaTagType.SPIKES }, - { - name: "Toxic Spikes", - move: MoveId.TOXIC_SPIKES, - tagType: ArenaTagType.TOXIC_SPIKES, - }, - { - name: "Stealth Rock", - move: MoveId.STEALTH_ROCK, - tagType: ArenaTagType.STEALTH_ROCK, - }, - { - name: "Sticky Web", - move: MoveId.STICKY_WEB, - tagType: ArenaTagType.STICKY_WEB, - }, - ])("General checks - $name", ({ move, tagType }) => { - it("should add a persistent tag to the opposing side of the field", async () => { - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - expect(game).not.toHaveArenaTag(tagType); - - game.move.use(move); - await game.toNextTurn(); - - // Tag should've been added to the opposing side of the field - expect(game).not.toHaveArenaTag(tagType, ArenaTagSide.PLAYER); - expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY); - }); - - // TODO: re-enable after re-fixing hazards moves - it.todo("should work when all targets fainted", async () => { - game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000); - await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]); - - const [enemy1, enemy2] = game.scene.getEnemyField(); - - game.move.use(MoveId.HYPER_VOICE, BattlerIndex.PLAYER); - game.move.use(MoveId.SPIKES, BattlerIndex.PLAYER_2); - await game.toEndOfTurn(); - - expect(enemy1.isFainted()).toBe(true); - expect(enemy2.isFainted()).toBe(true); - expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined(); - }); - - const maxLayers = tagType === ArenaTagType.SPIKES ? 3 : tagType === ArenaTagType.TOXIC_SPIKES ? 2 : 1; - const msgText = - maxLayers === 1 - ? "should fail if added while already present" - : `can be added up to ${maxLayers} times in a row before failing`; - - it(msgText, async () => { - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - - const feebas = game.field.getPlayerPokemon(); - - // set up hazards until at max layers - for (let i = 0; i < maxLayers; i++) { - game.move.use(move); - await game.toNextTurn(); - - expect(feebas).toHaveUsedMove({ move, result: MoveResult.SUCCESS }); - expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: i + 1 }); - } - - game.move.use(move); - await game.toNextTurn(); - - expect(feebas).toHaveUsedMove({ move, result: MoveResult.FAIL }); - expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: maxLayers }); - }); - }); - - describe("Spikes", () => { - it.each<{ layers: number; damage: number }>([ - { layers: 1, damage: 12.5 }, - { layers: 2, damage: 100 / 6 }, - { layers: 3, damage: 25 }, - ])("should play message and deal $damage% of the target's max HP at $layers", async ({ layers, damage }) => { - for (let i = 0; i < layers; i++) { - game.scene.arena.addTag(ArenaTagType.SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); - } - - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - const enemy = game.field.getEnemyPokemon(); - expect(enemy).toHaveTakenDamage((enemy.getMaxHp() * damage) / 100); - expect(game.textInterceptor.logs).toContain( - i18next.t("arenaTag:spikesActivateTrap", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemy), - }), - ); - }); - }); - - describe("Toxic Spikes", () => { - it.each<{ name: string; layers: number; status: StatusEffect }>([ - { name: "Poison", layers: 1, status: StatusEffect.POISON }, - { name: "Toxic", layers: 2, status: StatusEffect.TOXIC }, - ])("should apply $name at $layers without displaying neutralization msg", async ({ layers, status }) => { - for (let i = 0; i < layers; i++) { - game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); - } - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - const enemy = game.field.getEnemyPokemon(); - expect(enemy).toHaveStatusEffect(status); - // shoudl - expect(game.textInterceptor.logs).not.toContain( - i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemy), - moveName: allMoves[MoveId.TOXIC_SPIKES].name, - }), - ); - }); - }); - - it("should be removed without triggering upon a grounded Poison-type switching in", async () => { - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); - - game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); - - game.doSwitchPokemon(1); - await game.toNextTurn(); - - const ekans = game.field.getPlayerPokemon(); - expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER); - expect(game.textInterceptor.logs).not.toContain( - i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { - pokemonNameWithAffix: getPokemonNameWithAffix(ekans), - moveName: allMoves[MoveId.TOXIC_SPIKES].name, - }), - ); - expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON); - }); - - describe("Stealth Rock", () => { - it.each<{ multi: TypeDamageMultiplier; species: SpeciesId }>([ - { multi: 0.25, species: SpeciesId.LUCARIO }, - { multi: 0.5, species: SpeciesId.DURALUDON }, - { multi: 1, species: SpeciesId.LICKILICKY }, - { multi: 2, species: SpeciesId.DARMANITAN }, - { multi: 4, species: SpeciesId.ARTICUNO }, - ])("should deal damage based on the target's weakness to Rock - $multi", async ({ multi, species }) => { - game.override.enemySpecies(species); - game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); - - const enemy = game.field.getEnemyPokemon(); - expect(enemy.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true)).toBe(multi); - expect(enemy).toHaveTakenDamage(enemy.getMaxHp() * 0.125 * multi); - expect(game.textInterceptor.logs).toContain( - i18next.t("arenaTag:stealthRockActivateTrap", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemy), - }), - ); - }); - - it("should ignore strong winds for type effectiveness", async () => { - game.override.enemyAbility(AbilityId.DELTA_STREAM).enemySpecies(SpeciesId.RAYQUAZA); - game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); - - const rayquaza = game.field.getEnemyPokemon(); - // took 25% damage despite strong winds halving effectiveness - expect(rayquaza).toHaveTakenDamage(rayquaza.getMaxHp() * 0.25); - }); - }); - - describe("Sticky Web", () => { - it("should lower the target's speed by 1 stage on entry", async () => { - game.scene.arena.addTag(ArenaTagType.STICKY_WEB, 0, undefined, 0, ArenaTagSide.ENEMY); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); - - const enemy = game.field.getEnemyPokemon(); - expect(enemy).toHaveStatStage(Stat.SPD, -1); - expect(game.textInterceptor.logs).toContain( - i18next.t("arenaTag:stickyWebActivateTrap", { - pokemonName: enemy.getNameToRender(), - }), - ); - }); - }); -}); From dbea701d6d4705bc96b97e00c44b380c78ea6d0b Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:27:26 -0400 Subject: [PATCH 04/14] Update test/test-utils/matchers/to-have-arena-tag.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/matchers/to-have-arena-tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts index fd1a696c4df..425eb046a4e 100644 --- a/test/test-utils/matchers/to-have-arena-tag.ts +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -37,7 +37,7 @@ export function toHaveArenaTag( if (!received.scene?.arena) { return { pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + message: () => `Expected GameManager.${!received.scene ? "scene" : "scene.arena"} to be defined!`, }; } From 9455030fbea1241403a041cb214f9e39bad39047 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 15:48:08 -0400 Subject: [PATCH 05/14] More improvements and minor fixes --- test/@types/vitest.d.ts | 18 ++++++---- .../matchers/to-equal-array-unsorted.ts | 13 +++---- .../matchers/to-have-effective-stat.ts | 6 ++-- test/test-utils/matchers/to-have-terrain.ts | 4 +-- test/test-utils/matchers/to-have-types.ts | 35 ++++++++++++------- test/test-utils/matchers/to-have-used-move.ts | 7 ++-- test/test-utils/matchers/to-have-used-pp.ts | 6 ++-- test/test-utils/matchers/to-have-weather.ts | 8 ++--- 8 files changed, 56 insertions(+), 41 deletions(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 409a874ba84..4af28bb6414 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -10,7 +10,7 @@ import type { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; import type { Arena } from "#field/arena"; import type { Pokemon } from "#field/pokemon"; -import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat"; +import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; import type { TurnMove } from "#types/turn-move"; @@ -40,10 +40,16 @@ declare module "vitest" { * Check whether a {@linkcode Pokemon}'s current typing includes the given types. * * @param expected - The expected types (in any order) - * @param options - The options passed to the matcher + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * + * @param expected - The expected types (in any order) + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher */ toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; - toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; /** * Matcher to check the contents of a {@linkcode Pokemon}'s move history. @@ -62,11 +68,11 @@ declare module "vitest" { * * @param stat - The {@linkcode EffectiveStat} to check * @param expectedValue - The expected value of {@linkcode stat} - * @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher * @remarks * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void; + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; /** * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. @@ -93,7 +99,7 @@ declare module "vitest" { * @param expectedType - A partially-filled {@linkcode ArenaTag} containing the desired properties */ toHaveArenaTag( - expectedType: OneOther & { tagType: T }, // intersection required bc this doesn't preserve T + expectedType: OneOther & { tagType: T }, // intersection required to preserve T ): void; /** * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-array-unsorted.ts index 846ea9e7779..97398689032 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-array-unsorted.ts @@ -1,4 +1,5 @@ import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** @@ -14,22 +15,22 @@ export function toEqualArrayUnsorted( ): SyncExpectationResult { if (!Array.isArray(received)) { return { - pass: false, - message: () => `Expected an array, but got ${this.utils.stringify(received)}!`, + pass: this.isNot, + message: () => `Expected to receive an array, but got ${receivedStr(received)}!`, }; } if (received.length !== expected.length) { return { pass: false, - message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`, - actual: received, + message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`, expected, + actual: received, }; } - const actualSorted = received.slice().sort(); - const expectedSorted = expected.slice().sort(); + const actualSorted = received.toSorted(); + const expectedSorted = expected.toSorted(); const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]); const actualStr = getOnelineDiffStr.call(this, actualSorted); diff --git a/test/test-utils/matchers/to-have-effective-stat.ts b/test/test-utils/matchers/to-have-effective-stat.ts index bc10a646c02..fba285e49e7 100644 --- a/test/test-utils/matchers/to-have-effective-stat.ts +++ b/test/test-utils/matchers/to-have-effective-stat.ts @@ -6,7 +6,7 @@ import { getStatName } from "#test/test-utils/string-utils"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; -export interface ToHaveEffectiveStatMatcherOptions { +export interface toHaveEffectiveStatOptions { /** * The target {@linkcode Pokemon} * @see {@linkcode Pokemon.getEffectiveStat} @@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions { * @param received - The object to check. Should be a {@linkcode Pokemon} * @param stat - The {@linkcode EffectiveStat} to check * @param expectedValue - The expected value of the {@linkcode stat} - * @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions} + * @param options - The {@linkcode toHaveEffectiveStatOptions} * @returns Whether the matcher passed */ export function toHaveEffectiveStat( @@ -38,7 +38,7 @@ export function toHaveEffectiveStat( received: unknown, stat: EffectiveStat, expectedValue: number, - { enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {}, + { enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {}, ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index 292c32abafc..29a56ceb23b 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -21,14 +21,14 @@ export function toHaveTerrain( if (!isGameManagerInstance(received)) { return { pass: false, - message: () => `Expected GameManager, but got ${receivedStr(received)}!`, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, }; } if (!received.scene?.arena) { return { pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index 3f16f740583..e7aab8e7d94 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils"; export interface toHaveTypesOptions { /** - * Whether to enforce exact matches (`true`) or superset matches (`false`). - * @defaultValue `true` + * Value dictating the strength of the enforced typing match. + * + * Possible values (in ascending order of strength) are: + * - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order** + * - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order** + * - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types + * (all must be present, but extras can be there) + * @defaultValue `"unordered"` */ - exact?: boolean; + mode?: "ordered" | "unordered" | "superset"; /** * Optional arguments to pass to {@linkcode Pokemon.getTypes}. */ @@ -18,31 +24,34 @@ export interface toHaveTypesOptions { } /** - * Matcher that checks if an array contains exactly the given items, disregarding order. - * @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s. - * @param options - The {@linkcode toHaveTypesOptions | options} for this matcher + * Matcher that checks if a {@linkcode Pokemon}'s typing is as expected. + * @param received - The object to check. Should be a {@linkcode Pokemon} + * @param expected - An array of one or more {@linkcode PokemonType}s to compare against. + * @param mode - The mode to perform the matching; * @returns The result of the matching */ export function toHaveTypes( this: MatcherState, received: unknown, expected: [PokemonType, ...PokemonType[]], - options: toHaveTypesOptions = {}, + { mode = "unordered", args = [] }: toHaveTypesOptions = {}, ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`, }; } - const actualTypes = received.getTypes(...(options.args ?? [])).sort(); - const expectedTypes = expected.slice().sort(); + // Avoid sorting the types if strict ordering is desired + const actualTypes = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted(); + const expectedTypes = mode === "ordered" ? expected : expected.toSorted(); // Exact matches do not care about subset equality - const matchers = options.exact - ? [...this.customTesters, this.utils.iterableEquality] - : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; + const matchers = + mode === "superset" + ? [...this.customTesters, this.utils.iterableEquality] + : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; const pass = this.equals(actualTypes, expectedTypes, matchers); const actualStr = stringifyEnumArray(PokemonType, actualTypes); diff --git a/test/test-utils/matchers/to-have-used-move.ts b/test/test-utils/matchers/to-have-used-move.ts index ef90e4dbad9..3a3008fdf50 100644 --- a/test/test-utils/matchers/to-have-used-move.ts +++ b/test/test-utils/matchers/to-have-used-move.ts @@ -27,7 +27,7 @@ export function toHaveUsedMove( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } @@ -37,7 +37,7 @@ export function toHaveUsedMove( if (move === undefined) { return { - pass: false, + pass: this.isNot, message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`, actual: received.getLastXMoves(-1), }; @@ -62,8 +62,7 @@ export function toHaveUsedMove( message: () => pass ? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!` - : // Replace newlines with spaces to preserve one-line ness - `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`, + : `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`, expected: expectedResult, actual: move, }; diff --git a/test/test-utils/matchers/to-have-used-pp.ts b/test/test-utils/matchers/to-have-used-pp.ts index 3b606a535bc..ce7f5b5ea23 100644 --- a/test/test-utils/matchers/to-have-used-pp.ts +++ b/test/test-utils/matchers/to-have-used-pp.ts @@ -28,7 +28,7 @@ export function toHaveUsedPP( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } @@ -36,7 +36,7 @@ export function toHaveUsedPP( const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; if (coerceArray(override).length > 0) { return { - pass: false, + pass: this.isNot, message: () => `Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`, }; @@ -48,7 +48,7 @@ export function toHaveUsedPP( const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove); if (movesetMoves.length !== 1) { return { - pass: false, + pass: this.isNot, message: () => `Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`, expected: expectedMove, diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index 49433b2137b..21a3a54c80d 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -20,15 +20,15 @@ export function toHaveWeather( ): SyncExpectationResult { if (!isGameManagerInstance(received)) { return { - pass: false, - message: () => `Expected GameManager, but got ${receivedStr(received)}!`, + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, }; } if (!received.scene?.arena) { return { - pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + pass: this.isNot, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } From df8d1dc8c7de2e50604505ea5a2eb861a3fccc16 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 16:13:10 -0400 Subject: [PATCH 06/14] More semantic changes --- test/@types/vitest.d.ts | 47 +++++++------------ .../matchers/to-have-ability-applied.ts | 2 +- test/test-utils/matchers/to-have-arena-tag.ts | 4 +- .../matchers/to-have-effective-stat.ts | 2 +- test/test-utils/matchers/to-have-fainted.ts | 2 +- test/test-utils/matchers/to-have-full-hp.ts | 2 +- test/test-utils/matchers/to-have-hp.ts | 2 +- .../test-utils/matchers/to-have-stat-stage.ts | 4 +- .../matchers/to-have-status-effect.ts | 2 +- .../matchers/to-have-taken-damage.ts | 2 +- test/test-utils/matchers/to-have-terrain.ts | 4 +- test/test-utils/matchers/to-have-types.ts | 26 ++++++---- test/test-utils/matchers/to-have-used-move.ts | 14 +++--- test/test-utils/matchers/to-have-used-pp.ts | 10 ++-- test/test-utils/string-utils.ts | 6 ++- 15 files changed, 64 insertions(+), 65 deletions(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 4af28bb6414..086675da78d 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -24,7 +24,7 @@ import type { PokemonMove } from "#moves/pokemon-move"; import type { OneOther } from "#test/@types/test-helpers"; declare module "vitest" { - interface Assertion { + interface Assertion { /** * Check whether an array contains EXACTLY the given items (in any order). * @@ -34,38 +34,28 @@ declare module "vitest" { * @param expected - The expected contents of the array, in any order * @see {@linkcode expect.arrayContaining} */ - toEqualArrayUnsorted(expected: E[]): void; + toEqualArrayUnsorted(expected: T[]): void; /** * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * - * @param expected - The expected types (in any order) + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher */ - toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * - * @param expected - The expected types (in any order) - * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher - */ - toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; /** - * Matcher to check the contents of a {@linkcode Pokemon}'s move history. - * - * @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove} - * containing the desired properties to check + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check * @param index - The index of the move history entry to check, in order from most recent to least recent. * Default `0` (last used move) * @see {@linkcode Pokemon.getLastXMoves} */ - toHaveUsedMove(expected: MoveId | AtLeastOne, index?: number): void; + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; /** * Check whether a {@linkcode Pokemon}'s effective stat is as expected * (checked after all stat value modifications). - * * @param stat - The {@linkcode EffectiveStat} to check * @param expectedValue - The expected value of {@linkcode stat} * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher @@ -95,15 +85,13 @@ declare module "vitest" { /** * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * - * @param expectedType - A partially-filled {@linkcode ArenaTag} containing the desired properties + * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties */ toHaveArenaTag( - expectedType: OneOther & { tagType: T }, // intersection required to preserve T + expectedTag: OneOther & { tagType: T }, // intersection required to preserve T ): void; /** * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * * @param expectedType - The {@linkcode ArenaTagType} of the desired tag * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or * {@linkcode ArenaTagSide.BOTH} to check both sides; @@ -138,7 +126,7 @@ declare module "vitest" { /** * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The expected {@linkcode AbilityId} + * @param expectedAbilityId - The expected {@linkcode AbilityId} to */ toHaveAbilityApplied(expectedAbilityId: AbilityId): void; @@ -151,21 +139,20 @@ declare module "vitest" { /** * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs - * as otherwise the Pokemon will be GC'ed and rendered `undefined`. + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs, + * as otherwise the Pokemon will be removed from the field and garbage collected. */ toHaveFainted(): void; /** * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP + * @param moveId - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP * @param ppUsed - The numerical amount of PP that should have been consumed, * or `all` to indicate the move should be _out_ of PP * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}, - * does not contain {@linkcode expectedMove} - * or contains the desired move more than once, this will fail the test. + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE} + * or does not contain exactly 1 copy of {@linkcode moveId}, this will fail the test. */ - toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void; + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; } } diff --git a/test/test-utils/matchers/to-have-ability-applied.ts b/test/test-utils/matchers/to-have-ability-applied.ts index a3921e6371c..8ef343c6d74 100644 --- a/test/test-utils/matchers/to-have-ability-applied.ts +++ b/test/test-utils/matchers/to-have-ability-applied.ts @@ -21,7 +21,7 @@ export function toHaveAbilityApplied( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts index 425eb046a4e..1aba0009465 100644 --- a/test/test-utils/matchers/to-have-arena-tag.ts +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -36,8 +36,8 @@ export function toHaveArenaTag( if (!received.scene?.arena) { return { - pass: false, - message: () => `Expected GameManager.${!received.scene ? "scene" : "scene.arena"} to be defined!`, + pass: this.isNot, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } diff --git a/test/test-utils/matchers/to-have-effective-stat.ts b/test/test-utils/matchers/to-have-effective-stat.ts index fba285e49e7..dda6bc7e91e 100644 --- a/test/test-utils/matchers/to-have-effective-stat.ts +++ b/test/test-utils/matchers/to-have-effective-stat.ts @@ -42,7 +42,7 @@ export function toHaveEffectiveStat( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-fainted.ts b/test/test-utils/matchers/to-have-fainted.ts index 73ca96a31b5..f3e84e7a425 100644 --- a/test/test-utils/matchers/to-have-fainted.ts +++ b/test/test-utils/matchers/to-have-fainted.ts @@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-full-hp.ts b/test/test-utils/matchers/to-have-full-hp.ts index 3d7c8f9458d..893bb647283 100644 --- a/test/test-utils/matchers/to-have-full-hp.ts +++ b/test/test-utils/matchers/to-have-full-hp.ts @@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-hp.ts b/test/test-utils/matchers/to-have-hp.ts index 20d171b23ce..e6463383ac2 100644 --- a/test/test-utils/matchers/to-have-hp.ts +++ b/test/test-utils/matchers/to-have-hp.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-stat-stage.ts b/test/test-utils/matchers/to-have-stat-stage.ts index feecd650bef..a9ae910aece 100644 --- a/test/test-utils/matchers/to-have-stat-stage.ts +++ b/test/test-utils/matchers/to-have-stat-stage.ts @@ -23,14 +23,14 @@ export function toHaveStatStage( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } if (expectedStage < -6 || expectedStage > 6) { return { - pass: false, + pass: this.isNot, message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`, }; } diff --git a/test/test-utils/matchers/to-have-status-effect.ts b/test/test-utils/matchers/to-have-status-effect.ts index 9bcc0b5097f..fa5f0346ebd 100644 --- a/test/test-utils/matchers/to-have-status-effect.ts +++ b/test/test-utils/matchers/to-have-status-effect.ts @@ -28,7 +28,7 @@ export function toHaveStatusEffect( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-taken-damage.ts b/test/test-utils/matchers/to-have-taken-damage.ts index 77c60ae836a..55c163a2dc7 100644 --- a/test/test-utils/matchers/to-have-taken-damage.ts +++ b/test/test-utils/matchers/to-have-taken-damage.ts @@ -24,7 +24,7 @@ export function toHaveTakenDamage( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index 29a56ceb23b..438d72bc957 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -20,14 +20,14 @@ export function toHaveTerrain( ): SyncExpectationResult { if (!isGameManagerInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, }; } if (!received.scene?.arena) { return { - pass: false, + pass: this.isNot, message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index e7aab8e7d94..d4ced6185ac 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -26,14 +26,14 @@ export interface toHaveTypesOptions { /** * Matcher that checks if a {@linkcode Pokemon}'s typing is as expected. * @param received - The object to check. Should be a {@linkcode Pokemon} - * @param expected - An array of one or more {@linkcode PokemonType}s to compare against. + * @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against. * @param mode - The mode to perform the matching; * @returns The result of the matching */ export function toHaveTypes( this: MatcherState, received: unknown, - expected: [PokemonType, ...PokemonType[]], + expectedTypes: [PokemonType, ...PokemonType[]], { mode = "unordered", args = [] }: toHaveTypesOptions = {}, ): SyncExpectationResult { if (!isPokemonInstance(received)) { @@ -43,19 +43,27 @@ export function toHaveTypes( }; } + // Return early if no types were passed in + if (expectedTypes.length === 0) { + return { + pass: this.isNot, + message: () => "Expected to recieve a non-empty array of PokemonTypes, but got one!", + }; + } + // Avoid sorting the types if strict ordering is desired - const actualTypes = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted(); - const expectedTypes = mode === "ordered" ? expected : expected.toSorted(); + const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted(); + const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted(); // Exact matches do not care about subset equality const matchers = mode === "superset" ? [...this.customTesters, this.utils.iterableEquality] : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; - const pass = this.equals(actualTypes, expectedTypes, matchers); + const pass = this.equals(actualSorted, expectedSorted, matchers); - const actualStr = stringifyEnumArray(PokemonType, actualTypes); - const expectedStr = stringifyEnumArray(PokemonType, expectedTypes); + const actualStr = stringifyEnumArray(PokemonType, actualSorted); + const expectedStr = stringifyEnumArray(PokemonType, expectedSorted); const pkmName = getPokemonNameWithAffix(received); return { @@ -64,7 +72,7 @@ export function toHaveTypes( pass ? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!` : `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`, - expected: expectedTypes, - actual: actualTypes, + expected: expectedSorted, + actual: actualSorted, }; } diff --git a/test/test-utils/matchers/to-have-used-move.ts b/test/test-utils/matchers/to-have-used-move.ts index 3a3008fdf50..3697b3e0bc6 100644 --- a/test/test-utils/matchers/to-have-used-move.ts +++ b/test/test-utils/matchers/to-have-used-move.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** * Matcher to check the contents of a {@linkcode Pokemon}'s move history. * @param received - The actual value received. Should be a {@linkcode Pokemon} - * @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used, + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, * or a partially filled {@linkcode TurnMove} containing the desired properties to check * @param index - The index of the move history entry to check, in order from most recent to least recent. * Default `0` (last used move) @@ -22,7 +22,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveUsedMove( this: MatcherState, received: unknown, - expectedResult: MoveId | AtLeastOne, + expectedMove: MoveId | AtLeastOne, index = 0, ): SyncExpectationResult { if (!isPokemonInstance(received)) { @@ -44,26 +44,26 @@ export function toHaveUsedMove( } // Coerce to a `TurnMove` - if (typeof expectedResult === "number") { - expectedResult = { move: expectedResult }; + if (typeof expectedMove === "number") { + expectedMove = { move: expectedMove }; } const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`; - const pass = this.equals(move, expectedResult, [ + const pass = this.equals(move, expectedMove, [ ...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality, ]); - const expectedStr = getOnelineDiffStr.call(this, expectedResult); + const expectedStr = getOnelineDiffStr.call(this, expectedMove); return { pass, message: () => pass ? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!` : `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`, - expected: expectedResult, + expected: expectedMove, actual: move, }; } diff --git a/test/test-utils/matchers/to-have-used-pp.ts b/test/test-utils/matchers/to-have-used-pp.ts index ce7f5b5ea23..269d97c6ee2 100644 --- a/test/test-utils/matchers/to-have-used-pp.ts +++ b/test/test-utils/matchers/to-have-used-pp.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** * Matcher to check the amount of PP consumed by a {@linkcode Pokemon}. * @param received - The actual value received. Should be a {@linkcode Pokemon} - * @param expectedValue - The {@linkcode MoveId} that should have consumed PP + * @param moveId - The {@linkcode MoveId} that should have consumed PP * @param ppUsed - The numerical amount of PP that should have been consumed, * or `all` to indicate the move should be _out_ of PP * @returns Whether the matcher passed @@ -23,7 +23,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveUsedPP( this: MatcherState, received: unknown, - expectedMove: MoveId, + moveId: MoveId, ppUsed: number | "all", ): SyncExpectationResult { if (!isPokemonInstance(received)) { @@ -43,15 +43,15 @@ export function toHaveUsedPP( } const pkmName = getPokemonNameWithAffix(received); - const moveStr = getEnumStr(MoveId, expectedMove); + const moveStr = getEnumStr(MoveId, moveId); - const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove); + const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId); if (movesetMoves.length !== 1) { return { pass: this.isNot, message: () => `Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`, - expected: expectedMove, + expected: moveId, actual: received.getMoveset(), }; } diff --git a/test/test-utils/string-utils.ts b/test/test-utils/string-utils.ts index bd3dd7c2fa9..c997356fbd6 100644 --- a/test/test-utils/string-utils.ts +++ b/test/test-utils/string-utils.ts @@ -174,10 +174,14 @@ export function getStatName(s: Stat): string { * Convert an object into a oneline diff to be shown in an error message. * @param obj - The object to return the oneline diff of * @returns The updated diff + * @example + * ```ts + * const diff = getOnelineDiffStr.call(this, obj) + * ``` */ export function getOnelineDiffStr(this: MatcherState, obj: unknown): string { return this.utils .stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false }) .replace(/\n/g, " ") // Replace newlines with spaces - .replace(/,(\s*)}$/g, "$1}"); + .replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas } From c89accc6731549b490014343de0993b2f018c51a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 16:26:39 -0400 Subject: [PATCH 07/14] Shuffled a few funcs around --- test/@types/vitest.d.ts | 89 ++++++++++--------- test/test-utils/matchers/to-have-arena-tag.ts | 5 +- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 086675da78d..603acfb0bb5 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,5 +1,5 @@ import type { TerrainType } from "#app/data/terrain"; -import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; +import type { ArenaTag } from "#data/arena-tag"; import type { AbilityId } from "#enums/ability-id"; import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; @@ -21,7 +21,7 @@ import "vitest"; import type Overrides from "#app/overrides"; import type { ArenaTagSide } from "#enums/arena-tag-side"; import type { PokemonMove } from "#moves/pokemon-move"; -import type { OneOther } from "#test/@types/test-helpers"; +import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; declare module "vitest" { interface Assertion { @@ -36,6 +36,38 @@ declare module "vitest" { */ toEqualArrayUnsorted(expected: T[]): void; + // #region Arena Matchers + + /** + * Check whether the current {@linkcode WeatherType} is as expected. + * @param expectedWeatherType - The expected {@linkcode WeatherType} + */ + toHaveWeather(expectedWeatherType: WeatherType): void; + + /** + * Check whether the current {@linkcode TerrainType} is as expected. + * @param expectedTerrainType - The expected {@linkcode TerrainType} + */ + toHaveTerrain(expectedTerrainType: TerrainType): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties + */ + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides; + * default `ArenaTagSide.BOTH` + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + // #endregion Arena Matchers + + // #region Pokemon Matchers + /** * Check whether a {@linkcode Pokemon}'s current typing includes the given types. * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` @@ -64,46 +96,6 @@ declare module "vitest" { */ toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - - /** - * Check whether the current {@linkcode WeatherType} is as expected. - * @param expectedWeatherType - The expected {@linkcode WeatherType} - */ - toHaveWeather(expectedWeatherType: WeatherType): void; - - /** - * Check whether the current {@linkcode TerrainType} is as expected. - * @param expectedTerrainType - The expected {@linkcode TerrainType} - */ - toHaveTerrain(expectedTerrainType: TerrainType): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties - */ - toHaveArenaTag( - expectedTag: OneOther & { tagType: T }, // intersection required to preserve T - ): void; - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or - * {@linkcode ArenaTagSide.BOTH} to check both sides; - * default `ArenaTagSide.BOTH` - */ - toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; - - /** - * Check whether a {@linkcode Pokemon} is at full HP. - */ - toHaveFullHp(): void; - /** * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, @@ -136,6 +128,13 @@ declare module "vitest" { */ toHaveHp(expectedHp: number): void; + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + /** * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). * @remarks @@ -144,6 +143,10 @@ declare module "vitest" { */ toHaveFainted(): void; + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; /** * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. * @param moveId - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP @@ -154,5 +157,7 @@ declare module "vitest" { * or does not contain exactly 1 copy of {@linkcode moveId}, this will fail the test. */ toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; + + // #region Pokemon Matchers } } diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts index 1aba0009465..f02f350d3e3 100644 --- a/test/test-utils/matchers/to-have-arena-tag.ts +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -9,7 +9,10 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { NonFunctionPropertiesRecursive } from "#types/type-helpers"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; -export type toHaveArenaTagOptions = OneOther; +// intersection required to preserve T for inferences +export type toHaveArenaTagOptions = OneOther & { + tagType: T; +}; /** * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. From b98ff5ae906c3fcd2c4019c08d6a305d0b7b4de6 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 17:12:59 -0400 Subject: [PATCH 08/14] Moar fixups to strings --- test/test-utils/matchers/to-have-arena-tag.ts | 36 +++++++++---------- test/test-utils/matchers/to-have-terrain.ts | 4 +-- test/test-utils/matchers/to-have-weather.ts | 4 +-- test/test-utils/string-utils.ts | 8 ++--- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts index f02f350d3e3..2d03711fd46 100644 --- a/test/test-utils/matchers/to-have-arena-tag.ts +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -1,10 +1,10 @@ import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; import type { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; +import type { ArenaTagType } from "#enums/arena-tag-type"; import type { OneOther } from "#test/@types/test-helpers"; // biome-ignore lint/correctness/noUnusedImports: TSDoc import type { GameManager } from "#test/test-utils/game-manager"; -import { getEnumStr, getOnelineDiffStr, stringifyEnumArray } from "#test/test-utils/string-utils"; +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; import type { NonFunctionPropertiesRecursive } from "#types/type-helpers"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; @@ -17,7 +17,7 @@ export type toHaveArenaTagOptions = OneOther( this: MatcherState, received: unknown, // simplified types used for brevity; full overloads are in `vitest.d.ts` - expectedType: T | (Partial> & { tagType: T; side: ArenaTagSide }), + expectedTag: T | (Partial> & { tagType: T; side: ArenaTagSide }), side?: ArenaTagSide, ): SyncExpectationResult { if (!isGameManagerInstance(received)) { @@ -44,40 +44,36 @@ export function toHaveArenaTag( }; } - if (typeof expectedType === "string") { + if (typeof expectedTag === "string") { // Coerce lone `tagType`s into objects // Bangs are ok as we enforce safety via overloads - expectedType = { tagType: expectedType, side: side! }; + expectedTag = { tagType: expectedTag, side: side! }; } // We need to get all tags for the case of checking properties of a tag present on both sides of the arena - const tags = received.scene.arena.findTagsOnSide(t => t.tagType === expectedType.tagType, expectedType.side); - if (!tags.length) { - const expectedStr = getEnumStr(ArenaTagType, expectedType.tagType); + const tags = received.scene.arena.findTagsOnSide(t => t.tagType === expectedTag.tagType, expectedTag.side); + if (tags.length === 0) { return { pass: false, - message: () => `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, - expected: getEnumStr(ArenaTagType, expectedType.tagType), - actual: stringifyEnumArray( - ArenaTagType, - received.scene.arena.tags.map(t => t.tagType), - ), + message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`, + expected: expectedTag.tagType, + actual: received.scene.arena.tags.map(t => t.tagType), }; } // Pass if any of the matching tags meet our criteria const pass = tags.some(tag => - this.equals(tag, expectedType, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), + this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), ); - const expectedStr = getOnelineDiffStr.call(this, expectedType); + const expectedStr = getOnelineDiffStr.call(this, expectedTag); return { pass, message: () => pass - ? `Expected the arena to NOT have a tag matching ${expectedStr}, but it did!` - : `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, - expected: expectedType, + ? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!` + : `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: expectedTag, actual: tags, }; } diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index 438d72bc957..f951abed0b3 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -41,8 +41,8 @@ export function toHaveTerrain( pass, message: () => pass - ? `Expected Arena to NOT have ${expectedStr} active, but it did!` - : `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`, + ? `Expected the Arena to NOT have ${expectedStr} active, but it did!` + : `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`, expected: expectedTerrainType, actual, }; diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index 21a3a54c80d..ffb1e0aad97 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -41,8 +41,8 @@ export function toHaveWeather( pass, message: () => pass - ? `Expected Arena to NOT have ${expectedStr} weather active, but it did!` - : `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`, + ? `Expected the Arena to NOT have ${expectedStr} weather active, but it did!` + : `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`, expected: expectedWeatherType, actual, }; diff --git a/test/test-utils/string-utils.ts b/test/test-utils/string-utils.ts index c997356fbd6..6c29c04c107 100644 --- a/test/test-utils/string-utils.ts +++ b/test/test-utils/string-utils.ts @@ -34,10 +34,10 @@ interface getEnumStrOptions { * @returns The stringified representation of `val` as dictated by the options. * @example * ```ts - * enum fakeEnum { - * ONE: 1, - * TWO: 2, - * THREE: 3, + * enum testEnum { + * ONE = 1, + * TWO = 2, + * THREE = 3, * } * getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)" * getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)" From e968063eaa8d0d5fa447a5a831286fea47f80912 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 17:21:24 -0400 Subject: [PATCH 09/14] Added `toHavePositionalTag` matcher --- test/@types/vitest.d.ts | 21 +++- test/matchers.setup.ts | 12 +- test/moves/wish.test.ts | 32 ++---- .../matchers/to-have-positional-tag.ts | 108 ++++++++++++++++++ 4 files changed, 145 insertions(+), 28 deletions(-) create mode 100644 test/test-utils/matchers/to-have-positional-tag.ts diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 603acfb0bb5..e9f2dcee116 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -17,11 +17,13 @@ import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; import type { toDmgValue } from "utils/common"; import type { expect } from "vitest"; -import "vitest"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; +import { PositionalTagType } from "#enums/positional-tag-type"; import type Overrides from "#app/overrides"; import type { ArenaTagSide } from "#enums/arena-tag-side"; import type { PokemonMove } from "#moves/pokemon-move"; import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; declare module "vitest" { interface Assertion { @@ -64,16 +66,29 @@ declare module "vitest" { */ toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled {@linkcode PositionalTag} containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of {@linkcode expectedType} that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; + // #endregion Arena Matchers // #region Pokemon Matchers /** * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param expectedTags - The expected {@linkcode PokemonType}s to check against; must have length `>0` * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher */ - toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + toHaveTypes(expectedTags: PokemonType[], options?: toHaveTypesOptions): void; /** * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index 447911c1d87..f76a9423ab3 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -6,6 +6,7 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect"; import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage"; @@ -23,19 +24,20 @@ import { expect } from "vitest"; expect.extend({ toEqualArrayUnsorted, - toHaveTypes, - toHaveUsedMove, - toHaveEffectiveStat, - toHaveTakenDamage, toHaveWeather, toHaveTerrain, toHaveArenaTag, - toHaveFullHp, + toHavePositionalTag, + toHaveTypes, + toHaveUsedMove, + toHaveEffectiveStat, toHaveStatusEffect, toHaveStatStage, toHaveBattlerTag, toHaveAbilityApplied, toHaveHp, + toHaveTakenDamage, + toHaveFullHp, toHaveFainted, toHaveUsedPP, }); diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 147c598106b..55877edbfd4 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -39,15 +39,6 @@ describe("Move - Wish", () => { .enemyLevel(100); }); - /** - * Expect that wish is active with the specified number of attacks. - * @param numAttacks - The number of wish instances that should be queued; default `1` - */ - function expectWishActive(numAttacks = 1) { - const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH); - expect(wishes).toHaveLength(numAttacks); - } - it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => { await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]); @@ -58,19 +49,19 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.doSwitchPokemon(1); await game.toEndOfTurn(); - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); expect(game.textInterceptor.logs).toContain( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(alomomola), }), ); - expect(alomomola.hp).toBe(1); - expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1); + expect(alomomola).toHaveHp(1); + expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1); }); it("should work if the user has full HP, but not if it already has an active Wish", async () => { @@ -82,13 +73,13 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.move.use(MoveId.WISH); await game.toEndOfTurn(); expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1); - expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL }); }); it("should function independently of Future Sight", async () => { @@ -103,7 +94,8 @@ describe("Move - Wish", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); - expectWishActive(1); + expect(game).toHavePositionalTag(PositionalTagType.WISH); + expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK); }); it("should work in double battles and trigger in order of creation", async () => { @@ -127,7 +119,7 @@ describe("Move - Wish", () => { await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); await game.toNextTurn(); - expectWishActive(4); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 4); // Lower speed to change turn order alomomola.setStatStage(Stat.SPD, 6); @@ -141,7 +133,7 @@ describe("Move - Wish", () => { await game.phaseInterceptor.to("PositionalTagPhase"); // all wishes have activated and added healing phases - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); expect(healPhases).toHaveLength(4); @@ -165,14 +157,14 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); await game.toEndOfTurn(); // Wish went away without doing anything - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); expect(game.textInterceptor.logs).not.toContain( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(blissey), diff --git a/test/test-utils/matchers/to-have-positional-tag.ts b/test/test-utils/matchers/to-have-positional-tag.ts new file mode 100644 index 00000000000..108a72d2fd0 --- /dev/null +++ b/test/test-utils/matchers/to-have-positional-tag.ts @@ -0,0 +1,108 @@ +import type { SerializedPositionalTag, serializedPosTagMap } from "#data/positional-tags/load-positional-tag"; +import type { PositionalTagType } from "#enums/positional-tag-type"; +import type { OneOther } from "#test/@types/test-helpers"; +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import { toTitleCase } from "#utils/strings"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +export type toHavePositionalTagOptions

= OneOther & { + tagType: P; +}; + +/** + * Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active. + * @param received - The object to check. Should be the current {@linkcode GameManager} + * @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag} + * containing the desired properties + * @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]` + * @returns The result of the matching + */ +export function toHavePositionalTag

( + this: MatcherState, + received: unknown, + // simplified types used for brevity; full overloads are in `vitest.d.ts` + expectedTag: P | (Partial & { tagType: P }), + count = 1, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena?.positionalTagManager) { + return { + pass: this.isNot, + message: () => + `Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`, + }; + } + + // TODO: Increase limit if triple battles are added + if (count < 0 || count > 4) { + return { + pass: this.isNot, + message: () => `Expected count to be between 0 and 4, but got ${count} instead!`, + }; + } + + const allTags = received.scene.arena.positionalTagManager.tags; + const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType; + const matchingTags = allTags.filter(t => t.tagType === tagType); + + // If checking exclusively tag type, check solely the number of ,atching tags on field + if (typeof expectedTag === "string") { + const pass = matchingTags.length === count; + const expectedStr = getPosTagStr(expectedTag); + + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!` + : `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`, + expected: expectedTag, + actual: allTags, + }; + } + + // Check for equality with the provided object + + if (matchingTags.length === 0) { + return { + pass: false, + message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`, + expected: expectedTag.tagType, + actual: received.scene.arena.tags.map(t => t.tagType), + }; + } + + // Pass if any of the matching tags meet our criteria + const pass = matchingTags.some(tag => + this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), + ); + + const expectedStr = getOnelineDiffStr.call(this, expectedTag); + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!` + : `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: expectedTag, + actual: matchingTags, + }; +} + +function getPosTagStr(pType: PositionalTagType, count = 1): string { + let ret = toTitleCase(pType) + "Tag"; + if (count > 1) { + ret += "s"; + } + return ret; +} From 71801fe298b5d70ad75e4edaf5a25884cd215248 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:04:20 -0400 Subject: [PATCH 10/14] Update test/test-utils/matchers/to-have-types.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/matchers/to-have-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index d4ced6185ac..ce72571ee2f 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -47,7 +47,7 @@ export function toHaveTypes( if (expectedTypes.length === 0) { return { pass: this.isNot, - message: () => "Expected to recieve a non-empty array of PokemonTypes, but got one!", + message: () => "Expected to receive a non-empty array of PokemonTypes!", }; } From cd890025d1f1201e789fd01fabd8f2d8292fda0b Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:04:31 -0400 Subject: [PATCH 11/14] Update test/@types/vitest.d.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/@types/vitest.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index e9f2dcee116..04c017f8e73 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -133,7 +133,7 @@ declare module "vitest" { /** * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The expected {@linkcode AbilityId} to + * @param expectedAbilityId - The expected {@linkcode AbilityId} to check for */ toHaveAbilityApplied(expectedAbilityId: AbilityId): void; From 7e7ca6b3fabe085a130a1c2b25eb0b3eddb2f338 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:04:39 -0400 Subject: [PATCH 12/14] Update test/test-utils/matchers/to-have-positional-tag.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/matchers/to-have-positional-tag.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test-utils/matchers/to-have-positional-tag.ts b/test/test-utils/matchers/to-have-positional-tag.ts index 108a72d2fd0..d3a62b8ceef 100644 --- a/test/test-utils/matchers/to-have-positional-tag.ts +++ b/test/test-utils/matchers/to-have-positional-tag.ts @@ -1,9 +1,10 @@ -import type { SerializedPositionalTag, serializedPosTagMap } from "#data/positional-tags/load-positional-tag"; -import type { PositionalTagType } from "#enums/positional-tag-type"; -import type { OneOther } from "#test/@types/test-helpers"; // biome-ignore-start lint/correctness/noUnusedImports: TSDoc import type { GameManager } from "#test/test-utils/game-manager"; // biome-ignore-end lint/correctness/noUnusedImports: TSDoc + +import type { SerializedPositionalTag, serializedPosTagMap } from "#data/positional-tags/load-positional-tag"; +import type { PositionalTagType } from "#enums/positional-tag-type"; +import type { OneOther } from "#test/@types/test-helpers"; import { getOnelineDiffStr } from "#test/test-utils/string-utils"; import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; import { toTitleCase } from "#utils/strings"; From 9298ff82822ace265b810be5b12523d226a99cff Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:04:55 -0400 Subject: [PATCH 13/14] Update test/test-utils/matchers/to-have-positional-tag.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/matchers/to-have-positional-tag.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test-utils/matchers/to-have-positional-tag.ts b/test/test-utils/matchers/to-have-positional-tag.ts index d3a62b8ceef..a50a26f5c6d 100644 --- a/test/test-utils/matchers/to-have-positional-tag.ts +++ b/test/test-utils/matchers/to-have-positional-tag.ts @@ -73,7 +73,6 @@ export function toHavePositionalTag

( } // Check for equality with the provided object - if (matchingTags.length === 0) { return { pass: false, From 6866248b411111c04bdfef8c5281c208cc89d707 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:05:04 -0400 Subject: [PATCH 14/14] Update test/test-utils/matchers/to-have-positional-tag.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/test-utils/matchers/to-have-positional-tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/matchers/to-have-positional-tag.ts b/test/test-utils/matchers/to-have-positional-tag.ts index a50a26f5c6d..5ec9c71f3a1 100644 --- a/test/test-utils/matchers/to-have-positional-tag.ts +++ b/test/test-utils/matchers/to-have-positional-tag.ts @@ -56,7 +56,7 @@ export function toHavePositionalTag

( const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType; const matchingTags = allTags.filter(t => t.tagType === tagType); - // If checking exclusively tag type, check solely the number of ,atching tags on field + // If checking exclusively tag type, check solely the number of matching tags on field if (typeof expectedTag === "string") { const pass = matchingTags.length === count; const expectedStr = getPosTagStr(expectedTag);