mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 07:29:30 +02:00
[Test] Port over + augment remaining test matchers from pkty (#6159)
* Partially ported over pkty matchers (WIP) * Cleaned up some more matchers * Fiexd up matchers * Fixed up remaining matchers * Removed the word "matcher" from the pkty matcher functions If we want them back we can always undo this commit and convert the other custom ones * Added wip spite test * Added `toHaveUsedPP` matcher * Fixed up docs and tests * Fixed spite test * Ran biome * Apply Biome * Reverted biome breaking i18next * Update src/typings/i18next.d.ts comment * Fixed log message to not be overly verbose * Added option to check for all PP used in pp matcher + cleaned up grudge tests * Fixed up tests * Fixed tests and such * Fix various TSDocs + missing TSDoc imports
This commit is contained in:
parent
acb1f4184b
commit
5ed9e152ab
@ -19,7 +19,6 @@
|
||||
// and having to verify whether each individual file is ignored
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/*.d.ts",
|
||||
"!**/dist/**/*",
|
||||
"!**/build/**/*",
|
||||
"!**/coverage/**/*",
|
||||
@ -180,7 +179,7 @@
|
||||
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes),
|
||||
// as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates).
|
||||
{
|
||||
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts"],
|
||||
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts", "**/*.d.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
|
10
global.d.ts
vendored
10
global.d.ts
vendored
@ -1,7 +1,6 @@
|
||||
import type { AnyFn } from "#types/type-helpers";
|
||||
import type { SetupServerApi } from "msw/node";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Only used in testing.
|
||||
@ -11,4 +10,11 @@ declare global {
|
||||
* To set up your own server in a test see `game-data.test.ts`
|
||||
*/
|
||||
var server: SetupServerApi;
|
||||
|
||||
// Overloads for `Function.apply` and `Function.call` to add type safety on matching argument types
|
||||
interface Function {
|
||||
apply<T extends AnyFn>(this: T, thisArg: ThisParameterType<T>, argArray: Parameters<T>): ReturnType<T>;
|
||||
|
||||
call<T extends AnyFn>(this: T, thisArg: ThisParameterType<T>, ...argArray: Parameters<T>): ReturnType<T>;
|
||||
}
|
||||
}
|
||||
|
@ -94,3 +94,12 @@ export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
||||
export type CoerceNullPropertiesToUndefined<T extends object> = {
|
||||
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type helper to mark all properties in `T` as optional, while still mandating that at least 1
|
||||
* of its properties be present.
|
||||
*
|
||||
* 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 extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
||||
|
@ -3560,6 +3560,7 @@ export class GrudgeTag extends SerializableBattlerTag {
|
||||
* @param sourcePokemon - The source of the move that fainted the tag's bearer
|
||||
* @returns `false` if Grudge activates its effect or lapses
|
||||
*/
|
||||
// TODO: Confirm whether this should interact with copying moves
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
|
||||
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
||||
|
1
src/typings/i18next.d.ts
vendored
1
src/typings/i18next.d.ts
vendored
@ -3,6 +3,7 @@ import type { TOptions } from "i18next";
|
||||
// Module declared to make referencing keys in the localization files type-safe.
|
||||
declare module "i18next" {
|
||||
interface TFunction {
|
||||
// biome-ignore lint/style/useShorthandFunctionType: This needs to be an interface due to interface merging
|
||||
(key: string | string[], options?: TOptions & Record<string, unknown>): string;
|
||||
}
|
||||
}
|
||||
|
42
src/typings/phaser/index.d.ts
vendored
42
src/typings/phaser/index.d.ts
vendored
@ -1,7 +1,7 @@
|
||||
import "phaser";
|
||||
|
||||
declare module "phaser" {
|
||||
namespace GameObjects {
|
||||
namespace GameObjects {
|
||||
interface GameObject {
|
||||
width: number;
|
||||
|
||||
@ -16,45 +16,45 @@ declare module "phaser" {
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Container {
|
||||
interface Container {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Sprite {
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Sprite {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Image {
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Image {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface NineSlice {
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface NineSlice {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Text {
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Text {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Rectangle {
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
interface Rectangle {
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
*/
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
}
|
||||
setPositionRelative(guideObject: any, x: number, y: number): this;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Input {
|
||||
namespace Input {
|
||||
namespace Gamepad {
|
||||
interface GamepadPlugin {
|
||||
/**
|
||||
|
18
src/vite.env.d.ts
vendored
18
src/vite.env.d.ts
vendored
@ -1,16 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_PORT?: string;
|
||||
readonly VITE_BYPASS_LOGIN?: string;
|
||||
readonly VITE_BYPASS_TUTORIAL?: string;
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
readonly VITE_SERVER_URL?: string;
|
||||
readonly VITE_DISCORD_CLIENT_ID?: string;
|
||||
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
||||
readonly VITE_I18N_DEBUG?: string;
|
||||
readonly VITE_PORT?: string;
|
||||
readonly VITE_BYPASS_LOGIN?: string;
|
||||
readonly VITE_BYPASS_TUTORIAL?: string;
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
readonly VITE_SERVER_URL?: string;
|
||||
readonly VITE_DISCORD_CLIENT_ID?: string;
|
||||
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
||||
readonly VITE_I18N_DEBUG?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
135
test/@types/vitest.d.ts
vendored
135
test/@types/vitest.d.ts
vendored
@ -1,26 +1,139 @@
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { TerrainType } from "#app/data/terrain";
|
||||
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 { 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 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 requires the array contain
|
||||
* _at least_ the listed items.
|
||||
* Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality
|
||||
* (as opposed to full equality).
|
||||
*
|
||||
* @param expected - The expected contents of the array, in any order.
|
||||
* @param expected - The expected contents of the array, in any order
|
||||
* @see {@linkcode expect.arrayContaining}
|
||||
*/
|
||||
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.
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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<TurnMove>, 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 - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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 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,
|
||||
* or a partially filled {@linkcode Status} containing the desired properties
|
||||
*/
|
||||
toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}.
|
||||
* @param expectedBattlerTagType - The expected {@linkcode BattlerTagType}
|
||||
*/
|
||||
toHaveBattlerTag(expectedBattlerTagType: BattlerTagType): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
||||
*/
|
||||
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
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 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.
|
||||
*/
|
||||
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,18 @@
|
||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||
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";
|
||||
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
||||
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";
|
||||
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";
|
||||
|
||||
/*
|
||||
@ -10,4 +23,17 @@ import { expect } from "vitest";
|
||||
expect.extend({
|
||||
toEqualArrayUnsorted,
|
||||
toHaveTypes,
|
||||
toHaveUsedMove,
|
||||
toHaveEffectiveStat,
|
||||
toHaveTakenDamage,
|
||||
toHaveWeather,
|
||||
toHaveTerrain,
|
||||
toHaveFullHp,
|
||||
toHaveStatusEffect,
|
||||
toHaveStatStage,
|
||||
toHaveBattlerTag,
|
||||
toHaveAbilityApplied,
|
||||
toHaveHp,
|
||||
toHaveFainted,
|
||||
toHaveUsedPP,
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -23,68 +24,64 @@ describe("Moves - Grudge", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.EMBER, MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.SHEDINJA)
|
||||
.enemyAbility(AbilityId.WONDER_GUARD)
|
||||
.enemyMoveset([MoveId.GRUDGE, MoveId.SPLASH]);
|
||||
.enemySpecies(SpeciesId.RATTATA)
|
||||
.startingLevel(100)
|
||||
.enemyAbility(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("should reduce the PP of the Pokemon's move to 0 when the user has fainted", async () => {
|
||||
it("should reduce the PP of an attack that faints the user to 0", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
game.move.select(MoveId.EMBER);
|
||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
const ratatta = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.GUILLOTINE);
|
||||
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
await game.phaseInterceptor.to("FaintPhase");
|
||||
|
||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.EMBER);
|
||||
|
||||
expect(playerMove?.getPpRatio()).toBe(0);
|
||||
// Ratatta should have fainted and consumed all of Guillotine's PP
|
||||
expect(ratatta).toHaveFainted();
|
||||
expect(feebas).toHaveUsedPP(MoveId.GUILLOTINE, "all");
|
||||
});
|
||||
|
||||
it("should remain in effect until the user's next move", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
const ratatta = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.EMBER);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
game.move.use(MoveId.GUILLOTINE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.EMBER);
|
||||
|
||||
expect(playerMove?.getPpRatio()).toBe(0);
|
||||
expect(ratatta).toHaveFainted();
|
||||
expect(feebas).toHaveUsedPP(MoveId.GUILLOTINE, "all");
|
||||
});
|
||||
|
||||
it("should not reduce the opponent's PP if the user dies to weather/indirect damage", async () => {
|
||||
it("should not reduce PP if the user dies to weather/indirect damage", async () => {
|
||||
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
|
||||
game.override
|
||||
.moveset([MoveId.FALSE_SWIPE])
|
||||
.startingLevel(100)
|
||||
.ability(AbilityId.SAND_STREAM)
|
||||
.enemySpecies(SpeciesId.RATTATA);
|
||||
await game.classicMode.startBattle([SpeciesId.GEODUDE]);
|
||||
game.override.weather(WeatherType.SANDSTORM);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
const ratatta = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.FALSE_SWIPE);
|
||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
||||
game.move.use(MoveId.FALSE_SWIPE);
|
||||
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
|
||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.FALSE_SWIPE);
|
||||
expect(playerMove?.getPpRatio()).toBeGreaterThan(0);
|
||||
expect(ratatta).toHaveFainted();
|
||||
expect(feebas).toHaveUsedPP(MoveId.FALSE_SWIPE, 1);
|
||||
});
|
||||
});
|
||||
|
126
test/moves/spite.test.ts
Normal file
126
test/moves/spite.test.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Spite", () => {
|
||||
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
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should reduce the PP of the target's last used 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.SPITE);
|
||||
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.selectEnemyMove(MoveId.SPLASH);
|
||||
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();
|
||||
game.move.changeMoveset(karp, [MoveId.SPLASH, MoveId.TACKLE]);
|
||||
|
||||
game.move.use(MoveId.SPITE);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should fail if the target's last used move is out of PP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
game.move.changeMoveset(karp, [MoveId.TACKLE]);
|
||||
karp.moveset[0].ppUsed = 0;
|
||||
|
||||
game.move.use(MoveId.SPITE);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should fail if the target's last used move is not in their moveset", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
game.move.changeMoveset(karp, [MoveId.TACKLE]);
|
||||
// Fake magikarp having used Splash the turn prior
|
||||
karp.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.ENEMY], useMode: MoveUseMode.NORMAL });
|
||||
|
||||
game.move.use(MoveId.SPITE);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should ignore virtual and Dancer-induced moves", async () => {
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.DANCER);
|
||||
game.move.forceMetronomeMove(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const [karp1, karp2] = game.scene.getEnemyField();
|
||||
game.move.changeMoveset(karp1, [MoveId.SPLASH, MoveId.METRONOME, MoveId.SWORDS_DANCE]);
|
||||
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();
|
||||
|
||||
// Spite ignored virtual splash and swords dance, instead only docking from metronome
|
||||
expect(karp1).toHaveUsedPP(MoveId.SPLASH, 0);
|
||||
expect(karp1).toHaveUsedPP(MoveId.SWORDS_DANCE, 0);
|
||||
expect(karp1).toHaveUsedPP(MoveId.METRONOME, 5);
|
||||
});
|
||||
});
|
@ -1,43 +1,47 @@
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* Matcher to check if an array contains exactly the given items, disregarding order.
|
||||
* @param received - The object to check. Should be an array of elements.
|
||||
* @returns The result of the matching
|
||||
* 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
|
||||
*/
|
||||
export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expected: unknown): SyncExpectationResult {
|
||||
export function toEqualArrayUnsorted(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expected: unknown[],
|
||||
): SyncExpectationResult {
|
||||
if (!Array.isArray(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
pass: false,
|
||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.isArray(expected)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve an array, but got ${this.utils.stringify(expected)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (received.length !== expected.length) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve array of length ${received.length}, but got ${expected.length}!`,
|
||||
pass: false,
|
||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
||||
actual: received,
|
||||
expected,
|
||||
};
|
||||
}
|
||||
|
||||
const gotSorted = received.slice().sort();
|
||||
const wantSorted = expected.slice().sort();
|
||||
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||
const actualSorted = received.slice().sort();
|
||||
const expectedSorted = expected.slice().sort();
|
||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||
|
||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedSorted);
|
||||
|
||||
return {
|
||||
pass: this.isNot !== pass,
|
||||
pass,
|
||||
message: () =>
|
||||
`Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`,
|
||||
actual: gotSorted,
|
||||
expected: wantSorted,
|
||||
pass
|
||||
? `Expected ${actualStr} to NOT exactly equal ${expectedStr} without order, but it did!`
|
||||
: `Expected ${actualStr} to exactly equal ${expectedStr} without order, but it didn't!`,
|
||||
expected: expectedSorted,
|
||||
actual: actualSorted,
|
||||
};
|
||||
}
|
||||
|
43
test/test-utils/matchers/to-have-ability-applied.ts
Normal file
43
test/test-utils/matchers/to-have-ability-applied.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toHaveAbilityApplied(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedAbilityId: AbilityId,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pass = received.waveData.abilitiesApplied.has(expectedAbilityId);
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const expectedAbilityStr = getEnumStr(AbilityId, expectedAbilityId);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have applied ${expectedAbilityStr}, but it did!`
|
||||
: `Expected ${pkmName} to have applied ${expectedAbilityStr}, but it didn't!`,
|
||||
expected: expectedAbilityId,
|
||||
actual: received.waveData.abilitiesApplied,
|
||||
};
|
||||
}
|
43
test/test-utils/matchers/to-have-battler-tag.ts
Normal file
43
test/test-utils/matchers/to-have-battler-tag.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toHaveBattlerTag(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedBattlerTagType: BattlerTagType,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pass = !!received.getTag(expectedBattlerTagType);
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
// "BattlerTagType.SEEDED (=1)"
|
||||
const expectedTagStr = getEnumStr(BattlerTagType, expectedBattlerTagType, { prefix: "BattlerTagType." });
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have ${expectedTagStr}, but it did!`
|
||||
: `Expected ${pkmName} to have ${expectedTagStr}, but it didn't!`,
|
||||
expected: expectedBattlerTagType,
|
||||
actual: received.summonData.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
66
test/test-utils/matchers/to-have-effective-stat.ts
Normal file
66
test/test-utils/matchers/to-have-effective-stat.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { EffectiveStat } from "#enums/stat";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { Move } from "#moves/move";
|
||||
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 {
|
||||
/**
|
||||
* The target {@linkcode Pokemon}
|
||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||
*/
|
||||
enemy?: Pokemon;
|
||||
/**
|
||||
* The {@linkcode Move} being used
|
||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||
*/
|
||||
move?: Move;
|
||||
/**
|
||||
* Whether a critical hit occurred or not
|
||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
isCritical?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveEffectiveStat(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
stat: EffectiveStat,
|
||||
expectedValue: number,
|
||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Change once getEffectiveStat is refactored to take an object literal
|
||||
const actualValue = received.getEffectiveStat(stat, enemy, move, undefined, undefined, undefined, isCritical);
|
||||
const pass = actualValue === expectedValue;
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const statName = getStatName(stat);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have ${expectedValue} ${statName}, but it did!`
|
||||
: `Expected ${pkmName} to have ${expectedValue} ${statName}, but got ${actualValue} instead!`,
|
||||
expected: expectedValue,
|
||||
actual: actualValue,
|
||||
};
|
||||
}
|
35
test/test-utils/matchers/to-have-fainted.ts
Normal file
35
test/test-utils/matchers/to-have-fainted.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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 that checks if a {@linkcode Pokemon} has fainted.
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pass = received.isFainted();
|
||||
|
||||
const hp = received.hp;
|
||||
const maxHp = received.getMaxHp();
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have fainted, but it did!`
|
||||
: `Expected ${pkmName} to have fainted, but it didn't! (${hp}/${maxHp} HP)`,
|
||||
expected: 0,
|
||||
actual: hp,
|
||||
};
|
||||
}
|
35
test/test-utils/matchers/to-have-full-hp.ts
Normal file
35
test/test-utils/matchers/to-have-full-hp.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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 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
|
||||
*/
|
||||
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pass = received.isFullHp();
|
||||
|
||||
const hp = received.hp;
|
||||
const maxHp = received.getMaxHp();
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have full hp, but it did!`
|
||||
: `Expected ${pkmName} to have full hp, but it didn't! (${hp}/${maxHp} HP)`,
|
||||
expected: maxHp,
|
||||
actual: hp,
|
||||
};
|
||||
}
|
35
test/test-utils/matchers/to-have-hp.ts
Normal file
35
test/test-utils/matchers/to-have-hp.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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 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
|
||||
*/
|
||||
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actualHp = received.hp;
|
||||
const pass = actualHp === expectedHp;
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have ${expectedHp} HP, but it did!`
|
||||
: `Expected ${pkmName} to have ${expectedHp} HP, but got ${actualHp} HP instead!`,
|
||||
expected: expectedHp,
|
||||
actual: actualHp,
|
||||
};
|
||||
}
|
53
test/test-utils/matchers/to-have-stat-stage.ts
Normal file
53
test/test-utils/matchers/to-have-stat-stage.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { BattleStat } from "#enums/stat";
|
||||
import { getStatName } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* Matcher that checks if a Pokemon has a specific {@linkcode BattleStat | Stat} stage.
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}.
|
||||
* @param stat - The {@linkcode BattleStat | Stat} to check
|
||||
* @param expectedStage - The expected numerical value of {@linkcode stat}; should be within the range `[-6, 6]`
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveStatStage(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
stat: BattleStat,
|
||||
expectedStage: number,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedStage < -6 || expectedStage > 6) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actualStage = received.getStatStage(stat);
|
||||
const pass = actualStage === expectedStage;
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const statName = getStatName(stat);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName}'s ${statName} stat stage to NOT be ${expectedStage}, but it was!`
|
||||
: `Expected ${pkmName}'s ${statName} stat stage to be ${expectedStage}, but got ${actualStage} instead!`,
|
||||
expected: expectedStage,
|
||||
actual: actualStage,
|
||||
};
|
||||
}
|
83
test/test-utils/matchers/to-have-status-effect.ts
Normal file
83
test/test-utils/matchers/to-have-status-effect.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { Status } from "#data/status-effect";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { getEnumStr, getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
export type expectedStatusType =
|
||||
| StatusEffect
|
||||
| { effect: StatusEffect.TOXIC; toxicTurnCount: number }
|
||||
| { effect: StatusEffect.SLEEP; sleepTurnsRemaining: number };
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveStatusEffect(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedStatus: expectedStatusType,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||
|
||||
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.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<typeof expectedStatus, StatusEffect>).effect;
|
||||
}
|
||||
|
||||
if (typeof expectedStatus === "number") {
|
||||
const pass = this.equals(actualEffect, expectedStatus, [...this.customTesters, this.utils.iterableEquality]);
|
||||
|
||||
const actualStr = getEnumStr(StatusEffect, actualEffect, { prefix: "StatusEffect." });
|
||||
const expectedStr = getEnumStr(StatusEffect, expectedStatus, { prefix: "StatusEffect." });
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have ${expectedStr}, but it did!`
|
||||
: `Expected ${pkmName} to have status effect ${expectedStr}, but got ${actualStr} instead!`,
|
||||
expected: expectedStatus,
|
||||
actual: actualEffect,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for equality of all fields (for toxic turn count/etc)
|
||||
const actualStatus = received.status;
|
||||
const pass = this.equals(received, expectedStatus, [
|
||||
...this.customTesters,
|
||||
this.utils.subsetEquality,
|
||||
this.utils.iterableEquality,
|
||||
]);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedStatus);
|
||||
const actualStr = getOnelineDiffStr.call(this, actualStatus);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName}'s status to NOT match ${expectedStr}, but it did!`
|
||||
: `Expected ${pkmName}'s status to match ${expectedStr}, but got ${actualStr} instead!`,
|
||||
expected: expectedStatus,
|
||||
actual: actualStatus,
|
||||
};
|
||||
}
|
46
test/test-utils/matchers/to-have-taken-damage.ts
Normal file
46
test/test-utils/matchers/to-have-taken-damage.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import { toDmgValue } from "#utils/common";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* @param expectedDamageTaken - The expected amount of damage the {@linkcode Pokemon} has taken
|
||||
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveTakenDamage(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedDamageTaken: number,
|
||||
roundDown = true,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const expectedDmgValue = roundDown ? toDmgValue(expectedDamageTaken) : expectedDamageTaken;
|
||||
const actualDmgValue = received.getInverseHp();
|
||||
const pass = actualDmgValue === expectedDmgValue;
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have taken ${expectedDmgValue} damage, but it did!`
|
||||
: `Expected ${pkmName} to have taken ${expectedDmgValue} damage, but got ${actualDmgValue} instead!`,
|
||||
expected: expectedDmgValue,
|
||||
actual: actualDmgValue,
|
||||
};
|
||||
}
|
62
test/test-utils/matchers/to-have-terrain.ts
Normal file
62
test/test-utils/matchers/to-have-terrain.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toHaveTerrain(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedTerrainType: TerrainType,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actual = received.scene.arena.getTerrainType();
|
||||
const pass = actual === expectedTerrainType;
|
||||
const actualStr = toTerrainStr(actual);
|
||||
const expectedStr = toTerrainStr(expectedTerrainType);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||
expected: expectedTerrainType,
|
||||
actual,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human readable string of the current {@linkcode TerrainType}.
|
||||
* @param terrainType - The {@linkcode TerrainType} to transform
|
||||
* @returns A human readable string
|
||||
*/
|
||||
function toTerrainStr(terrainType: TerrainType) {
|
||||
if (terrainType === TerrainType.NONE) {
|
||||
return "no terrain";
|
||||
}
|
||||
// "Electric Terrain (=2)"
|
||||
return getEnumStr(TerrainType, terrainType, { casing: "Title", suffix: " Terrain" });
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Pokemon } from "#field/pokemon";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { stringifyEnumArray } from "#test/test-utils/string-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
import { isPokemonInstance, receivedStr } from "../test-utils";
|
||||
|
||||
export interface toHaveTypesOptions {
|
||||
/**
|
||||
@ -15,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
|
||||
@ -23,42 +26,36 @@ export interface toHaveTypesOptions {
|
||||
export function toHaveTypes(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expected: unknown,
|
||||
expected: [PokemonType, ...PokemonType[]],
|
||||
options: toHaveTypesOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!(received instanceof Pokemon)) {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected a Pokemon, but got ${this.utils.stringify(received)}!`,
|
||||
pass: false,
|
||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.isArray(expected) || expected.length === 0) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve an array with length >=1, but got ${this.utils.stringify(expected)}!`,
|
||||
};
|
||||
}
|
||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
||||
const expectedTypes = expected.slice().sort();
|
||||
|
||||
if (!expected.every((t): t is PokemonType => t in PokemonType)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve array of PokemonTypes but got ${this.utils.stringify(expected)}!`,
|
||||
};
|
||||
}
|
||||
// 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 pass = this.equals(actualTypes, expectedTypes, matchers);
|
||||
|
||||
const gotSorted = pkmnTypeToStr(received.getTypes(...(options.args ?? [])));
|
||||
const wantSorted = pkmnTypeToStr(expected.slice());
|
||||
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
pass: this.isNot !== pass,
|
||||
message: () => `Expected ${received.name} to have types ${this.utils.stringify(wantSorted)}, but got ${gotSorted}!`,
|
||||
actual: gotSorted,
|
||||
expected: wantSorted,
|
||||
pass,
|
||||
message: () =>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function pkmnTypeToStr(p: PokemonType[]): string[] {
|
||||
return p.sort().map(type => PokemonType[type]);
|
||||
}
|
||||
|
70
test/test-utils/matchers/to-have-used-move.ts
Normal file
70
test/test-utils/matchers/to-have-used-move.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import { getOnelineDiffStr, getOrdinal } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import type { AtLeastOne } from "#types/type-helpers";
|
||||
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,
|
||||
* 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)
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveUsedMove(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
||||
index = 0,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const move: TurnMove | undefined = received.getLastXMoves(-1)[index];
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
if (move === undefined) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||
actual: received.getLastXMoves(-1),
|
||||
};
|
||||
}
|
||||
|
||||
// Coerce to a `TurnMove`
|
||||
if (typeof expectedResult === "number") {
|
||||
expectedResult = { move: expectedResult };
|
||||
}
|
||||
|
||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||
|
||||
const pass = this.equals(move, expectedResult, [
|
||||
...this.customTesters,
|
||||
this.utils.subsetEquality,
|
||||
this.utils.iterableEquality,
|
||||
]);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
||||
return {
|
||||
pass,
|
||||
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: expectedResult,
|
||||
actual: move,
|
||||
};
|
||||
}
|
77
test/test-utils/matchers/to-have-used-pp.ts
Normal file
77
test/test-utils/matchers/to-have-used-pp.ts
Normal file
@ -0,0 +1,77 @@
|
||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
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 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
|
||||
* @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 | "all",
|
||||
): 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!`,
|
||||
expected: expectedMove,
|
||||
actual: received.getMoveset(),
|
||||
};
|
||||
}
|
||||
|
||||
const move = movesetMoves[0]; // will be the only move in the array
|
||||
|
||||
let ppStr: string = ppUsed.toString();
|
||||
if (typeof ppUsed === "string") {
|
||||
ppStr = "all its";
|
||||
ppUsed = move.getMovePp();
|
||||
}
|
||||
const pass = move.ppUsed === ppUsed;
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName}'s ${moveStr} to NOT have used ${ppStr} PP, but it did!`
|
||||
: `Expected ${pkmName}'s ${moveStr} to have used ${ppStr} PP, but got ${move.ppUsed} instead!`,
|
||||
expected: ppUsed,
|
||||
actual: move.ppUsed,
|
||||
};
|
||||
}
|
62
test/test-utils/matchers/to-have-weather.ts
Normal file
62
test/test-utils/matchers/to-have-weather.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toHaveWeather(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedWeatherType: WeatherType,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actual = received.scene.arena.getWeatherType();
|
||||
const pass = actual === expectedWeatherType;
|
||||
const actualStr = toWeatherStr(actual);
|
||||
const expectedStr = toWeatherStr(expectedWeatherType);
|
||||
|
||||
return {
|
||||
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: expectedWeatherType,
|
||||
actual,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human readable representation of the current {@linkcode WeatherType}.
|
||||
* @param weatherType - The {@linkcode WeatherType} to transform
|
||||
* @returns A human readable string
|
||||
*/
|
||||
function toWeatherStr(weatherType: WeatherType) {
|
||||
if (weatherType === WeatherType.NONE) {
|
||||
return "no weather";
|
||||
}
|
||||
|
||||
return toTitleCase(WeatherType[weatherType]);
|
||||
}
|
183
test/test-utils/string-utils.ts
Normal file
183
test/test-utils/string-utils.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { getStatKey, type Stat } from "#enums/stat";
|
||||
import type { EnumOrObject, NormalEnum, TSNumericEnum } from "#types/enum-types";
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
import { enumValueToKey } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import type { MatcherState } from "@vitest/expect";
|
||||
import i18next from "i18next";
|
||||
|
||||
type Casing = "Preserve" | "Title";
|
||||
|
||||
interface getEnumStrOptions {
|
||||
/**
|
||||
* A string denoting the casing method to use.
|
||||
* @defaultValue "Preserve"
|
||||
*/
|
||||
casing?: Casing;
|
||||
/**
|
||||
* If present, will be prepended to the beginning of the enum string.
|
||||
*/
|
||||
prefix?: string;
|
||||
/**
|
||||
* If present, will be added to the end of the enum string.
|
||||
*/
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of an enum member or const object value, alongside its corresponding value.
|
||||
* @param obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||
* @param enums - One of {@linkcode obj}'s values
|
||||
* @param casing - A string denoting the casing method to use; default `Preserve`
|
||||
* @param prefix - An optional string to be prepended to the enum's string representation
|
||||
* @param suffix - An optional string to be appended to the enum's string representation
|
||||
* @returns The stringified representation of `val` as dictated by the options.
|
||||
* @example
|
||||
* ```ts
|
||||
* enum fakeEnum {
|
||||
* 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)"
|
||||
* ```
|
||||
*/
|
||||
export function getEnumStr<E extends EnumOrObject>(
|
||||
obj: E,
|
||||
val: ObjectValues<E>,
|
||||
{ casing = "Preserve", prefix = "", suffix = "" }: getEnumStrOptions = {},
|
||||
): string {
|
||||
let casingFunc: ((s: string) => string) | undefined;
|
||||
switch (casing) {
|
||||
case "Preserve":
|
||||
break;
|
||||
case "Title":
|
||||
casingFunc = toTitleCase;
|
||||
break;
|
||||
}
|
||||
|
||||
let stringPart =
|
||||
obj[val] !== undefined
|
||||
? // TS reverse mapped enum
|
||||
(obj[val] as string)
|
||||
: // Normal enum/`const object`
|
||||
(enumValueToKey(obj as NormalEnum<E>, val) as string);
|
||||
|
||||
if (casingFunc) {
|
||||
stringPart = casingFunc(stringPart);
|
||||
}
|
||||
|
||||
return `${prefix}${stringPart}${suffix} (=${val})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of enums or `const object`s into a readable string version.
|
||||
* @param obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||
* @param enums - An array of {@linkcode obj}'s values
|
||||
* @returns The stringified representation of `enums`.
|
||||
* @example
|
||||
* ```ts
|
||||
* enum fakeEnum {
|
||||
* ONE: 1,
|
||||
* TWO: 2,
|
||||
* THREE: 3,
|
||||
* }
|
||||
* console.log(stringifyEnumArray(fakeEnum, [fakeEnum.ONE, fakeEnum.TWO, fakeEnum.THREE])); // Output: "[ONE, TWO, THREE] (=[1, 2, 3])"
|
||||
* ```
|
||||
*/
|
||||
export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyof E][]): string {
|
||||
if (obj.length === 0) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
const vals = enums.slice();
|
||||
/** An array of string names */
|
||||
let names: string[];
|
||||
|
||||
if (obj[enums[0]] !== undefined) {
|
||||
// Reverse mapping exists - `obj` is a `TSNumericEnum` and its reverse mapped counterparts are strings
|
||||
names = enums.map(e => (obj as TSNumericEnum<E>)[e] as string);
|
||||
} else {
|
||||
// No reverse mapping exists means `obj` is a `NormalEnum`.
|
||||
// NB: This (while ugly) should be more ergonomic than doing a repeated lookup for large `const object`s
|
||||
// as the `enums` array should be significantly shorter than the corresponding enum type.
|
||||
names = [];
|
||||
for (const [k, v] of Object.entries(obj as NormalEnum<E>)) {
|
||||
if (names.length === enums.length) {
|
||||
// No more names to get
|
||||
break;
|
||||
}
|
||||
// Find all matches for the given enum, assigning their keys to the names array
|
||||
findIndices(enums, v).forEach(matchIndex => {
|
||||
names[matchIndex] = k;
|
||||
});
|
||||
}
|
||||
}
|
||||
return `[${names.join(", ")}] (=[${vals.join(", ")}])`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the indices of all occurrences of a value in an array.
|
||||
* @param arr - The array to search
|
||||
* @param searchElement - The value to locate in the array
|
||||
* @param fromIndex - The array index at which to begin the search. If fromIndex is omitted, the
|
||||
* search starts at index 0
|
||||
*/
|
||||
function findIndices<T>(arr: T[], searchElement: T, fromIndex = 0): number[] {
|
||||
const indices: number[] = [];
|
||||
const arrSliced = arr.slice(fromIndex);
|
||||
for (const [index, value] of arrSliced.entries()) {
|
||||
if (value === searchElement) {
|
||||
indices.push(index);
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number into an English ordinal
|
||||
* @param num - The number to convert into an ordinal
|
||||
* @returns The ordinal representation of {@linkcode num}.
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(getOrdinal(1)); // Output: "1st"
|
||||
* console.log(getOrdinal(12)); // Output: "12th"
|
||||
* console.log(getOrdinal(24)); // Output: "24th"
|
||||
* ```
|
||||
*/
|
||||
export function getOrdinal(num: number): string {
|
||||
const tens = num % 10;
|
||||
const hundreds = num % 100;
|
||||
if (tens === 1 && hundreds !== 11) {
|
||||
return num + "st";
|
||||
}
|
||||
if (tens === 2 && hundreds !== 12) {
|
||||
return num + "nd";
|
||||
}
|
||||
if (tens === 3 && hundreds !== 13) {
|
||||
return num + "rd";
|
||||
}
|
||||
return num + "th";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized name of a {@linkcode Stat}.
|
||||
* @param s - The {@linkcode Stat} to check
|
||||
* @returns - The proper name for s, retrieved from the translations.
|
||||
*/
|
||||
export function getStatName(s: Stat): string {
|
||||
return i18next.t(getStatKey(s));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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}");
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { Pokemon } from "#field/pokemon";
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next, { type ParseKeys } from "i18next";
|
||||
import { vi } from "vitest";
|
||||
|
||||
@ -29,3 +31,54 @@ export function arrayOfRange(start: number, end: number) {
|
||||
export function getApiBaseUrl() {
|
||||
return import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001";
|
||||
}
|
||||
|
||||
type TypeOfResult = "undefined" | "object" | "boolean" | "number" | "bigint" | "string" | "symbol" | "function";
|
||||
|
||||
/**
|
||||
* Helper to determine the actual type of the received object as human readable string
|
||||
* @param received - The received object
|
||||
* @returns A human readable string of the received object (type)
|
||||
*/
|
||||
export function receivedStr(received: unknown, expectedType: TypeOfResult = "object"): string {
|
||||
if (received === null) {
|
||||
return "null";
|
||||
}
|
||||
if (received === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
if (typeof received !== expectedType) {
|
||||
return typeof received;
|
||||
}
|
||||
if (expectedType === "object") {
|
||||
return received.constructor.name;
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if the received object is an {@linkcode object}
|
||||
* @param received - The object to check
|
||||
* @returns Whether the object is an {@linkcode object}.
|
||||
*/
|
||||
function isObject(received: unknown): received is object {
|
||||
return received !== null && typeof received === "object";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if a given object is a {@linkcode Pokemon}.
|
||||
* @param received - The object to check
|
||||
* @return Whether `received` is a {@linkcode Pokemon} instance.
|
||||
*/
|
||||
export function isPokemonInstance(received: unknown): received is Pokemon {
|
||||
return isObject(received) && received instanceof Pokemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an object is a {@linkcode GameManager} instance
|
||||
* @param received - The object to check
|
||||
* @returns Whether the object is a {@linkcode GameManager} instance.
|
||||
*/
|
||||
export function isGameManagerInstance(received: unknown): received is GameManager {
|
||||
return isObject(received) && (received as GameManager).constructor.name === "GameManager";
|
||||
}
|
||||
|
38
test/types/type-helpers.test-d.ts
Normal file
38
test/types/type-helpers.test-d.ts
Normal 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 intersection 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>>();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user