From 77f6e5b36e18dbb340c0d0c2ee26af899e727cdf Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 27 Jul 2025 23:08:37 -0400 Subject: [PATCH] Fixed up docs and tests --- src/@types/type-helpers.ts | 2 +- test/@types/vitest.d.ts | 37 ++++++------ test/moves/spite.test.ts | 58 ++++++++++++++++++- .../matchers/to-equal-array-unsorted.ts | 2 +- .../matchers/to-have-ability-applied.ts | 2 +- .../matchers/to-have-battler-tag.ts | 2 +- .../matchers/to-have-effective-stat.ts | 4 +- test/test-utils/matchers/to-have-fainted.ts | 4 +- test/test-utils/matchers/to-have-full-hp.ts | 4 +- test/test-utils/matchers/to-have-hp.ts | 2 +- .../test-utils/matchers/to-have-stat-stage.ts | 2 +- .../matchers/to-have-status-effect.ts | 2 +- .../matchers/to-have-taken-damage.ts | 2 +- test/test-utils/matchers/to-have-terrain.ts | 2 +- test/test-utils/matchers/to-have-types.ts | 2 +- test/test-utils/matchers/to-have-weather.ts | 2 +- test/types/type-helpers.test-d.ts | 38 ++++++++++++ 17 files changed, 133 insertions(+), 34 deletions(-) create mode 100644 test/types/type-helpers.test-d.ts diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index 2085d22b0a9..893ac99e78a 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -94,4 +94,4 @@ export type CoerceNullPropertiesToUndefined = { * Distinct from {@linkcode Partial} as this requires at least 1 property to _not_ be undefined. * @typeParam T - The type to render partial */ -export type AtLeastOne = Partial & EnumValues<{ [K in keyof T]: Pick }>; +export type AtLeastOne = Partial & EnumValues<{ [K in keyof T]: Pick, K> }>; diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 0e9cd1b2fa3..ae1383492f0 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -8,17 +8,18 @@ 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 { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; +import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; 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 { TurnMove } from "#types/turn-move"; +import type { AtLeastOne } from "#types/type-helpers"; import type { expect } from "vitest"; import type Overrides from "#app/overrides"; +import type { PokemonMove } from "#moves/pokemon-move"; declare module "vitest" { interface Assertion { /** - * Matcher to check if an array contains EXACTLY the given items (in any order). + * Check whether 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). @@ -29,7 +30,7 @@ declare module "vitest" { toEqualArrayUnsorted(expected: E[]): void; /** - * Matcher to check if a {@linkcode Pokemon}'s current typing includes the given types + * 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 @@ -47,7 +48,7 @@ declare module "vitest" { toHaveUsedMove(expected: MoveId | AtLeastOne, index?: number): void; /** - * Matcher to check if a {@linkcode Pokemon Pokemon's} effective stat is as expected + * Check whether a {@linkcode Pokemon Pokemon's} effective stat is as expected * (checked after all stat value modifications). * * @param stat - The {@linkcode EffectiveStat} to check @@ -59,69 +60,69 @@ declare module "vitest" { toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void; /** - * Matcher to check if a {@linkcode Pokemon} has taken a specific amount of damage. + * 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; /** - * Matcher to check if the current {@linkcode WeatherType} is as expected. + * Check whether the current {@linkcode WeatherType} is as expected. * @param expectedWeatherType - The expected {@linkcode WeatherType} */ toHaveWeather(expectedWeatherType: WeatherType): void; /** - * Matcher to check if the current {@linkcode TerrainType} is as expected. + * Check whether the current {@linkcode TerrainType} is as expected. * @param expectedTerrainType - The expected {@linkcode TerrainType} */ toHaveTerrain(expectedTerrainType: TerrainType): void; /** - * Matcher to check if a {@linkcode Pokemon} is at full HP. + * Check whether 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}. + * 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, * 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. + * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. * @param stat - The {@linkcode BattleStat} to check * @param expectedStage - The expected stat stage value of {@linkcode stat} */ toHaveStatStage(stat: BattleStat, expectedStage: number): void; /** - * Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}. + * Check whether a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}. * @param expectedBattlerTagType - The expected {@linkcode BattlerTagType} */ toHaveBattlerTag(expectedBattlerTagType: BattlerTagType): void; /** - * Matcher to check if a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. * @param expectedAbilityId - The expected {@linkcode AbilityId} */ toHaveAbilityApplied(expectedAbilityId: AbilityId): void; /** - * Matcher to check if a {@linkcode Pokemon} has a specific amount of HP. + * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have */ toHaveHp(expectedHp: number): void; /** - * Matcher to check if a {@linkcode Pokemon} has fainted (as determined by {@linkcode Pokemon.isFainted}). + * Check whether 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 + * 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 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}, diff --git a/test/moves/spite.test.ts b/test/moves/spite.test.ts index 927d30fadd2..1b79d70f038 100644 --- a/test/moves/spite.test.ts +++ b/test/moves/spite.test.ts @@ -36,12 +36,68 @@ describe("Moves - Spite", () => { it("should reduce the PP of the target's last move by 4", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); + const karp = game.field.getEnemyPokemon(); + game.move.changeMoveset(karp, [MoveId.SPLASH, MoveId.TACKLE]); + + game.move.use(MoveId.SPLASH); + await game.move.selectEnemyMove(MoveId.TACKLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(karp).toHaveUsedPP(MoveId.TACKLE, 1); + game.move.use(MoveId.SPITE); await game.move.forceEnemyMove(MoveId.TACKLE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); + expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1); + }); + + it("should fail if the target has not used a move", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + const karp = game.field.getEnemyPokemon(); - expect(karp.getMoveset()).toBe(); + game.move.changeMoveset(karp, [MoveId.SPLASH, MoveId.TACKLE]); + + game.move.use(MoveId.SPLASH); + await game.move.selectEnemyMove(MoveId.TACKLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + const feebas = game.field.getPlayerPokemon(); + expect(feebas).toHaveUsedMove({}); + expect(karp).toHaveUsedPP(MoveId.TACKLE, 1); + + game.move.use(MoveId.SPITE); + await game.move.forceEnemyMove(MoveId.TACKLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); + + expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1); + }); + + it("should ignore virtual or Dancer-induced moves", async () => { + game.override.battleStyle("double").enemyAbility(AbilityId.DANCER); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const [karp1, karp2] = game.scene.getEnemyField(); + game.move.changeMoveset(karp1, [MoveId.SPLASH, MoveId.METRONOME]); + game.move.changeMoveset(karp2, [MoveId.SWORDS_DANCE, MoveId.TACKLE]); + + game.move.use(MoveId.SPITE); + await game.move.selectEnemyMove(MoveId.METRONOME); + await game.move.selectEnemyMove(MoveId.SWORDS_DANCE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(karp).toHaveUsedPP(MoveId.TACKLE, 1); + + game.move.use(MoveId.SPITE); + await game.move.forceEnemyMove(MoveId.TACKLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); + + expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1); }); }); diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-array-unsorted.ts index e9e3c6c3855..7f43908937d 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-array-unsorted.ts @@ -1,7 +1,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if an array contains exactly the given items, disregarding order. + * Matcher that checks if an array contains exactly the given items, disregarding order. * @param received - The received value. Should be an array of elements * @param expected - The array to check equality with * @returns Whether the matcher passed diff --git a/test/test-utils/matchers/to-have-ability-applied.ts b/test/test-utils/matchers/to-have-ability-applied.ts index de3f12a3c44..a3921e6371c 100644 --- a/test/test-utils/matchers/to-have-ability-applied.ts +++ b/test/test-utils/matchers/to-have-ability-applied.ts @@ -9,7 +9,7 @@ import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * Matcher that checks if a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. * @param received - The object to check. Should be a {@linkcode Pokemon} * @param expectedAbility - The {@linkcode AbilityId} to check for * @returns Whether the matcher passed diff --git a/test/test-utils/matchers/to-have-battler-tag.ts b/test/test-utils/matchers/to-have-battler-tag.ts index e2bee37849e..8ff82281d59 100644 --- a/test/test-utils/matchers/to-have-battler-tag.ts +++ b/test/test-utils/matchers/to-have-battler-tag.ts @@ -9,7 +9,7 @@ import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}. + * Matcher that checks if a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}. * @param received - The object to check. Should be a {@linkcode Pokemon} * @param expectedBattlerTagType - The {@linkcode BattlerTagType} to check for * @returns Whether the matcher passed diff --git a/test/test-utils/matchers/to-have-effective-stat.ts b/test/test-utils/matchers/to-have-effective-stat.ts index 0a5c7eb2aba..42faee2e904 100644 --- a/test/test-utils/matchers/to-have-effective-stat.ts +++ b/test/test-utils/matchers/to-have-effective-stat.ts @@ -26,8 +26,8 @@ export interface ToHaveEffectiveStatMatcherOptions { } /** - * Matcher to check if a {@linkcode Pokemon}'s effective stat equals the expected value - * @param received - The object to check. Should be a {@linkcode Pokemon}. + * Matcher that checks if a {@linkcode Pokemon}'s effective stat equals a certain value. + * @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} diff --git a/test/test-utils/matchers/to-have-fainted.ts b/test/test-utils/matchers/to-have-fainted.ts index 8b8340c778e..005bdd9c4b4 100644 --- a/test/test-utils/matchers/to-have-fainted.ts +++ b/test/test-utils/matchers/to-have-fainted.ts @@ -1,9 +1,11 @@ import { getPokemonNameWithAffix } from "#app/messages"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { Pokemon } from "#field/pokemon"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a Pokemon has fainted. + * Matcher that checks if a {@linkcode Pokemon} has fainted. * @param received - The object to check. Should be a {@linkcode Pokemon} * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-full-hp.ts b/test/test-utils/matchers/to-have-full-hp.ts index e88ef6b5dcd..57f40e3a5c9 100644 --- a/test/test-utils/matchers/to-have-full-hp.ts +++ b/test/test-utils/matchers/to-have-full-hp.ts @@ -1,9 +1,11 @@ import { getPokemonNameWithAffix } from "#app/messages"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { Pokemon } from "#field/pokemon"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a Pokemon is full hp. + * Matcher that checks if a {@linkcode Pokemon} is at full hp. * @param received - The object to check. Should be a {@linkcode Pokemon}. * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-hp.ts b/test/test-utils/matchers/to-have-hp.ts index ad9802dac3a..20d171b23ce 100644 --- a/test/test-utils/matchers/to-have-hp.ts +++ b/test/test-utils/matchers/to-have-hp.ts @@ -5,7 +5,7 @@ import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a Pokemon has a specific amount of HP + * Matcher that checks if a Pokemon has a specific amount of HP. * @param received - The object to check. Should be a {@linkcode Pokemon}. * @param expectedHp - The expected amount of HP the {@linkcode Pokemon} has * @returns Whether the matcher passed diff --git a/test/test-utils/matchers/to-have-stat-stage.ts b/test/test-utils/matchers/to-have-stat-stage.ts index e3b7e000773..10e05aed903 100644 --- a/test/test-utils/matchers/to-have-stat-stage.ts +++ b/test/test-utils/matchers/to-have-stat-stage.ts @@ -5,7 +5,7 @@ import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a Pokemon has a specific {@linkcode Stat} stage + * Matcher that checks if a Pokemon has a specific {@linkcode Stat} stage. * @param received - The object to check. Should be a {@linkcode Pokemon}. * @param stat - The {@linkcode Stat} to check * @param expectedStage - The expected numerical value of {@linkcode stat}; should 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 20adea3689a..352d2f736d1 100644 --- a/test/test-utils/matchers/to-have-status-effect.ts +++ b/test/test-utils/matchers/to-have-status-effect.ts @@ -14,7 +14,7 @@ export type expectedStatusType = | { effect: StatusEffect.SLEEP; sleepTurnsRemaining: number }; /** - * Matcher to check if a Pokemon's {@linkcode StatusEffect} is as expected + * Matcher that checks if a Pokemon's {@linkcode StatusEffect} is as expected * @param received - The actual value received. Should be a {@linkcode Pokemon} * @param expectedStatus - The {@linkcode StatusEffect} the Pokemon is expected to have, * or a partially filled {@linkcode Status} containing the desired properties diff --git a/test/test-utils/matchers/to-have-taken-damage.ts b/test/test-utils/matchers/to-have-taken-damage.ts index 6cfe57148dc..d085c24b073 100644 --- a/test/test-utils/matchers/to-have-taken-damage.ts +++ b/test/test-utils/matchers/to-have-taken-damage.ts @@ -4,7 +4,7 @@ import { toDmgValue } from "#utils/common"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if a Pokemon has taken a specific amount of damage. + * Matcher that checks if a Pokemon has taken a specific amount of damage. * Unless specified, will run the expected damage value through {@linkcode toDmgValue} * to round it down and make it a minimum of 1. * @param received - The object to check. Should be a {@linkcode Pokemon}. diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index 747c5e2d8b8..3c81a123ef0 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -4,7 +4,7 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if the {@linkcode TerrainType} is as expected + * Matcher that checks if the {@linkcode TerrainType} is as expected * @param received - The object to check. Should be an instance of {@linkcode GameManager}. * @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active * @returns Whether the matcher passed diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index ff865a1cf73..0c6006dbaeb 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -18,7 +18,7 @@ export interface toHaveTypesOptions { } /** - * Matcher to check if an array contains exactly the given items, disregarding order. + * 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 * @returns The result of the matching diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index 3e5eea9ae2b..8154efd234e 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -4,7 +4,7 @@ import { toTitleCase } from "#utils/strings"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher to check if the {@linkcode WeatherType} is as expected + * Matcher that checks if the {@linkcode WeatherType} is as expected * @param received - The object to check. Expects an instance of {@linkcode GameManager}. * @param expectedWeatherType - The expected {@linkcode WeatherType} * @returns Whether the matcher passed diff --git a/test/types/type-helpers.test-d.ts b/test/types/type-helpers.test-d.ts new file mode 100644 index 00000000000..7c1d7af38cd --- /dev/null +++ b/test/types/type-helpers.test-d.ts @@ -0,0 +1,38 @@ +import type { AtLeastOne } from "#types/type-helpers"; +import { describe, it } from "node:test"; +import { expectTypeOf } from "vitest"; + +type fakeObj = { + foo: number; + bar: string; + baz: number | string; +}; + +type optionalObj = { + foo: number; + bar: string; + baz?: number | string; +}; + +describe("AtLeastOne", () => { + it("should accept an object with at least 1 of its defined parameters", () => { + expectTypeOf<{ foo: number }>().toExtend>(); + expectTypeOf<{ bar: string }>().toExtend>(); + expectTypeOf<{ baz: number | string }>().toExtend>(); + }); + + it("should convert to a partial intersected with the union of all individual single properties", () => { + expectTypeOf>().branded.toEqualTypeOf< + Partial & ({ foo: number } | { bar: string } | { baz: number | string }) + >(); + }); + + it("should treat optional properties as required", () => { + expectTypeOf>().branded.toEqualTypeOf>(); + }); + + it("should not accept empty objects, even if optional properties are present", () => { + expectTypeOf>().not.toExtend>(); + expectTypeOf>().not.toExtend>(); + }); +});