mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
[Tests][UI/UX] Add automated tests for the pokedex (#5637)
* Remove unneeded fields from src/ui/filter-text.ts * Add setOverlayMode to phaseInterceptor * initialize pokemon starters before running tests * Add getWrappedText to mockText * Add initial pokedex test * Add test for wrapping cursor in pokedex view * Make pokedex use getPassiveAbility instead of checking passive map Allows for tests to mock passives * Add test for filtering double ability combinations * Add test for filtering by types * Mark failing test as TODO * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> * Use ts-expect-error instead of ts-ignore in comments Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Add save for pokedex tests * Add test for filtering by cost reduction * Add test for filtering by shiny * Add tests for filtering by cost reductions * Fix typo in test name * Update test/ui/pokedex.test.ts Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> * Update Mode import in pokedex test * Replace reference to Mode with UiMode --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
This commit is contained in:
parent
b848777880
commit
389ad6ceb6
@ -20,7 +20,6 @@ export class FilterText extends Phaser.GameObjects.Container {
|
|||||||
private window: Phaser.GameObjects.NineSlice;
|
private window: Phaser.GameObjects.NineSlice;
|
||||||
private labels: Phaser.GameObjects.Text[] = [];
|
private labels: Phaser.GameObjects.Text[] = [];
|
||||||
private selections: Phaser.GameObjects.Text[] = [];
|
private selections: Phaser.GameObjects.Text[] = [];
|
||||||
private selectionStrings: string[] = [];
|
|
||||||
private rows: FilterTextRow[] = [];
|
private rows: FilterTextRow[] = [];
|
||||||
public cursorObj: Phaser.GameObjects.Image;
|
public cursorObj: Phaser.GameObjects.Image;
|
||||||
public numFilters = 0;
|
public numFilters = 0;
|
||||||
@ -112,8 +111,6 @@ export class FilterText extends Phaser.GameObjects.Container {
|
|||||||
this.selections.push(filterTypesSelection);
|
this.selections.push(filterTypesSelection);
|
||||||
this.add(filterTypesSelection);
|
this.add(filterTypesSelection);
|
||||||
|
|
||||||
this.selectionStrings.push("");
|
|
||||||
|
|
||||||
this.calcFilterPositions();
|
this.calcFilterPositions();
|
||||||
this.numFilters++;
|
this.numFilters++;
|
||||||
|
|
||||||
@ -122,7 +119,6 @@ export class FilterText extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
resetSelection(index: number): void {
|
resetSelection(index: number): void {
|
||||||
this.selections[index].setText(this.defaultText);
|
this.selections[index].setText(this.defaultText);
|
||||||
this.selectionStrings[index] = "";
|
|
||||||
this.onChange();
|
this.onChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +200,17 @@ export class FilterText extends Phaser.GameObjects.Container {
|
|||||||
return this.selections[row].getWrappedText()[0];
|
return this.selections[row].getWrappedText()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly set the selection text for a specific filter row and then call the `onChange` function
|
||||||
|
*
|
||||||
|
* @param row - The filter row to set the text for
|
||||||
|
* @param value - The text to set for the filter row
|
||||||
|
*/
|
||||||
|
setValue(row: FilterTextRow, value: string) {
|
||||||
|
this.selections[row].setText(value);
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the nearest filter to the provided container on the y-axis
|
* Find the nearest filter to the provided container on the y-axis
|
||||||
* @param container the StarterContainer to compare position against
|
* @param container the StarterContainer to compare position against
|
||||||
|
@ -37,10 +37,9 @@ import { addWindow } from "./ui-theme";
|
|||||||
import type { OptionSelectConfig } from "./abstact-option-select-ui-handler";
|
import type { OptionSelectConfig } from "./abstact-option-select-ui-handler";
|
||||||
import { FilterText, FilterTextRow } from "./filter-text";
|
import { FilterText, FilterTextRow } from "./filter-text";
|
||||||
import { allAbilities } from "#app/data/data-lists";
|
import { allAbilities } from "#app/data/data-lists";
|
||||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
|
||||||
import { allMoves } from "#app/data/moves/move";
|
import { allMoves } from "#app/data/moves/move";
|
||||||
import { speciesTmMoves } from "#app/data/balance/tms";
|
import { speciesTmMoves } from "#app/data/balance/tms";
|
||||||
import { pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions";
|
import { pokemonStarters } from "#app/data/balance/pokemon-evolutions";
|
||||||
import { Biome } from "#enums/biome";
|
import { Biome } from "#enums/biome";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
|
|
||||||
@ -174,7 +173,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
|||||||
private scrollCursor: number;
|
private scrollCursor: number;
|
||||||
private oldCursor = -1;
|
private oldCursor = -1;
|
||||||
|
|
||||||
private allSpecies: PokemonSpecies[] = [];
|
|
||||||
private lastSpecies: PokemonSpecies;
|
private lastSpecies: PokemonSpecies;
|
||||||
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
|
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
|
||||||
private pokerusSpecies: PokemonSpecies[] = [];
|
private pokerusSpecies: PokemonSpecies[] = [];
|
||||||
@ -493,12 +491,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
for (const species of allSpecies) {
|
for (const species of allSpecies) {
|
||||||
this.speciesLoaded.set(species.speciesId, false);
|
this.speciesLoaded.set(species.speciesId, false);
|
||||||
this.allSpecies.push(species);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here code to declare 81 containers
|
// Here code to declare 81 containers
|
||||||
for (let i = 0; i < 81; i++) {
|
for (let i = 0; i < 81; i++) {
|
||||||
const pokemonContainer = new PokedexMonContainer(this.allSpecies[i]).setVisible(false);
|
const pokemonContainer = new PokedexMonContainer(allSpecies[i]).setVisible(false);
|
||||||
const pos = calcStarterPosition(i);
|
const pos = calcStarterPosition(i);
|
||||||
pokemonContainer.setPosition(pos.x, pos.y);
|
pokemonContainer.setPosition(pos.x, pos.y);
|
||||||
this.iconAnimHandler.addOrUpdate(pokemonContainer.icon, PokemonIconAnimMode.NONE);
|
this.iconAnimHandler.addOrUpdate(pokemonContainer.icon, PokemonIconAnimMode.NONE);
|
||||||
@ -1342,7 +1339,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
this.filteredPokemonData = [];
|
this.filteredPokemonData = [];
|
||||||
|
|
||||||
this.allSpecies.forEach(species => {
|
allSpecies.forEach(species => {
|
||||||
const starterId = this.getStarterSpeciesId(species.speciesId);
|
const starterId = this.getStarterSpeciesId(species.speciesId);
|
||||||
|
|
||||||
const currentDexAttr = this.getCurrentDexProps(species.speciesId);
|
const currentDexAttr = this.getCurrentDexProps(species.speciesId);
|
||||||
@ -1412,12 +1409,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
// Ability filter
|
// Ability filter
|
||||||
const abilities = [species.ability1, species.ability2, species.abilityHidden].map(a => allAbilities[a].name);
|
const abilities = [species.ability1, species.ability2, species.abilityHidden].map(a => allAbilities[a].name);
|
||||||
const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId)
|
// get the passive ability for the species
|
||||||
? species.speciesId
|
const passives = [species.getPassiveAbility()];
|
||||||
: starterPassiveAbilities.hasOwnProperty(starterId)
|
for (const form of species.forms) {
|
||||||
? starterId
|
passives.push(form.getPassiveAbility());
|
||||||
: pokemonPrevolutions[starterId];
|
}
|
||||||
const passives = starterPassiveAbilities[passiveId];
|
|
||||||
|
|
||||||
const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1);
|
const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1);
|
||||||
const fitsFormAbility1 = species.forms.some(form =>
|
const fitsFormAbility1 = species.forms.some(form =>
|
||||||
|
@ -21,6 +21,8 @@ import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
|
|||||||
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
|
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
|
||||||
import EventEmitter = Phaser.Events.EventEmitter;
|
import EventEmitter = Phaser.Events.EventEmitter;
|
||||||
import UpdateList = Phaser.GameObjects.UpdateList;
|
import UpdateList = Phaser.GameObjects.UpdateList;
|
||||||
|
import { PokedexMonContainer } from "#app/ui/pokedex-mon-container";
|
||||||
|
import MockContainer from "./mocks/mocksContainer/mockContainer";
|
||||||
// biome-ignore lint/style/noNamespaceImport: Necessary in order to mock the var
|
// biome-ignore lint/style/noNamespaceImport: Necessary in order to mock the var
|
||||||
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||||
|
|
||||||
@ -61,6 +63,10 @@ export default class GameWrapper {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
|
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
|
||||||
|
|
||||||
|
// Pokedex container is not actually mocking container, but the sprites they contain are mocked.
|
||||||
|
// We need to mock the remove function to not throw an error when removing a sprite.
|
||||||
|
PokedexMonContainer.prototype.remove = MockContainer.prototype.remove;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScene(scene: BattleScene) {
|
setScene(scene: BattleScene) {
|
||||||
|
@ -308,5 +308,14 @@ export default class MockText implements MockGameObject {
|
|||||||
return this.list;
|
return this.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the word wrap algorithm on the text, then returns an array of the lines
|
||||||
|
*/
|
||||||
|
getWrappedText() {
|
||||||
|
// Returns the wrapped text.
|
||||||
|
// return this.phaserText.getWrappedText();
|
||||||
|
return this.runWordWrap(this.text).split("\n");
|
||||||
|
}
|
||||||
|
|
||||||
on(_event: string | symbol, _fn: Function, _context?: any) {}
|
on(_event: string | symbol, _fn: Function, _context?: any) {}
|
||||||
}
|
}
|
||||||
|
@ -205,6 +205,7 @@ export default class PhaseInterceptor {
|
|||||||
private phaseFrom;
|
private phaseFrom;
|
||||||
private inProgress;
|
private inProgress;
|
||||||
private originalSetMode;
|
private originalSetMode;
|
||||||
|
private originalSetOverlayMode;
|
||||||
private originalSuperEnd;
|
private originalSuperEnd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -442,6 +443,7 @@ export default class PhaseInterceptor {
|
|||||||
*/
|
*/
|
||||||
initPhases() {
|
initPhases() {
|
||||||
this.originalSetMode = UI.prototype.setMode;
|
this.originalSetMode = UI.prototype.setMode;
|
||||||
|
this.originalSetOverlayMode = UI.prototype.setOverlayMode;
|
||||||
this.originalSuperEnd = Phase.prototype.end;
|
this.originalSuperEnd = Phase.prototype.end;
|
||||||
UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args);
|
UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args);
|
||||||
Phase.prototype.end = () => this.superEndPhase.call(this);
|
Phase.prototype.end = () => this.superEndPhase.call(this);
|
||||||
@ -508,6 +510,18 @@ export default class PhaseInterceptor {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mock to set overlay mode
|
||||||
|
* @param mode - The {@linkcode Mode} to set.
|
||||||
|
* @param args - Additional arguments to pass to the original method.
|
||||||
|
*/
|
||||||
|
setOverlayMode(mode: UiMode, ...args: unknown[]): Promise<void> {
|
||||||
|
const instance = this.scene.ui;
|
||||||
|
console.log("setOverlayMode", `${UiMode[mode]} (=${mode})`, args);
|
||||||
|
const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to start the prompt handler.
|
* Method to start the prompt handler.
|
||||||
*/
|
*/
|
||||||
@ -572,6 +586,7 @@ export default class PhaseInterceptor {
|
|||||||
phase.prototype.start = this.phases[phase.name].start;
|
phase.prototype.start = this.phases[phase.name].start;
|
||||||
}
|
}
|
||||||
UI.prototype.setMode = this.originalSetMode;
|
UI.prototype.setMode = this.originalSetMode;
|
||||||
|
UI.prototype.setOverlayMode = this.originalSetOverlayMode;
|
||||||
Phase.prototype.end = this.originalSuperEnd;
|
Phase.prototype.end = this.originalSuperEnd;
|
||||||
clearInterval(this.promptInterval);
|
clearInterval(this.promptInterval);
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
|
1
test/testUtils/saves/data_pokedex_tests.prsv
Normal file
1
test/testUtils/saves/data_pokedex_tests.prsv
Normal file
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ import { initLoggedInUser } from "#app/account";
|
|||||||
import { initAbilities } from "#app/data/abilities/ability";
|
import { initAbilities } from "#app/data/abilities/ability";
|
||||||
import { initBiomes } from "#app/data/balance/biomes";
|
import { initBiomes } from "#app/data/balance/biomes";
|
||||||
import { initEggMoves } from "#app/data/balance/egg-moves";
|
import { initEggMoves } from "#app/data/balance/egg-moves";
|
||||||
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
|
import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions";
|
||||||
import { initMoves } from "#app/data/moves/move";
|
import { initMoves } from "#app/data/moves/move";
|
||||||
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
|
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
|
||||||
import { initPokemonForms } from "#app/data/pokemon-forms";
|
import { initPokemonForms } from "#app/data/pokemon-forms";
|
||||||
@ -85,7 +85,6 @@ export function initTestFile() {
|
|||||||
HTMLCanvasElement.prototype.getContext = () => mockContext;
|
HTMLCanvasElement.prototype.getContext = () => mockContext;
|
||||||
|
|
||||||
// Initialize all of these things if and only if they have not been initialized yet
|
// Initialize all of these things if and only if they have not been initialized yet
|
||||||
// initSpecies();
|
|
||||||
if (!wasInitialized) {
|
if (!wasInitialized) {
|
||||||
wasInitialized = true;
|
wasInitialized = true;
|
||||||
initI18n();
|
initI18n();
|
||||||
@ -101,6 +100,8 @@ export function initTestFile() {
|
|||||||
initAbilities();
|
initAbilities();
|
||||||
initLoggedInUser();
|
initLoggedInUser();
|
||||||
initMysteryEncounters();
|
initMysteryEncounters();
|
||||||
|
// init the pokemon starters for the pokedex
|
||||||
|
initPokemonStarters();
|
||||||
}
|
}
|
||||||
|
|
||||||
manageListeners();
|
manageListeners();
|
||||||
|
492
test/ui/pokedex.test.ts
Normal file
492
test/ui/pokedex.test.ts
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
|
||||||
|
import PokedexUiHandler from "#app/ui/pokedex-ui-handler";
|
||||||
|
import { FilterTextRow } from "#app/ui/filter-text";
|
||||||
|
import { allAbilities } from "#app/data/data-lists";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { allSpecies, getPokemonSpecies, type PokemonForm } from "#app/data/pokemon-species";
|
||||||
|
import { Button } from "#enums/buttons";
|
||||||
|
import { DropDownColumn } from "#app/ui/filter-bar";
|
||||||
|
import type PokemonSpecies from "#app/data/pokemon-species";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { UiMode } from "#enums/ui-mode";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Information for the `data_pokedex_tests.psrv`:
|
||||||
|
|
||||||
|
Caterpie - Shiny 0
|
||||||
|
Rattata - Shiny 1
|
||||||
|
Ekans - Shiny 2
|
||||||
|
|
||||||
|
Chikorita has enough candies to unlock passive
|
||||||
|
Cyndaquil has first cost reduction unlocked, enough candies to buy the second
|
||||||
|
Totodile has first cost reduction unlocked, not enough candies to buy the second
|
||||||
|
Treecko has both cost reduction unlocked
|
||||||
|
Torchic has enough candies to do anything
|
||||||
|
Mudkip has passive unlocked
|
||||||
|
Turtwig has enough candies to purchase an egg
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all permutations of elements from an array
|
||||||
|
*/
|
||||||
|
function permutations<T>(array: T[], length: number): T[][] {
|
||||||
|
if (length === 0) {
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
return array.flatMap((item, index) =>
|
||||||
|
permutations([...array.slice(0, index), ...array.slice(index + 1)], length - 1).map(perm => [item, ...perm]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("UI - Pokedex", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
const mocks: MockInstance[] = [];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (mocks.length > 0) {
|
||||||
|
mocks.pop()?.mockRestore();
|
||||||
|
}
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the game to open the pokedex UI.
|
||||||
|
* @returns The handler for the pokedex UI.
|
||||||
|
*/
|
||||||
|
async function runToOpenPokedex(): Promise<PokedexUiHandler> {
|
||||||
|
// Open the pokedex UI.
|
||||||
|
await game.runToTitle();
|
||||||
|
|
||||||
|
await game.phaseInterceptor.setOverlayMode(UiMode.POKEDEX);
|
||||||
|
|
||||||
|
// Get the handler for the current UI.
|
||||||
|
const handler = game.scene.ui.getHandler();
|
||||||
|
expect(handler).toBeInstanceOf(PokedexUiHandler);
|
||||||
|
|
||||||
|
return handler as PokedexUiHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a set of pokemon that have a specific ability in allAbilities
|
||||||
|
* @param ability - The ability to filter for
|
||||||
|
*/
|
||||||
|
function getSpeciesWithAbility(ability: Abilities): Set<Species> {
|
||||||
|
const speciesSet = new Set<Species>();
|
||||||
|
for (const pkmn of allSpecies) {
|
||||||
|
if (
|
||||||
|
[pkmn.ability1, pkmn.ability2, pkmn.getPassiveAbility(), pkmn.abilityHidden].includes(ability) ||
|
||||||
|
pkmn.forms.some(form =>
|
||||||
|
[form.ability1, form.ability2, form.abilityHidden, form.getPassiveAbility()].includes(ability),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
speciesSet.add(pkmn.speciesId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return speciesSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a set of pokemon that have one of the specified type(s)
|
||||||
|
*
|
||||||
|
* Includes all forms of the pokemon
|
||||||
|
* @param types - The types to filter for
|
||||||
|
*/
|
||||||
|
function getSpeciesWithType(...types: PokemonType[]): Set<Species> {
|
||||||
|
const speciesSet = new Set<Species>();
|
||||||
|
const tySet = new Set<PokemonType>(types);
|
||||||
|
|
||||||
|
// get the pokemon and its forms
|
||||||
|
outer: for (const pkmn of allSpecies) {
|
||||||
|
// @ts-expect-error We know that type2 might be null.
|
||||||
|
if (tySet.has(pkmn.type1) || tySet.has(pkmn.type2)) {
|
||||||
|
speciesSet.add(pkmn.speciesId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const form of pkmn.forms) {
|
||||||
|
// @ts-expect-error We know that type2 might be null.
|
||||||
|
if (tySet.has(form.type1) || tySet.has(form.type2)) {
|
||||||
|
speciesSet.add(pkmn.speciesId);
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return speciesSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create mocks for the abilities of a species.
|
||||||
|
* This is used to set the abilities of a species to a specific value.
|
||||||
|
* All abilities are optional. Not providing one will set it to NONE.
|
||||||
|
*
|
||||||
|
* This will override the ability of the pokemon species only, unless set forms is true
|
||||||
|
*
|
||||||
|
* @param species - The species to set the abilities for
|
||||||
|
* @param ability - The ability to set for the first ability
|
||||||
|
* @param ability2 - The ability to set for the second ability
|
||||||
|
* @param hidden - The ability to set for the hidden ability
|
||||||
|
* @param passive - The ability to set for the passive ability
|
||||||
|
* @param setForms - Whether to also overwrite the abilities for each of the species' forms (defaults to `true`)
|
||||||
|
*/
|
||||||
|
function createAbilityMocks(
|
||||||
|
species: Species,
|
||||||
|
{
|
||||||
|
ability = Abilities.NONE,
|
||||||
|
ability2 = Abilities.NONE,
|
||||||
|
hidden = Abilities.NONE,
|
||||||
|
passive = Abilities.NONE,
|
||||||
|
setForms = true,
|
||||||
|
}: {
|
||||||
|
ability?: Abilities;
|
||||||
|
ability2?: Abilities;
|
||||||
|
hidden?: Abilities;
|
||||||
|
passive?: Abilities;
|
||||||
|
setForms?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const pokemon = getPokemonSpecies(species);
|
||||||
|
const checks: [PokemonSpecies | PokemonForm] = [pokemon];
|
||||||
|
if (setForms) {
|
||||||
|
checks.push(...pokemon.forms);
|
||||||
|
}
|
||||||
|
for (const p of checks) {
|
||||||
|
mocks.push(vi.spyOn(p, "ability1", "get").mockReturnValue(ability));
|
||||||
|
mocks.push(vi.spyOn(p, "ability2", "get").mockReturnValue(ability2));
|
||||||
|
mocks.push(vi.spyOn(p, "abilityHidden", "get").mockReturnValue(hidden));
|
||||||
|
mocks.push(vi.spyOn(p, "getPassiveAbility").mockReturnValue(passive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************
|
||||||
|
* Tests for Filters *
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
it("should filter to show only the pokemon with an ability when filtering by ability", async () => {
|
||||||
|
// await game.importData("test/testUtils/saves/everything.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// Get name of overgrow
|
||||||
|
const overgrow = allAbilities[Abilities.OVERGROW].name;
|
||||||
|
|
||||||
|
// @ts-expect-error `filterText` is private
|
||||||
|
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, overgrow);
|
||||||
|
|
||||||
|
// filter all species to be the pokemon that have overgrow
|
||||||
|
const overgrowSpecies = getSpeciesWithAbility(Abilities.OVERGROW);
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
const filteredSpecies = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
|
||||||
|
|
||||||
|
expect(filteredSpecies).toEqual(overgrowSpecies);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter to show only pokemon with ability and passive when filtering by 2 abilities", async () => {
|
||||||
|
// Setup mocks for the ability and passive combinations
|
||||||
|
const whitelist: Species[] = [];
|
||||||
|
const blacklist: Species[] = [];
|
||||||
|
|
||||||
|
const filter_ab1 = Abilities.OVERGROW;
|
||||||
|
const filter_ab2 = Abilities.ADAPTABILITY;
|
||||||
|
const ab1_instance = allAbilities[filter_ab1];
|
||||||
|
const ab2_instance = allAbilities[filter_ab2];
|
||||||
|
|
||||||
|
// Create a species with passive set and each "ability" field
|
||||||
|
const baseObj = {
|
||||||
|
ability: Abilities.BALL_FETCH,
|
||||||
|
ability2: Abilities.NONE,
|
||||||
|
hidden: Abilities.BLAZE,
|
||||||
|
passive: Abilities.TORRENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock pokemon to have the exhaustive combination of the two selected abilities
|
||||||
|
const attrs: (keyof typeof baseObj)[] = ["ability", "ability2", "hidden", "passive"];
|
||||||
|
for (const [idx, value] of permutations(attrs, 2).entries()) {
|
||||||
|
createAbilityMocks(Species.BULBASAUR + idx, {
|
||||||
|
...baseObj,
|
||||||
|
[value[0]]: filter_ab1,
|
||||||
|
[value[1]]: filter_ab2,
|
||||||
|
});
|
||||||
|
if (value.includes("passive")) {
|
||||||
|
whitelist.push(Species.BULBASAUR + idx);
|
||||||
|
} else {
|
||||||
|
blacklist.push(Species.BULBASAUR + idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error `filterText` is private
|
||||||
|
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, ab1_instance.name);
|
||||||
|
// @ts-expect-error `filterText` is private
|
||||||
|
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_2, ab2_instance.name);
|
||||||
|
|
||||||
|
let whiteListCount = 0;
|
||||||
|
// @ts-expect-error `filteredPokemonData` is private
|
||||||
|
for (const species of pokedexHandler.filteredPokemonData) {
|
||||||
|
expect(blacklist, "entry must have one of the abilities as a passive").not.toContain(species.species.speciesId);
|
||||||
|
|
||||||
|
const rawAbility = [species.species.ability1, species.species.ability2, species.species.abilityHidden];
|
||||||
|
const rawPassive = species.species.getPassiveAbility();
|
||||||
|
|
||||||
|
const c1 = rawPassive === ab1_instance.id && rawAbility.includes(ab2_instance.id);
|
||||||
|
const c2 = c1 || (rawPassive === ab2_instance.id && rawAbility.includes(ab1_instance.id));
|
||||||
|
|
||||||
|
expect(c2, "each filtered entry should have the ability and passive combination").toBe(true);
|
||||||
|
if (whitelist.includes(species.species.speciesId)) {
|
||||||
|
whiteListCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(whiteListCount).toBe(whitelist.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter to show only the pokemon with a type when filtering by a single type", async () => {
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
|
||||||
|
|
||||||
|
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL);
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
|
||||||
|
|
||||||
|
expect(filteredPokemon).toEqual(expectedPokemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Todo: Pokemon with a mega that adds a type do not show up in the filter, e.g. pinsir.
|
||||||
|
it.todo("should show only the pokemon with one of the types when filtering by multiple types", async () => {
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.FLYING + 1);
|
||||||
|
|
||||||
|
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL, PokemonType.FLYING);
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
|
||||||
|
|
||||||
|
expect(filteredPokemon).toEqual(expectedPokemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering for unlockable cost reduction only shows species with sufficient candies", async () => {
|
||||||
|
// load the save file
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
|
||||||
|
// Cycling 4 times to get to the "can unlock" for cost reduction
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
// index 1 is the cost reduction
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedPokemon = new Set([
|
||||||
|
Species.CHIKORITA,
|
||||||
|
Species.CYNDAQUIL,
|
||||||
|
Species.TORCHIC,
|
||||||
|
Species.TURTWIG,
|
||||||
|
Species.EKANS,
|
||||||
|
Species.MUDKIP,
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(pokemon =>
|
||||||
|
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering by passive unlocked only shows species that have their passive", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
|
||||||
|
filter.toggleOptionState(0); // cycle to Passive: Yes
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(
|
||||||
|
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.MUDKIP,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering for pokemon that can unlock passive shows only species with sufficient candies", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
|
||||||
|
// Cycling 4 times to get to the "can unlock" for passive
|
||||||
|
const expectedPokemon = new Set([
|
||||||
|
Species.EKANS,
|
||||||
|
Species.CHIKORITA,
|
||||||
|
Species.CYNDAQUIL,
|
||||||
|
Species.TORCHIC,
|
||||||
|
Species.TURTWIG,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// cycling twice to get to the "can unlock" for passive
|
||||||
|
filter.toggleOptionState(0);
|
||||||
|
filter.toggleOptionState(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(pokemon =>
|
||||||
|
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering for pokemon that have any cost reduction shows only the species that have unlocked a cost reduction", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
const expectedPokemon = new Set([Species.TREECKO, Species.CYNDAQUIL, Species.TOTODILE]);
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
// Cycle 1 time for cost reduction
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(pokemon =>
|
||||||
|
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering for pokemon that have a single cost reduction shows only the species that have unlocked a single cost reduction", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
const expectedPokemon = new Set([Species.CYNDAQUIL, Species.TOTODILE]);
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
// Cycle 2 times for one cost reduction
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(pokemon =>
|
||||||
|
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering for pokemon that have two cost reductions sorts only shows the species that have unlocked both cost reductions", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
|
||||||
|
// Cycle 3 time for two cost reductions
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
pokedexHandler.filteredPokemonData.every(
|
||||||
|
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.TREECKO,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtering by shiny status shows the caught pokemon with the selected shiny tier", async () => {
|
||||||
|
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
// @ts-expect-error - `filterBar` is private
|
||||||
|
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.CAUGHT);
|
||||||
|
filter.toggleOptionState(3);
|
||||||
|
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
let filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
|
||||||
|
|
||||||
|
// Red shiny
|
||||||
|
expect(filteredPokemon.length).toBe(1);
|
||||||
|
expect(filteredPokemon[0], "tier 1 shiny").toBe(Species.CATERPIE);
|
||||||
|
|
||||||
|
// tier 2 shiny
|
||||||
|
filter.toggleOptionState(3);
|
||||||
|
filter.toggleOptionState(2);
|
||||||
|
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
|
||||||
|
expect(filteredPokemon.length).toBe(1);
|
||||||
|
expect(filteredPokemon[0], "tier 2 shiny").toBe(Species.RATTATA);
|
||||||
|
|
||||||
|
filter.toggleOptionState(2);
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
|
||||||
|
expect(filteredPokemon.length).toBe(1);
|
||||||
|
expect(filteredPokemon[0], "tier 3 shiny").toBe(Species.EKANS);
|
||||||
|
|
||||||
|
// filter by no shiny
|
||||||
|
filter.toggleOptionState(1);
|
||||||
|
filter.toggleOptionState(4);
|
||||||
|
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
|
||||||
|
expect(filteredPokemon.length).toBe(27);
|
||||||
|
expect(filteredPokemon, "not shiny").not.toContain(Species.CATERPIE);
|
||||||
|
expect(filteredPokemon, "not shiny").not.toContain(Species.RATTATA);
|
||||||
|
expect(filteredPokemon, "not shiny").not.toContain(Species.EKANS);
|
||||||
|
});
|
||||||
|
|
||||||
|
/****************************
|
||||||
|
* Tests for UI Input *
|
||||||
|
****************************/
|
||||||
|
|
||||||
|
// TODO: fix cursor wrapping
|
||||||
|
it.todo(
|
||||||
|
"should wrap the cursor to the top when moving to an empty entry when there are more than 81 pokemon",
|
||||||
|
async () => {
|
||||||
|
const pokedexHandler = await runToOpenPokedex();
|
||||||
|
|
||||||
|
// Filter by gen 2 so we can pan a specific amount.
|
||||||
|
// @ts-expect-error `filterBar` is private
|
||||||
|
pokedexHandler.filterBar.getFilter(DropDownColumn.GEN).options[2].toggleOptionState();
|
||||||
|
pokedexHandler.updateStarters();
|
||||||
|
// @ts-expect-error - `filteredPokemonData` is private
|
||||||
|
expect(pokedexHandler.filteredPokemonData.length, "pokemon in gen2").toBe(100);
|
||||||
|
|
||||||
|
// Let's try to pan to the right to see what the pokemon it points to is.
|
||||||
|
|
||||||
|
// pan to the right once and down 11 times
|
||||||
|
pokedexHandler.processInput(Button.RIGHT);
|
||||||
|
// Nab the pokemon that is selected for comparison later.
|
||||||
|
|
||||||
|
// @ts-expect-error - `lastSpecies` is private
|
||||||
|
const selectedPokemon = pokedexHandler.lastSpecies.speciesId;
|
||||||
|
for (let i = 0; i < 11; i++) {
|
||||||
|
pokedexHandler.processInput(Button.DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error `lastSpecies` is private
|
||||||
|
expect(selectedPokemon).toEqual(pokedexHandler.lastSpecies.speciesId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user