[Test] Improve error message + typing on toHaveUsedMove (#6681)

* [Test] Improve error message on `toHaveUsedMove`

* Fixed typing on test stuff + added caching on `toHaveArenaTagOptions`

* Fixed matcher breaking with single move arguments

* Fixed typing errors in `vitest.d.ts`

* Fixed typing importing from the wrong file

* Fixed wish test type errors

* Reverted type changes to battler tag matchers by request
This commit is contained in:
Bertie690 2025-12-17 21:57:34 -05:00 committed by GitHub
parent f48ec4c51e
commit b381d196cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 41 additions and 24 deletions

View File

@ -1,4 +1,4 @@
import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers";
import type { AtLeastOne, NonFunctionProperties } from "#types/type-helpers";
/**
* Helper type to admit an object containing the given properties
@ -22,6 +22,6 @@ import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#typ
* @typeParam O - The object to source keys from
* @typeParam K - One or more of O's keys to render mandatory
*/
export type OneOther<O extends object, K extends keyof O> = AtLeastOne<Omit<nonFunc<O>, K>> & {
[key in K]: O[K];
};
// NB: no need to recursively exclude non function properties
// TODO: Figure out how to force K to not be a method property
export type OneOther<O extends object, K extends keyof O> = Pick<O, K> & AtLeastOne<Omit<NonFunctionProperties<O>, K>>;

View File

@ -3,6 +3,7 @@ import "vitest";
import type Overrides from "#app/overrides";
import type { Phase } from "#app/phase";
import type { ArenaTag } from "#data/arena-tag";
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import type { TerrainType } from "#data/terrain";
import type { AbilityId } from "#enums/ability-id";
import type { ArenaTagSide } from "#enums/arena-tag-side";
@ -10,11 +11,11 @@ import type { ArenaTagType } from "#enums/arena-tag-type";
import type { BattlerTagType } from "#enums/battler-tag-type";
import type { MoveId } from "#enums/move-id";
import type { PokemonType } from "#enums/pokemon-type";
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import type { PositionalTagType } from "#enums/positional-tag-type";
import type { BattleStat, EffectiveStat } from "#enums/stat";
import type { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon";
import type { OneOther } from "#test/@types/test-helpers";
import type { GameManager } from "#test/test-utils/game-manager";
import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag";
import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag";
@ -24,7 +25,6 @@ import type { expectedStatusType } from "#test/test-utils/matchers/to-have-statu
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
import type { PhaseString } from "#types/phase-types";
import type { TurnMove } from "#types/turn-move";
import type { AtLeastOne } from "#types/type-helpers";
import type { toDmgValue } from "#utils/common";
import type { expect } from "vitest";
@ -154,7 +154,7 @@ interface PokemonMatchers {
* @param index - The index of the move history entry to check, in order from most recent to least recent; default `0`
* @see {@linkcode Pokemon.getLastXMoves}
*/
toHaveUsedMove(expectedMove: MoveId | AtLeastOne<TurnMove>, index?: number): void;
toHaveUsedMove(expectedMove: MoveId | OneOther<TurnMove, "move">, index?: number): void;
/**
* Check whether a {@linkcode Pokemon}'s effective stat is as expected

View File

@ -79,7 +79,7 @@ describe("Moves - Sleep Talk", () => {
game.move.select(MoveId.SLEEP_TALK);
await game.toNextTurn();
expect(feebas).toHaveUsedMove({ result: MoveResult.FAIL });
expect(feebas).toHaveUsedMove({ move: MoveId.SLEEP_TALK, result: MoveResult.FAIL });
});
it("should fail if the user has no valid moves", async () => {

View File

@ -79,7 +79,7 @@ describe("Move - Wish", () => {
await game.toEndOfTurn();
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL });
expect(alomomola).toHaveUsedMove({ move: MoveId.WISH, result: MoveResult.FAIL });
});
it("should function independently of Future Sight", async () => {

View File

@ -1,10 +1,10 @@
import { getPokemonNameWithAffix } from "#app/messages";
import type { MoveId } from "#enums/move-id";
import { MoveId } from "#enums/move-id";
import type { Pokemon } from "#field/pokemon";
import { getOnelineDiffStr, getOrdinal } from "#test/test-utils/string-utils";
import type { OneOther } from "#test/@types/test-helpers";
import { getEnumStr, 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";
/**
@ -12,14 +12,14 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
* @param received - The actual value received. Should be a {@linkcode Pokemon}
* @param expectedMove - 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)
* @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,
expectedMove: MoveId | AtLeastOne<TurnMove>,
expectedMove: MoveId | OneOther<TurnMove, "move">,
index = 0,
): SyncExpectationResult {
if (!isPokemonInstance(received)) {
@ -29,10 +29,10 @@ export function toHaveUsedMove(
};
}
const move: TurnMove | undefined = received.getLastXMoves(-1)[index];
const historyMove: TurnMove | undefined = received.getLastXMoves(-1)[index];
const pkmName = getPokemonNameWithAffix(received);
if (move === undefined) {
if (historyMove === undefined) {
return {
pass: this.isNot,
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
@ -40,20 +40,37 @@ export function toHaveUsedMove(
};
}
// Coerce to a `TurnMove`
if (typeof expectedMove === "number") {
expectedMove = { move: expectedMove };
}
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
const pass = this.equals(move, expectedMove, [
// Break out early if a move-only comparison was done or if the move ID did not match
const expectedId = typeof expectedMove === "number" ? expectedMove : expectedMove.move;
const actualId = historyMove.move;
const sameId = this.equals(actualId, expectedId, this.customTesters);
if (typeof expectedMove === "number" || !sameId) {
const expectedIdStr = getEnumStr(MoveId, expectedId);
const actualIdStr = getEnumStr(MoveId, actualId);
return {
pass: sameId,
// Expected Magikarp' 5th most recent move to be PHOTON_GEYSER, but got METRONOME instead!
message: () =>
sameId
? `Expected ${pkmName}'s ${moveIndexStr} to NOT be ${expectedIdStr}, but it was!`
: `Expected ${pkmName}'s ${moveIndexStr} to be ${expectedIdStr}, but got ${actualIdStr} instead!`,
expected: expectedMove,
actual: historyMove,
};
}
// Compare equality with the provided object
const pass = this.equals(historyMove, expectedMove, [
...this.customTesters,
this.utils.subsetEquality,
this.utils.iterableEquality,
]);
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
return {
pass,
message: () =>
@ -61,6 +78,6 @@ export function toHaveUsedMove(
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
: `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
expected: expectedMove,
actual: move,
actual: historyMove,
};
}