Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import App from "./App";
import * as pokemonApi from "./api/pokemon";
import { DUMMY_DATA } from "./utils/testData";
import { createWrapper } from "./utils/testWrapper";

vi.mock("./api/pokemon", () => ({
getPokemonByID: vi.fn(),
}));

// jsdom has no layout, so the scroll the list does after loading is a no-op
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn();
});

beforeEach(() => {
window.localStorage.clear();
window.sessionStorage.clear();
vi.clearAllMocks();

// Every id resolves to a pokemon so the list around the current one fills up
vi.spyOn(pokemonApi, "getPokemonByID").mockImplementation(async (id) => ({
...DUMMY_DATA[0],
id,
}));
});

function renderApp() {
render(<App />, { wrapper: createWrapper() });
}

describe("App", () => {
test("shows the first pokemon once it has loaded", async () => {
renderApp();

expect(screen.getByText("Loading...")).toBeInTheDocument();

await waitFor(() => {
expect(pokemonApi.getPokemonByID).toHaveBeenCalledWith(1);
});
});

test("moves to the next pokemon when the user asks for it", async () => {
renderApp();

await waitFor(() => {
expect(screen.getByRole("button", { name: /Next/ })).toBeInTheDocument();
});

fireEvent.click(screen.getByRole("button", { name: /Next/ }));

await waitFor(() => {
expect(pokemonApi.getPokemonByID).toHaveBeenCalledWith(2);
});
});

test("cannot go back past the first pokemon", async () => {
renderApp();

await waitFor(() => {
expect(screen.getByRole("button", { name: /Prev/ })).toBeDisabled();
});
});

test("keeps a favorite in local storage", async () => {
renderApp();

await waitFor(() => {
expect(
screen.getByRole("button", { name: "Add to favorites" }),
).toBeInTheDocument();
});

fireEvent.click(screen.getByRole("button", { name: "Add to favorites" }));

expect(window.localStorage.getItem("favorites")).toBe("[1]");
});

test("keeps a filter choice in session storage", async () => {
renderApp();

await waitFor(() => {
expect(screen.getByLabelText("Highest id first")).toBeInTheDocument();
});

fireEvent.click(screen.getByLabelText("Highest id first"));

await waitFor(() => {
expect(window.sessionStorage.getItem("filters")).toContain("id-desc");
});
});

test("restores the rules that were stored before", async () => {
window.sessionStorage.setItem(
"filters",
JSON.stringify({ type: ["electric"], onlyFavorites: true }),
);

renderApp();

await waitFor(() => {
expect(screen.getByLabelText("electric")).toBeChecked();
});
expect(screen.getByLabelText("Only show favorites")).toBeChecked();
});
});
57 changes: 57 additions & 0 deletions src/api/pokemon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { getPokemonByID } from "./pokemon";
import { DUMMY_DATA } from "../utils/testData";

// Nothing here is allowed to reach the network, so fetch is replaced
const fetchMock = vi.fn();

beforeEach(() => {
vi.stubGlobal("fetch", fetchMock);
fetchMock.mockReset();
});

afterEach(() => {
vi.unstubAllGlobals();
});

describe("getPokemonByID", () => {
test("asks the api for the id it was given", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => DUMMY_DATA[2],
});

const result = await getPokemonByID(25);

expect(fetchMock).toHaveBeenCalledWith(
"https://pokeapi.co/api/v2/pokemon/25",
);
expect(result).toEqual(DUMMY_DATA[2]);
});

test("throws when the api answers with an error", async () => {
fetchMock.mockResolvedValue({ ok: false, json: async () => ({}) });

await expect(getPokemonByID(25)).rejects.toThrow("Failed to fetch pokemon");
});

test("rejects an id above the highest pokemon without calling the api", async () => {
await expect(getPokemonByID(1026)).rejects.toThrow("Invalid parameter");
expect(fetchMock).not.toHaveBeenCalled();
});

test("rejects a negative id without calling the api", async () => {
await expect(getPokemonByID(-1)).rejects.toThrow("Invalid parameter");
expect(fetchMock).not.toHaveBeenCalled();
});

test("accepts the last valid id", async () => {
fetchMock.mockResolvedValue({ ok: true, json: async () => DUMMY_DATA[0] });

await getPokemonByID(1025);

expect(fetchMock).toHaveBeenCalledWith(
"https://pokeapi.co/api/v2/pokemon/1025",
);
});
});
86 changes: 86 additions & 0 deletions src/components/FavoritePokemon/FavoritePokemon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, test, vi } from "vitest";
import { FavoritePokemon } from "./FavoritePokemon";
import { DUMMY_DATA } from "../../utils/testData";
import type { PokemonData } from "../../controllers/pokemonController";

function setup(pokemonList: PokemonData[], currentId = 1) {
const onSelectPokemon = vi.fn();

render(
<FavoritePokemon
pokemonList={pokemonList}
currentId={currentId}
onSelectPokemon={onSelectPokemon}
/>,
);

return { onSelectPokemon };
}

