Fixed up docs and tests

This commit is contained in:
Bertie690 2025-07-27 23:08:37 -04:00
parent b384535188
commit 77f6e5b36e
17 changed files with 133 additions and 34 deletions

View File

@ -94,4 +94,4 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
* 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<T> = Partial<T> & EnumValues<{ [K in keyof T]: Pick<T, K> }>;
export type AtLeastOne<T> = Partial<T> & EnumValues<{ [K in keyof T]: Pick<Required<T>, K> }>;

View File

@ -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<E>(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<TurnMove>, 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},

View File

@ -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);
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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

View File

@ -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]`

View File

@ -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

View File

@ -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}.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<AtLeastOne<fakeObj>>();
expectTypeOf<{ bar: string }>().toExtend<AtLeastOne<fakeObj>>();
expectTypeOf<{ baz: number | string }>().toExtend<AtLeastOne<fakeObj>>();
});
it("should convert to a partial intersected with the union of all individual single properties", () => {
expectTypeOf<AtLeastOne<fakeObj>>().branded.toEqualTypeOf<
Partial<fakeObj> & ({ foo: number } | { bar: string } | { baz: number | string })
>();
});
it("should treat optional properties as required", () => {
expectTypeOf<AtLeastOne<fakeObj>>().branded.toEqualTypeOf<AtLeastOne<optionalObj>>();
});
it("should not accept empty objects, even if optional properties are present", () => {
expectTypeOf<Record<string, never>>().not.toExtend<AtLeastOne<fakeObj>>();
expectTypeOf<Record<string, never>>().not.toExtend<AtLeastOne<optionalObj>>();
});
});