Cleaned up some more matchers

This commit is contained in:
Bertie690 2025-07-27 19:13:05 -04:00
parent 917fb596b4
commit 3a6026d637
6 changed files with 81 additions and 50 deletions

View File

@ -11,6 +11,7 @@ import type { TerrainType } from "#app/data/terrain";
import type { WeatherType } from "#enums/weather-type";
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
import { TurnMove } from "#types/turn-move";
import { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect-matcher";
declare module "vitest" {
interface Assertion {
@ -31,7 +32,7 @@ declare module "vitest" {
* @param expected - The expected types (in any order).
* @param options - The options passed to the matcher.
*/
toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void;
toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void;
/**
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
@ -64,9 +65,10 @@ declare module "vitest" {
/**
* Matcher to check if a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
* @param expectedStatusEffect - The expected {@linkcode StatusEffect}
* @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have,
* or a partially filled {@linkcode Status} containing the desired properties
*/
toHaveStatusEffect(expectedStatusEffect: StatusEffect): void;
toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void;
/**
* Matcher to check if the current {@linkcode WeatherType} is as expected.

View File

@ -5,7 +5,6 @@ import { toHaveEffectiveStatMatcher } from "#test/test-utils/matchers/to-have-ef
import { toHaveFaintedMatcher } from "#test/test-utils/matchers/to-have-fainted";
import { toHaveFullHpMatcher } from "#test/test-utils/matchers/to-have-full-hp";
import { toHaveHpMatcher } from "#test/test-utils/matchers/to-have-hp";
import { toHaveMoveResultMatcher } from "#test/test-utils/matchers/to-have-move-result-matcher";
import { toHaveStatStageMatcher } from "#test/test-utils/matchers/to-have-stat-stage-matcher";
import { toHaveStatusEffectMatcher } from "#test/test-utils/matchers/to-have-status-effect-matcher";
import { toHaveTakenDamageMatcher } from "#test/test-utils/matchers/to-have-taken-damage-matcher";
@ -23,7 +22,6 @@ import { expect } from "vitest";
expect.extend({
toEqualArrayUnsorted,
toHaveTypes,
toHaveMoveResult: toHaveMoveResultMatcher,
toHaveUsedMove: toHaveUsedMoveMatcher,
toHaveEffectiveStat: toHaveEffectiveStatMatcher,
toHaveTakenDamage: toHaveTakenDamageMatcher,

View File

@ -1,8 +1,43 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { PokemonType } from "#enums/pokemon-type";
import { describe, expect, it } from "vitest";
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("a", () => {
it("r", () => {
expect(1).toHaveTypes([PokemonType.FLYING]);
describe("Utils - Fff", () => {
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)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it("should do XYZ", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
expect(game.field.getPlayerPokemon()).toHaveTypes([PokemonType.WATER]);
expect.soft(game.field.getPlayerPokemon()).toHaveTypes([PokemonType.FLYING]);
expect.soft(game.field.getPlayerPokemon()).toHaveTypes([PokemonType.WATER]);
});
});

View File

@ -3,13 +3,12 @@ import type { Pokemon } from "#field/pokemon";
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
import { getPokemonNameWithAffix } from "#app/messages";
import type { Status } from "#data/status-effect";
import { StatusEffect } from "#enums/status-effect";
import { getEnumStr } from "#test/test-utils/string-utils";
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import type { NonFunctionPropertiesRecursive } from "#types/type-helpers";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
export type expectedType =
export type expectedStatusType =
| StatusEffect
| { effect: StatusEffect.TOXIC; toxicTurnCount: number }
| { effect: StatusEffect.SLEEP; sleepTurnsRemaining: number };
@ -24,7 +23,7 @@ export type expectedType =
export function toHaveStatusEffectMatcher(
this: MatcherState,
received: unknown,
expectedStatus: expectedType,
expectedStatus: expectedStatusType,
): SyncExpectationResult {
if (!isPokemonInstance(received)) {
return {
@ -33,19 +32,28 @@ export function toHaveStatusEffectMatcher(
};
}
// Convert to Status
const expStatus: { effect: StatusEffect } & Partial<NonFunctionPropertiesRecursive<Status>> =
typeof expectedStatus === "number"
? {
effect: expectedStatus,
}
: expectedStatus;
const pkmName = getPokemonNameWithAffix(received);
// If expected to have no status,
if (expStatus.effect === StatusEffect.NONE) {
k;
// Check exclusively effect equality
if (typeof expectedStatus === "number" || received.status?.effect !== expectedStatus.effect) {
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
const pass = this.equals(actualEffect, expectedStatus, [...this.customTesters, this.utils.iterableEquality]);
const actualStr = getEnumStr(StatusEffect, actualEffect, { prefix: "StatusEffect." });
const expectedStr = getEnumStr(StatusEffect, actualEffect, { prefix: "StatusEffect." });
return {
pass,
message: () =>
pass
? `Expected ${pkmName} NOT to 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)
const actualStatus = received.status;
const pass = this.equals(received, expectedStatus, [
...this.customTesters,
@ -53,13 +61,13 @@ export function toHaveStatusEffectMatcher(
this.utils.iterableEquality,
]);
const pkmName = getPokemonNameWithAffix(received);
return {
pass,
message: () =>
pass
? `Expected ${pkmName} NOT to have ${expectedStatusEffectStr}, but it did!`
: `Expected ${pkmName} to have status effect: ${expectedStatusEffectStr}, but got: ${actualStatusEffectStr}!`,
? `Expected ${pkmName}'s status NOT to match ${this.utils.stringify(expectedStatus)}, but it did!`
: `Expected ${pkmName}'s status to match ${this.utils.stringify(expectedStatus)}, but got ${this.utils.stringify(actualStatus)} instead!`,
expected: expectedStatus,
actual: actualStatus,
};
}

View File

@ -26,7 +26,7 @@ export interface toHaveTypesOptions {
export function toHaveTypes(
this: MatcherState,
received: unknown,
expected: unknown,
expected: [PokemonType, ...PokemonType[]],
options: toHaveTypesOptions = {},
): SyncExpectationResult {
if (!isPokemonInstance(received)) {
@ -36,32 +36,21 @@ export function toHaveTypes(
};
}
if (!Array.isArray(expected) || expected.length === 0) {
return {
pass: false,
message: () => `Expected to receive 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: false,
message: () => `Expected to receive array of PokemonTypes but got ${this.utils.stringify(expected)}!`,
};
}
const actualSorted = stringifyEnumArray(PokemonType, received.getTypes(...(options.args ?? [])).sort());
const expectedSorted = stringifyEnumArray(PokemonType, expected.slice().sort());
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
// 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(actualSorted, expectedSorted, matchers);
const pass = this.equals(actualStr, expectedStr, matchers);
return {
pass,
message: () =>
`Expected ${getPokemonNameWithAffix(received)} to have types ${this.utils.stringify(expectedSorted)}, but got ${actualSorted}!`,
actual: actualSorted,
expected: expectedSorted,
message: () => `Expected ${getPokemonNameWithAffix(received)} to have types ${expectedStr}, but got ${actualStr}!`,
actual: actualTypes,
expected: expectedTypes,
};
}

View File

@ -25,7 +25,7 @@ interface getEnumStrOptions {
* @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 suffix - An optional string to be prepended to the enum's string representation.
* @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
@ -38,7 +38,6 @@ interface getEnumStrOptions {
* console.log(getEnumStr(fakeEnum, fakeEnum.ONE)); // Output: "ONE (=1)"
* console.log(getEnumStr(fakeEnum, fakeEnum.TWO, {case: "Title", suffix: " Terrain"})); // Output: "Two Terrain (=2)"
* ```
*/
export function getEnumStr<E extends EnumOrObject>(
obj: E,