diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 2c54ed20535..0e9cd1b2fa3 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -13,6 +13,7 @@ import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types import { TurnMove } from "#types/turn-move"; import { AtLeastOne } from "#types/type-helpers"; import type { expect } from "vitest"; +import type Overrides from "#app/overrides"; declare module "vitest" { interface Assertion { @@ -20,7 +21,7 @@ declare module "vitest" { * Matcher to check if an array contains EXACTLY the given items (in any order). * * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality - * (as opposed to full equality) + * (as opposed to full equality). * * @param expected - The expected contents of the array, in any order * @see {@linkcode expect.arrayContaining} @@ -28,10 +29,10 @@ declare module "vitest" { toEqualArrayUnsorted(expected: E[]): void; /** - * Matcher to check if a {@linkcode Pokemon}'s current typing includes the given types. + * Matcher to check if 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 expected - The expected types (in any order) + * @param options - The options passed to the matcher */ toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; @@ -47,7 +48,7 @@ declare module "vitest" { /** * Matcher to check if a {@linkcode Pokemon Pokemon's} effective stat is as expected - * (checked after all mstat value modifications). + * (checked after all stat value modifications). * * @param stat - The {@linkcode EffectiveStat} to check * @param expectedValue - The expected value of {@linkcode stat} @@ -64,13 +65,6 @@ declare module "vitest" { */ toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - /** - * Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. - * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, - * or a partially filled {@linkcode Status} containing the desired properties - */ - toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; - /** * Matcher to check if the current {@linkcode WeatherType} is as expected. * @param expectedWeatherType - The expected {@linkcode WeatherType} @@ -84,10 +78,17 @@ declare module "vitest" { toHaveTerrain(expectedTerrainType: TerrainType): void; /** - * Matcher to check if a {@linkcode Pokemon} has full HP. + * Matcher to check if a {@linkcode Pokemon} is at full HP. */ toHaveFullHp(): void; + /** + * Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. + * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, + * or a partially filled {@linkcode Status} containing the desired properties + */ + toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; + /** * Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. * @param stat - The {@linkcode BattleStat} to check @@ -102,7 +103,7 @@ declare module "vitest" { toHaveBattlerTag(expectedBattlerTagType: BattlerTagType): void; /** - * Matcher to check if a {@linkcode Pokemon} had a specific {@linkcode AbilityId} applied. + * Matcher to check if a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. * @param expectedAbilityId - The expected {@linkcode AbilityId} */ toHaveAbilityApplied(expectedAbilityId: AbilityId): void; @@ -117,5 +118,15 @@ declare module "vitest" { * Matcher to check if a {@linkcode Pokemon} has fainted (as determined by {@linkcode Pokemon.isFainted}). */ toHaveFainted(): void; + + /** + * Matcher to check th + * @param expectedValue - The {@linkcode MoveId} that should have consumed PP + * @param ppUsed - The amount of PP that should have been consumed + * @remarks + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE} or {@linkcode OPP_MOVESET_OVERRIDE}, + * or contains the desired move more than once, this will fail the test. + */ + toHaveUsedPP(expectedMove: MoveId, ppUsed: number): void; } } diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index d3d14ee1700..03b29302916 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -11,6 +11,7 @@ import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damag import { toHaveTerrain } from "#test/test-utils/matchers/to-have-terrain"; import { toHaveTypes } from "#test/test-utils/matchers/to-have-types"; import { toHaveUsedMove } from "#test/test-utils/matchers/to-have-used-move"; +import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp"; import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather"; import { expect } from "vitest"; @@ -34,4 +35,5 @@ expect.extend({ toHaveAbilityApplied, toHaveHp, toHaveFainted, + toHaveUsedPP, }); diff --git a/test/moves/spite.test.ts b/test/moves/spite.test.ts index d78013a33b3..927d30fadd2 100644 --- a/test/moves/spite.test.ts +++ b/test/moves/spite.test.ts @@ -42,6 +42,6 @@ describe("Moves - Spite", () => { await game.toEndOfTurn(); const karp = game.field.getEnemyPokemon(); - expect(karp.getMoveset()).toBe(true); + expect(karp.getMoveset()).toBe(); }); }); diff --git a/test/test-utils/matchers/to-have-used-pp.ts b/test/test-utils/matchers/to-have-used-pp.ts new file mode 100644 index 00000000000..5a4036cbe3a --- /dev/null +++ b/test/test-utils/matchers/to-have-used-pp.ts @@ -0,0 +1,67 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import Overrides from "#app/overrides"; +import { MoveId } from "#enums/move-id"; +// biome-ignore lint/correctness/noUnusedImports: TSDocs +import type { Pokemon } from "#field/pokemon"; +import { getEnumStr } from "#test/test-utils/string-utils"; +import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; +import { coerceArray } from "#utils/common"; +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 ppUsed - The amount of PP that should have been consumed + * @returns Whether the matcher passed + * @remarks + * If the same move appears in the Pokemon's moveset multiple times, this will fail the test! + */ +export function toHaveUsedPP( + this: MatcherState, + received: unknown, + expectedMove: MoveId, + ppUsed: number, +): SyncExpectationResult { + if (!isPokemonInstance(received)) { + return { + pass: false, + message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, + }; + } + + const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; + if (coerceArray(override).length > 0) { + return { + pass: false, + message: () => + `Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`, + }; + } + + const pkmName = getPokemonNameWithAffix(received); + const moveStr = getEnumStr(MoveId, expectedMove); + + const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove); + if (movesetMoves.length !== 1) { + return { + pass: false, + message: () => + `Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`, + actual: received.getMoveset(), + }; + } + + const move = movesetMoves[0]; + const pass = move.ppUsed === ppUsed; + + return { + pass, + message: () => + pass + ? `Expected ${pkmName}'s ${moveStr} to NOT have used ${ppUsed} PP, but it did!` + : `Expected ${pkmName}'s ${moveStr} to have used ${ppUsed} PP, but got ${move.ppUsed} instead!`, + expected: ppUsed, + actual: move.ppUsed, + }; +}