describe("FavoritePokemon Component", () => {
test("renders the pokemon it is given", () => {
setup([DUMMY_DATA[0], DUMMY_DATA[2]]);

expect(screen.getByText("bulbasaur")).toBeInTheDocument();
expect(screen.getByText("pikachu")).toBeInTheDocument();
expect(screen.getByText("#001")).toBeInTheDocument();
expect(screen.getByText("#025")).toBeInTheDocument();
});

test("orders by id no matter how the list arrives", () => {
setup([DUMMY_DATA[2], DUMMY_DATA[0], DUMMY_DATA[1]]);

const names = screen
.getAllByRole("button")
.map((button) => button.textContent?.replace(/[^a-z]/g, ""));

expect(names).toEqual(["bulbasaur", "charmander", "pikachu"]);
});

test("leaves the array it was given alone", () => {
const list = [DUMMY_DATA[2], DUMMY_DATA[0]];

setup(list);

expect(list[0]).toBe(DUMMY_DATA[2]);
});

test("marks the one that is selected", () => {
setup([DUMMY_DATA[0], DUMMY_DATA[2]], 25);

const selected = screen.getByRole("button", { name: /pikachu/ });

expect(selected).toHaveAttribute("aria-current", "true");
expect(
screen.getByRole("button", { name: /bulbasaur/ }),
).not.toHaveAttribute("aria-current");
});

test("reports the pokemon the user picked", () => {
const { onSelectPokemon } = setup([DUMMY_DATA[0], DUMMY_DATA[2]]);

fireEvent.click(screen.getByRole("button", { name: /pikachu/ }));

expect(onSelectPokemon).toHaveBeenCalledWith(25);
});

test("says so when there are no favorites yet", () => {
setup([]);

expect(screen.getByText("No favorites yet.")).toBeInTheDocument();
expect(screen.queryAllByRole("button")).toHaveLength(0);
});

test("matches snapshot", () => {
const { asFragment } = render(
<FavoritePokemon
pokemonList={[DUMMY_DATA[0], DUMMY_DATA[2]]}
currentId={25}
onSelectPokemon={vi.fn()}
/>,
);

expect(asFragment()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`FavoritePokemon Component > matches snapshot 1`] = `
<DocumentFragment>
<div
class="favorite-container"
>
<h2>
Favorites
</h2>
<ul
class="favorite-list"
>
<li>
<button
class="favorite-item "
type="button"
>
<img
alt="bulbasaur"
class="favorite-sprite"
src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
/>
<div
class="favorite-info"
>
<span
class="dex-number"
>
#001
</span>
<span
class="pokemon-name"
>
bulbasaur
</span>
</div>
</button>
</li>
<li>
<button
aria-current="true"
class="favorite-item selected"
type="button"
>
<img
alt="pikachu"
class="favorite-sprite"
src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png"
/>
<div
class="favorite-info"
>
<span
class="dex-number"
>
#025
</span>
<span
class="pokemon-name"
>
pikachu
</span>
</div>
</button>
</li>
</ul>
</div>
</DocumentFragment>
`;
50 changes: 49 additions & 1 deletion src/components/PokemonCard/PokemonCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { PokemonCard } from "./PokemonCard";
import { DUMMY_DATA } from "../../utils/testData";

const mockPokemon = DUMMY_DATA[2];

function spriteSrc() {
return screen.getByRole("img").getAttribute("src");
}

describe("PokemonCard Component", () => {
test("renders pokemon name and general information", () => {
render(<PokemonCard current={mockPokemon} />);
Expand All @@ -17,6 +21,50 @@ describe("PokemonCard Component", () => {
expect(screen.getByText("6 kg")).toBeDefined();
});

test("adds up the base stats", () => {
render(<PokemonCard current={mockPokemon} />);

const total = mockPokemon.stats.reduce(
(sum, stat) => sum + stat.base_stat,
0,
);

expect(screen.getByText(String(total))).toBeInTheDocument();
});

test("steps through the sprites when the image is clicked", () => {
render(<PokemonCard current={mockPokemon} />);

expect(spriteSrc()).toBe(mockPokemon.sprites.front_default);

fireEvent.click(screen.getByRole("button"));
expect(spriteSrc()).toBe(mockPokemon.sprites.back_default);

fireEvent.click(screen.getByRole("button"));
expect(spriteSrc()).toBe(mockPokemon.sprites.front_shiny);
});

test("wraps back to the first sprite at the end", () => {
render(<PokemonCard current={mockPokemon} />);

for (let i = 0; i < 4; i++) {
fireEvent.click(screen.getByRole("button"));
}

expect(spriteSrc()).toBe(mockPokemon.sprites.front_default);
});

test("goes back to the default sprite when another pokemon is shown", () => {
const { rerender } = render(<PokemonCard current={mockPokemon} />);

fireEvent.click(screen.getByRole("button"));
expect(spriteSrc()).toBe(mockPokemon.sprites.back_default);

rerender(<PokemonCard current={DUMMY_DATA[0]} />);

expect(spriteSrc()).toBe(DUMMY_DATA[0].sprites.front_default);
});

test("matches snapshot", () => {
const { asFragment } = render(<PokemonCard current={mockPokemon} />);
expect(asFragment()).toMatchSnapshot();
Expand Down
Loading