-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+ toggleFavorite(currentId)}
+ />
+
+
+
- {query.isPending &&
Loading...
}
- {query.isError &&
Failed to load Pokemon
}
- {query.data &&
}
-
toggleFavorite(pokemonId)}
- />
+ {query.isPending && Loading...
}
+ {query.isError && Failed to load Pokemon
}
+ {query.data && }
+
+
+ {query.data && (
+
+ )}
+
+
+ {!isLoadingFavorites && (
+
+ )}
+
);
}
diff --git a/src/api/pokemon.test.ts b/src/api/pokemon.test.ts
new file mode 100644
index 0000000..91919a3
--- /dev/null
+++ b/src/api/pokemon.test.ts
@@ -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",
+ );
+ });
+});
diff --git a/src/components/FavoritePokemon/FavoritePokemon.css b/src/components/FavoritePokemon/FavoritePokemon.css
index 3b8e517..9f840b2 100644
--- a/src/components/FavoritePokemon/FavoritePokemon.css
+++ b/src/components/FavoritePokemon/FavoritePokemon.css
@@ -1,46 +1,90 @@
.favorite-container {
width: 100%;
- max-width: 400px;
- margin: 0 auto;
+ margin: 0;
+ overflow: hidden;
}
.favorite-list {
list-style-type: none;
- padding: 0;
margin: 0;
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 8px;
+ max-height: 95%;
+ box-sizing: border-box;
+ background: #f8f9fa;
+ padding: 12px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ overflow: scroll !important;
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+ scrollbar-width: none; /* Firefox, Safari 18.2+, Chromium 121+ */
+}
+
+.favorite-list .selected {
+ border-color: #007bff;
+ background: #e6f2ff;
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
+/* Rendered as a