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;
/**
* 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 type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
import { TurnMove } from "#types/turn-move";
import { AtLeastOne } from "#types/type-helpers";
import type { expect } from "vitest";
declare module "vitest" {
@ -42,7 +43,7 @@ declare module "vitest" {
* Default `0` (last used move)
* @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

View File

@ -6,7 +6,11 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
* @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: 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) {
return {
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,
expected,
};
}
// Create shallow copies of the arrays in case we have
const gotSorted = received.slice().sort();
const wantSorted = expected.slice().sort();
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
@ -37,8 +35,10 @@ export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expe
return {
pass,
message: () =>
`Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`,
actual: gotSorted,
pass
? `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,
actual: gotSorted,
};
}

View File

@ -4,6 +4,7 @@ import type { Pokemon } from "#field/pokemon";
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";
@ -28,14 +29,14 @@ export function toHaveAbilityAppliedMatcher(
const pass = received.waveData.abilitiesApplied.has(expectedAbilityId);
const pkmName = getPokemonNameWithAffix(received);
const expectedAbilityStr = `${AbilityId[expectedAbilityId]} (=${expectedAbilityId})`;
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 did not!`,
: `Expected ${pkmName} to have applied ${expectedAbilityStr}, but it didn't!`,
expected: expectedAbilityId,
actual: received.waveData.abilitiesApplied,
};

View File

@ -36,7 +36,7 @@ export function toHaveBattlerTag(
message: () =>
pass
? `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,
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 hp = received.hp;
const maxHp = received.getMaxHp();
const pkmName = getPokemonNameWithAffix(received);
return {
pass,
message: () =>
pass
? `Expected ${pkmName} NOT to have fainted, but it did! (${hp}/${maxHp} HP)`
: `Expected ${pkmName} to have fainted, but it did not. (${hp}/${maxHp} HP)`,
? `Expected ${pkmName} to NOT have fainted, but it did!`
: `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);
return {
pass,
message: () =>
pass
? `Expected ${pkmName} to NOT have full hp (${ofHpStr}), but it did!`
: `Expected ${pkmName} to have full hp, but found ${ofHpStr}.`,
? `Expected ${pkmName} to NOT have full hp, but it did!`
: `Expected ${pkmName} to have full hp, but it didn't! (${hp}/${maxHp} HP)`,
};
}

View File

@ -1,4 +1,6 @@
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";
@ -20,13 +22,14 @@ export function toHaveHpMatcher(this: MatcherState, received: unknown, expectedH
const pass = actualHp === expectedHp;
const pkmName = getPokemonNameWithAffix(received);
const maxHp = received.getMaxHp();
return {
pass,
message: () =>
pass
? `Expected ${pkmName} to NOT have ${expectedHp}/${maxHp} HP, but it did!`
: `Expected ${pkmName} to have ${expectedHp}/${maxHp} HP, but found ${actualHp}/${maxHp} HP.`,
? `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,
};
}

View File

@ -1,5 +1,6 @@
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 type { MatcherState, SyncExpectationResult } from "@vitest/expect";
@ -34,7 +35,7 @@ export function toHaveStatStageMatcher(
const pass = actualStage === expectedStage;
const pkmName = getPokemonNameWithAffix(received);
const statName = Stat[stat];
const statName = getStatName(stat);
return {
pass,

View File

@ -46,7 +46,7 @@ export function toHaveStatusEffectMatcher(
pass,
message: () =>
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: expectedStatus,
actual: actualEffect,
@ -65,7 +65,7 @@ export function toHaveStatusEffectMatcher(
pass,
message: () =>
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: expectedStatus,
actual: actualStatus,

View File

@ -38,7 +38,7 @@ export function toHaveTerrainMatcher(
message: () =>
pass
? `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,
expected: expectedTerrainType,
};

View File

@ -39,17 +39,22 @@ export function toHaveTypes(
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
const expectedTypes = 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(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 {
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,
expected: expectedTypes,
};

View File

@ -5,12 +5,14 @@ import type { Pokemon } from "#field/pokemon";
import { 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 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 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.
* Default `0` (last used move)
* @returns Whether the matcher passed
@ -18,7 +20,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
export function toHaveUsedMoveMatcher(
this: MatcherState,
received: unknown,
expectedResult: MoveId | Partial<TurnMove>,
expectedResult: MoveId | AtLeastOne<TurnMove>,
index = 0,
): SyncExpectationResult {
if (!isPokemonInstance(received)) {
@ -56,8 +58,8 @@ export function toHaveUsedMoveMatcher(
pass,
message: () =>
pass
? `Expected ${pkmName}'s ${moveIndexStr} NOT to 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 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)} instead!`,
expected: expectedResult,
actual: move,
};

View File

@ -1,6 +1,6 @@
import { WeatherType } from "#enums/weather-type";
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";
/**
@ -38,9 +38,9 @@ export function toHaveWeatherMatcher(
message: () =>
pass
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr}!`,
actual: actualStr,
expected: expectedStr,
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
actual,
expected: expectedWeatherType,
};
}
@ -54,6 +54,5 @@ function toWeatherStr(weatherType: WeatherType) {
return "no weather";
}
// TODO: Change to use updated string utils
return toReadableString(WeatherType[weatherType]);
return toTitleCase(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
* @returns The ordinal representation of {@linkcode num}.
* @example
@ -129,6 +129,11 @@ export function getOrdinal(num: number): string {
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));
}