Fixed log message to not be overly verbose

This commit is contained in:
Bertie690 2025-07-28 11:20:31 -04:00
parent 3a6a16f3ff
commit 4b447073a7
10 changed files with 102 additions and 35 deletions

8
global.d.ts vendored
View File

@ -1,3 +1,4 @@
import type { AnyFn } from "#types/type-helpers";
import type { SetupServerApi } from "msw/node";
declare global {
@ -9,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>;
}
}

View File

@ -1,3 +1,4 @@
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/**
@ -28,17 +29,20 @@ export function toEqualArrayUnsorted(
}
// 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]);
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,
message: () =>
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,
? `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,
};
}

View File

@ -29,14 +29,14 @@ export function toHaveBattlerTag(
const pass = !!received.getTag(expectedBattlerTagType);
const pkmName = getPokemonNameWithAffix(received);
// "BattlerTagType.SEEDED (=1)"
const expectedTagStr = getEnumStr(BattlerTagType, expectedBattlerTagType);
const expectedTagStr = getEnumStr(BattlerTagType, expectedBattlerTagType, { prefix: "BattlerTagType." });
return {
pass,
message: () =>
pass
? `Expected ${pkmName} to NOT have BattlerTagType.${expectedTagStr}, but it did!`
: `Expected ${pkmName} to have BattlerTagType.${expectedTagStr}, but it didn't!`,
? `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),
};

View File

@ -43,7 +43,7 @@ export function toHaveStatStage(
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!`,
actual: actualStage,
expected: expectedStage,
actual: actualStage,
};
}

View File

@ -4,7 +4,7 @@ import type { Pokemon } from "#field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { StatusEffect } from "#enums/status-effect";
import { getEnumStr } from "#test/test-utils/string-utils";
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";
@ -33,14 +33,20 @@ export function toHaveStatusEffect(
}
const pkmName = getPokemonNameWithAffix(received);
// Check exclusively effect equality
if (typeof expectedStatus === "number" || received.status?.effect !== expectedStatus.effect) {
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, actualEffect, { prefix: "StatusEffect." });
const expectedStr = getEnumStr(StatusEffect, expectedStatus, { prefix: "StatusEffect." });
return {
pass,
@ -53,7 +59,7 @@ export function toHaveStatusEffect(
};
}
// Check for equality of all fields (for toxic turn count)
// Check for equality of all fields (for toxic turn count/etc)
const actualStatus = received.status;
const pass = this.equals(received, expectedStatus, [
...this.customTesters,
@ -61,12 +67,15 @@ export function toHaveStatusEffect(
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 ${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 NOT match ${expectedStr}, but it did!`
: `Expected ${pkmName}'s status to match ${expectedStr}, but got ${actualStr} instead!`,
expected: expectedStatus,
actual: actualStatus,
};

View File

@ -39,8 +39,8 @@ export function toHaveTerrain(
pass
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
actual,
expected: expectedTerrainType,
actual,
};
}
@ -53,5 +53,6 @@ function toTerrainStr(terrainType: TerrainType) {
if (terrainType === TerrainType.NONE) {
return "no terrain";
}
// "Electric Terrain (=2)"
return getEnumStr(TerrainType, terrainType, { casing: "Title", suffix: " Terrain" });
}

View File

@ -55,7 +55,7 @@ export function toHaveTypes(
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,
actual: actualTypes,
};
}

View File

@ -2,7 +2,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import type { MoveId } from "#enums/move-id";
// biome-ignore lint/correctness/noUnusedImports: TSDocs
import type { Pokemon } from "#field/pokemon";
import { getOrdinal } from "#test/test-utils/string-utils";
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";
@ -54,12 +54,14 @@ export function toHaveUsedMove(
this.utils.iterableEquality,
]);
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
return {
pass,
message: () =>
pass
? `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 ${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,
};

View File

@ -39,8 +39,8 @@ export function toHaveWeather(
pass
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
actual,
expected: expectedWeatherType,
actual,
};
}

View File

@ -2,6 +2,7 @@ import { getStatKey, type Stat } from "#enums/stat";
import type { EnumOrObject, EnumValues, NormalEnum, TSNumericEnum } from "#types/enum-types";
import { enumValueToKey } from "#utils/enums";
import { toTitleCase } from "#utils/strings";
import type { MatcherState } from "@vitest/expect";
import i18next from "i18next";
type Casing = "Preserve" | "Title";
@ -23,12 +24,12 @@ interface getEnumStrOptions {
}
/**
* Helper function to return the name of an enum member or const object value, alongside its corresponding value.
* 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.
* @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
@ -37,8 +38,8 @@ interface getEnumStrOptions {
* TWO: 2,
* THREE: 3,
* }
* console.log(getEnumStr(fakeEnum, fakeEnum.ONE)); // Output: "ONE (=1)"
* console.log(getEnumStr(fakeEnum, fakeEnum.TWO, {case: "Title", prefix: "fakeEnum."})); // Output: "fakeEnum.Two (=2)"
* 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>(
@ -73,7 +74,7 @@ export function getEnumStr<E extends EnumOrObject>(
* 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`
* @returns The stringified representation of `enums`.
* @example
* ```ts
* enum fakeEnum {
@ -90,19 +91,49 @@ export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyo
}
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
// 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`
names = enums.map(e => enumValueToKey(obj as NormalEnum<E>, e) as string);
// 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
@ -137,3 +168,15 @@ export function getOrdinal(num: number): string {
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}");
}