Fixed up remaining matchers

This commit is contained in:
Bertie690 2025-07-27 21:14:45 -04:00
parent a0de157246
commit 83392d7e86
15 changed files with 74 additions and 47 deletions

View File

@ -75,3 +75,12 @@ export type NonFunctionPropertiesRecursive<Class> = {
}; };
export type AbstractConstructor<T> = abstract new (...args: any[]) => T; export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
/**
* Type helper to mark all properties in `T` 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> = Partial<T> & EnumValues<{ [K in keyof T]: Pick<T, K> }>;

View File

@ -11,6 +11,7 @@ import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matcher
import { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect-matcher"; import { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect-matcher";
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
import { TurnMove } from "#types/turn-move"; import { TurnMove } from "#types/turn-move";
import { AtLeastOne } from "#types/type-helpers";
import type { expect } from "vitest"; import type { expect } from "vitest";
declare module "vitest" { declare module "vitest" {
@ -42,7 +43,7 @@ declare module "vitest" {
* Default `0` (last used move) * Default `0` (last used move)
* @see {@linkcode Pokemon.getLastXMoves} * @see {@linkcode Pokemon.getLastXMoves}
*/ */
toHaveUsedMove(expected: MoveId | Partial<TurnMove>, index?: number): void; toHaveUsedMove(expected: MoveId | AtLeastOne<TurnMove>, index?: number): void;
/** /**
* Matcher to check if a {@linkcode Pokemon Pokemon's} effective stat is as expected * Matcher to check if a {@linkcode Pokemon Pokemon's} effective stat is as expected

View File

@ -6,7 +6,11 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
* @param expected - The array to check equality with * @param expected - The array to check equality with
* @returns Whether the matcher passed * @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)) { if (!Array.isArray(received)) {
return { return {
pass: false, pass: false,
@ -14,22 +18,16 @@ export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expe
}; };
} }
if (!Array.isArray(expected)) {
return {
pass: false,
message: () => `Expected to receive an array, but got ${this.utils.stringify(expected)}!`,
};
}
if (received.length !== expected.length) { if (received.length !== expected.length) {
return { return {
pass: false, pass: false,
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length}!`, message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
actual: received, actual: received,
expected, expected,
}; };
} }
// Create shallow copies of the arrays in case we have
const gotSorted = received.slice().sort(); const gotSorted = received.slice().sort();
const wantSorted = expected.slice().sort(); const wantSorted = expected.slice().sort();
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]); const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
@ -37,8 +35,10 @@ export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expe
return { return {
pass, pass,
message: () => message: () =>
`Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`, pass
actual: gotSorted, ? `Expected ${this.utils.stringify(received)} to NOT exactly equal ${this.utils.stringify(expected)} without order!`
: `Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`,
expected: wantSorted, expected: wantSorted,
actual: gotSorted,
}; };
} }

View File

@ -4,6 +4,7 @@ import type { Pokemon } from "#field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { getEnumStr } from "#test/test-utils/string-utils";
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
@ -28,14 +29,14 @@ export function toHaveAbilityAppliedMatcher(
const pass = received.waveData.abilitiesApplied.has(expectedAbilityId); const pass = received.waveData.abilitiesApplied.has(expectedAbilityId);
const pkmName = getPokemonNameWithAffix(received); const pkmName = getPokemonNameWithAffix(received);
const expectedAbilityStr = `${AbilityId[expectedAbilityId]} (=${expectedAbilityId})`; const expectedAbilityStr = getEnumStr(AbilityId, expectedAbilityId);
return { return {
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName} to NOT have applied ${expectedAbilityStr}, but it did!` ? `Expected ${pkmName} to NOT have applied ${expectedAbilityStr}, but it did!`
: `Expected ${pkmName} to have applied ${expectedAbilityStr}, but it did not!`, : `Expected ${pkmName} to have applied ${expectedAbilityStr}, but it didn't!`,
expected: expectedAbilityId, expected: expectedAbilityId,
actual: received.waveData.abilitiesApplied, actual: received.waveData.abilitiesApplied,
}; };

View File

@ -36,7 +36,7 @@ export function toHaveBattlerTag(
message: () => message: () =>
pass pass
? `Expected ${pkmName} to NOT have BattlerTagType.${expectedTagStr}, but it did!` ? `Expected ${pkmName} to NOT have BattlerTagType.${expectedTagStr}, but it did!`
: `Expected ${pkmName} to have BattlerTagType.${expectedTagStr}, but it did not!`, : `Expected ${pkmName} to have BattlerTagType.${expectedTagStr}, but it didn't!`,
expected: expectedBattlerTagType, expected: expectedBattlerTagType,
actual: received.summonData.tags.map(t => t.tagType), actual: received.summonData.tags.map(t => t.tagType),
}; };

View File

@ -15,17 +15,17 @@ export function toHaveFaintedMatcher(this: MatcherState, received: unknown): Syn
}; };
} }
const { hp } = received;
const maxHp = received.getMaxHp();
const pass = received.isFainted(); const pass = received.isFainted();
const hp = received.hp;
const maxHp = received.getMaxHp();
const pkmName = getPokemonNameWithAffix(received); const pkmName = getPokemonNameWithAffix(received);
return { return {
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName} NOT to have fainted, but it did! (${hp}/${maxHp} HP)` ? `Expected ${pkmName} to NOT have fainted, but it did!`
: `Expected ${pkmName} to have fainted, but it did not. (${hp}/${maxHp} HP)`, : `Expected ${pkmName} to have fainted, but it didn't! (${hp}/${maxHp} HP)`,
}; };
} }

View File

@ -15,16 +15,17 @@ export function toHaveFullHpMatcher(this: MatcherState, received: unknown): Sync
}; };
} }
const pass = received.isFullHp() === true; const pass = received.isFullHp();
const ofHpStr = `${received.getInverseHp()}/${received.getMaxHp()} HP`; const hp = received.hp;
const maxHp = received.getMaxHp();
const pkmName = getPokemonNameWithAffix(received); const pkmName = getPokemonNameWithAffix(received);
return { return {
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName} to NOT have full hp (${ofHpStr}), but it did!` ? `Expected ${pkmName} to NOT have full hp, but it did!`
: `Expected ${pkmName} to have full hp, but found ${ofHpStr}.`, : `Expected ${pkmName} to have full hp, but it didn't! (${hp}/${maxHp} HP)`,
}; };
} }

View File

@ -1,4 +1,6 @@
import { getPokemonNameWithAffix } from "#app/messages"; 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 { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
@ -20,13 +22,14 @@ export function toHaveHpMatcher(this: MatcherState, received: unknown, expectedH
const pass = actualHp === expectedHp; const pass = actualHp === expectedHp;
const pkmName = getPokemonNameWithAffix(received); const pkmName = getPokemonNameWithAffix(received);
const maxHp = received.getMaxHp();
return { return {
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName} to NOT have ${expectedHp}/${maxHp} HP, but it did!` ? `Expected ${pkmName} to NOT have ${expectedHp} HP, but it did!`
: `Expected ${pkmName} to have ${expectedHp}/${maxHp} HP, but found ${actualHp}/${maxHp} HP.`, : `Expected ${pkmName} to have ${expectedHp} HP, but got ${actualHp} HP instead!`,
expected: expectedHp,
actual: actualHp,
}; };
} }

View File

@ -1,5 +1,6 @@
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { type BattleStat, Stat } from "#enums/stat"; import type { BattleStat } from "#enums/stat";
import { getStatName } from "#test/test-utils/string-utils";
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
@ -34,7 +35,7 @@ export function toHaveStatStageMatcher(
const pass = actualStage === expectedStage; const pass = actualStage === expectedStage;
const pkmName = getPokemonNameWithAffix(received); const pkmName = getPokemonNameWithAffix(received);
const statName = Stat[stat]; const statName = getStatName(stat);
return { return {
pass, pass,

View File

@ -46,7 +46,7 @@ export function toHaveStatusEffectMatcher(
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName} NOT to have ${expectedStr}, but it did!` ? `Expected ${pkmName} to NOT have ${expectedStr}, but it did!`
: `Expected ${pkmName} to have status effect ${expectedStr}, but got ${actualStr} instead!`, : `Expected ${pkmName} to have status effect ${expectedStr}, but got ${actualStr} instead!`,
expected: expectedStatus, expected: expectedStatus,
actual: actualEffect, actual: actualEffect,
@ -65,7 +65,7 @@ export function toHaveStatusEffectMatcher(
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName}'s status NOT to match ${this.utils.stringify(expectedStatus)}, but it did!` ? `Expected ${pkmName}'s status to NOT 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 ${pkmName}'s status to match ${this.utils.stringify(expectedStatus)}, but got ${this.utils.stringify(actualStatus)} instead!`,
expected: expectedStatus, expected: expectedStatus,
actual: actualStatus, actual: actualStatus,

View File

@ -38,7 +38,7 @@ export function toHaveTerrainMatcher(
message: () => message: () =>
pass pass
? `Expected Arena to NOT have ${expectedStr} active, but it did!` ? `Expected Arena to NOT have ${expectedStr} active, but it did!`
: `Expected Arena to have ${expectedStr} active, but got ${actualStr}!`, : `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
actual, actual,
expected: expectedTerrainType, expected: expectedTerrainType,
}; };

View File

@ -39,17 +39,22 @@ export function toHaveTypes(
const actualTypes = received.getTypes(...(options.args ?? [])).sort(); const actualTypes = received.getTypes(...(options.args ?? [])).sort();
const expectedTypes = expected.slice().sort(); const expectedTypes = expected.slice().sort();
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
// Exact matches do not care about subset equality // Exact matches do not care about subset equality
const matchers = options.exact const matchers = options.exact
? [...this.customTesters, this.utils.iterableEquality] ? [...this.customTesters, this.utils.iterableEquality]
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
const pass = this.equals(actualStr, expectedStr, matchers); const pass = this.equals(actualTypes, expectedTypes, matchers);
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
const pkmName = getPokemonNameWithAffix(received);
return { return {
pass, pass,
message: () => `Expected ${getPokemonNameWithAffix(received)} to have types ${expectedStr}, but got ${actualStr}!`, message: () =>
pass
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
actual: actualTypes, actual: actualTypes,
expected: expectedTypes, expected: expectedTypes,
}; };

View File

@ -5,12 +5,14 @@ import type { Pokemon } from "#field/pokemon";
import { getOrdinal } from "#test/test-utils/string-utils"; import { getOrdinal } from "#test/test-utils/string-utils";
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import type { TurnMove } from "#types/turn-move"; import type { TurnMove } from "#types/turn-move";
import type { AtLeastOne } from "#types/type-helpers";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/** /**
* Matcher to check if a {@linkcode Pokemon} has used a specific {@linkcode MoveId} at the given . * Matcher to check the contents of a {@linkcode Pokemon}'s move history.
* @param received - The actual value received. Should be a {@linkcode Pokemon} * @param received - The actual value received. Should be a {@linkcode Pokemon}
* @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove} * @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. * @param index - The index of the move history entry to check, in order from most recent to least recent.
* Default `0` (last used move) * Default `0` (last used move)
* @returns Whether the matcher passed * @returns Whether the matcher passed
@ -18,7 +20,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
export function toHaveUsedMoveMatcher( export function toHaveUsedMoveMatcher(
this: MatcherState, this: MatcherState,
received: unknown, received: unknown,
expectedResult: MoveId | Partial<TurnMove>, expectedResult: MoveId | AtLeastOne<TurnMove>,
index = 0, index = 0,
): SyncExpectationResult { ): SyncExpectationResult {
if (!isPokemonInstance(received)) { if (!isPokemonInstance(received)) {
@ -56,8 +58,8 @@ export function toHaveUsedMoveMatcher(
pass, pass,
message: () => message: () =>
pass pass
? `Expected ${pkmName}'s ${moveIndexStr} NOT to match ${this.utils.stringify(expectedResult)}, but it did!` ? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${this.utils.stringify(expectedResult)}, but it did!`
: `Expected ${pkmName}'s ${moveIndexStr} to match ${this.utils.stringify(expectedResult)}, but got ${this.utils.stringify(move)}!`, : `Expected ${pkmName}'s ${moveIndexStr} to match ${this.utils.stringify(expectedResult)}, but got ${this.utils.stringify(move)} instead!`,
expected: expectedResult, expected: expectedResult,
actual: move, actual: move,
}; };

View File

@ -1,6 +1,6 @@
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
import { toReadableString } from "#utils/common"; import { toTitleCase } from "#utils/strings";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/** /**
@ -38,9 +38,9 @@ export function toHaveWeatherMatcher(
message: () => message: () =>
pass pass
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!` ? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr}!`, : `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
actual: actualStr, actual,
expected: expectedStr, expected: expectedWeatherType,
}; };
} }
@ -54,6 +54,5 @@ function toWeatherStr(weatherType: WeatherType) {
return "no weather"; return "no weather";
} }
// TODO: Change to use updated string utils return toTitleCase(WeatherType[weatherType]);
return toReadableString(WeatherType[weatherType]);
} }

View File

@ -104,7 +104,7 @@ export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyo
} }
/** /**
* Convert a number into an English ordinal. * Convert a number into an English ordinal
* @param num - The number to convert into an ordinal * @param num - The number to convert into an ordinal
* @returns The ordinal representation of {@linkcode num}. * @returns The ordinal representation of {@linkcode num}.
* @example * @example
@ -129,6 +129,11 @@ export function getOrdinal(num: number): string {
return num + "th"; 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 { export function getStatName(s: Stat): string {
return i18next.t(getStatKey(s)); return i18next.t(getStatKey(s));
} }