diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index f06f16ef86e..89c427059c3 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -75,3 +75,12 @@ export type NonFunctionPropertiesRecursive = { }; export type AbstractConstructor = 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 = Partial & EnumValues<{ [K in keyof T]: Pick }>; diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index d4aa5a447f4..3113b02ffb0 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -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, index?: number): void; + toHaveUsedMove(expected: MoveId | AtLeastOne, index?: number): void; /** * Matcher to check if a {@linkcode Pokemon Pokemon's} effective stat is as expected diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-array-unsorted.ts index c3388765841..e9e3c6c3855 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-array-unsorted.ts @@ -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, }; } diff --git a/test/test-utils/matchers/to-have-ability-applied.ts b/test/test-utils/matchers/to-have-ability-applied.ts index 1170850d844..f1846dca398 100644 --- a/test/test-utils/matchers/to-have-ability-applied.ts +++ b/test/test-utils/matchers/to-have-ability-applied.ts @@ -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, }; diff --git a/test/test-utils/matchers/to-have-battler-tag.ts b/test/test-utils/matchers/to-have-battler-tag.ts index 51d020e8252..e2bee37849e 100644 --- a/test/test-utils/matchers/to-have-battler-tag.ts +++ b/test/test-utils/matchers/to-have-battler-tag.ts @@ -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), }; diff --git a/test/test-utils/matchers/to-have-fainted.ts b/test/test-utils/matchers/to-have-fainted.ts index 33f8d439d19..f8d2f2d8917 100644 --- a/test/test-utils/matchers/to-have-fainted.ts +++ b/test/test-utils/matchers/to-have-fainted.ts @@ -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)`, }; } diff --git a/test/test-utils/matchers/to-have-full-hp.ts b/test/test-utils/matchers/to-have-full-hp.ts index e8ed8b78364..96285ab6340 100644 --- a/test/test-utils/matchers/to-have-full-hp.ts +++ b/test/test-utils/matchers/to-have-full-hp.ts @@ -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)`, }; } diff --git a/test/test-utils/matchers/to-have-hp.ts b/test/test-utils/matchers/to-have-hp.ts index 36bf6b90a68..d8ef90e91d2 100644 --- a/test/test-utils/matchers/to-have-hp.ts +++ b/test/test-utils/matchers/to-have-hp.ts @@ -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, }; } diff --git a/test/test-utils/matchers/to-have-stat-stage-matcher.ts b/test/test-utils/matchers/to-have-stat-stage-matcher.ts index 466f933d246..08799b62a98 100644 --- a/test/test-utils/matchers/to-have-stat-stage-matcher.ts +++ b/test/test-utils/matchers/to-have-stat-stage-matcher.ts @@ -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, diff --git a/test/test-utils/matchers/to-have-status-effect-matcher.ts b/test/test-utils/matchers/to-have-status-effect-matcher.ts index bf1ecdd458b..b60d2639e46 100644 --- a/test/test-utils/matchers/to-have-status-effect-matcher.ts +++ b/test/test-utils/matchers/to-have-status-effect-matcher.ts @@ -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, diff --git a/test/test-utils/matchers/to-have-terrain-matcher.ts b/test/test-utils/matchers/to-have-terrain-matcher.ts index 67292ef8bd9..c9ce4a21620 100644 --- a/test/test-utils/matchers/to-have-terrain-matcher.ts +++ b/test/test-utils/matchers/to-have-terrain-matcher.ts @@ -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, }; diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index 59f43d9fd76..ff865a1cf73 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -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, }; diff --git a/test/test-utils/matchers/to-have-used-move-matcher.ts b/test/test-utils/matchers/to-have-used-move-matcher.ts index ff1e0d95ce4..9f1e55fae5b 100644 --- a/test/test-utils/matchers/to-have-used-move-matcher.ts +++ b/test/test-utils/matchers/to-have-used-move-matcher.ts @@ -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, + expectedResult: MoveId | AtLeastOne, 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, }; diff --git a/test/test-utils/matchers/to-have-weather-matcher.ts b/test/test-utils/matchers/to-have-weather-matcher.ts index 7bc9fe52bbc..b2f88e805c0 100644 --- a/test/test-utils/matchers/to-have-weather-matcher.ts +++ b/test/test-utils/matchers/to-have-weather-matcher.ts @@ -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]); } diff --git a/test/test-utils/string-utils.ts b/test/test-utils/string-utils.ts index 6dcb7438f96..827461d2491 100644 --- a/test/test-utils/string-utils.ts +++ b/test/test-utils/string-utils.ts @@ -104,7 +104,7 @@ export function stringifyEnumArray(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)); }