From 610d7c8c7fa3d522c6310908c825f1ad9942b48a Mon Sep 17 00:00:00 2001 From: Emma Fredriksen Date: Thu, 17 Sep 2026 19:35:22 +0200 Subject: [PATCH 1/5] feat: set up test files and add tests for searchbar --- package-lock.json | 15 ++++++++++++ package.json | 4 +++- src/components/Searchbar.test.tsx | 39 +++++++++++++++++++++++++++++++ vitest.config.ts | 7 ++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/components/Searchbar.test.tsx create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 5c6bcfd..3f658e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/babel__core": "^7.20.5", "@types/node": "^24.13.3", "@types/react": "^19.2.18", @@ -1171,6 +1172,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", diff --git a/package.json b/package.json index 1e069d2..0cbd761 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { "@tanstack/react-query": "^5.103.0", @@ -20,6 +21,7 @@ "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/babel__core": "^7.20.5", "@types/node": "^24.13.3", "@types/react": "^19.2.18", diff --git a/src/components/Searchbar.test.tsx b/src/components/Searchbar.test.tsx new file mode 100644 index 0000000..e5be8e1 --- /dev/null +++ b/src/components/Searchbar.test.tsx @@ -0,0 +1,39 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom/vitest"; +import Searchbar from "./Searchbar"; +import { describe, it, expect, vi, afterEach } from "vitest"; + +afterEach(() => { + cleanup(); +}); + +describe("Searchbar", () => { + it("Searches based on a searchQuery", () => { + const onSearchChange = vi.fn(); + render(); + const input = screen.getByRole("searchbox"); + + expect(input).toHaveValue("Rick"); + }); + it("Searches based on a searchQuery with capital letters", () => { + const onSearchChange = vi.fn(); + render(); + const input = screen.getByRole("searchbox"); + + expect(input).toHaveValue("rIcK"); + }); + + it("calls onSearchChange when user types", async () => { + const user = userEvent.setup(); + const onSearchChange = vi.fn(); + + render(); + + const input = screen.getByRole("searchbox"); + + await user.type(input, "Rick"); + + expect(onSearchChange).toHaveBeenCalled(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..9f6250a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + }, +}); From c110f4b0ab26e4a88e44f77108edf6210eec7f8b Mon Sep 17 00:00:00 2001 From: Emma Fredriksen Date: Thu, 17 Sep 2026 20:01:05 +0200 Subject: [PATCH 2/5] test: add tests for CharacterCardSmall --- src/components/CharacterCardSmall.test.tsx | 72 +++++++++++++++++++ .../CharacterCardSmall.test.tsx.snap | 54 ++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/components/CharacterCardSmall.test.tsx create mode 100644 src/components/__snapshots__/CharacterCardSmall.test.tsx.snap diff --git a/src/components/CharacterCardSmall.test.tsx b/src/components/CharacterCardSmall.test.tsx new file mode 100644 index 0000000..b75859f --- /dev/null +++ b/src/components/CharacterCardSmall.test.tsx @@ -0,0 +1,72 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import CharacterCardSmall from "./CharacterCardSmall"; +import "@testing-library/jest-dom/vitest"; +import userEvent from "@testing-library/user-event"; + +afterEach(() => { + cleanup(); +}); + +describe("CharacterCardSmall", () => { + it("displays the character name", () => { + render( + , + ); + + expect(screen.getByText("Rick Sanchez")).toBeInTheDocument(); + }); + + it("displays the character image", () => { + render( + , + ); + + const image = screen.getByAltText("character image"); + expect(image).toHaveAttribute("src", "rick.jpg"); + }); + + it("calls onDetails when Details is clicked", async () => { + const user = userEvent.setup(); + const onDetails = vi.fn(); + + render( + , + ); + + await user.click(screen.getByRole("button", { name: "Details" })); + expect(onDetails).toHaveBeenCalledOnce(); + }); + + it("matches snapshot", () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/__snapshots__/CharacterCardSmall.test.tsx.snap b/src/components/__snapshots__/CharacterCardSmall.test.tsx.snap new file mode 100644 index 0000000..a9a10af --- /dev/null +++ b/src/components/__snapshots__/CharacterCardSmall.test.tsx.snap @@ -0,0 +1,54 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CharacterCardSmall > matches snapshot 1`] = ` +
+
+
+ +
+ character image +

+ Rick Sanchez +

+
+ +
+
+ +
+
+
+`; From 6a0aa3730c5ecc232aa88f21e317e672812be8ac Mon Sep 17 00:00:00 2001 From: Emma Fredriksen Date: Thu, 17 Sep 2026 20:58:41 +0200 Subject: [PATCH 3/5] test: create tests for CharacterDetailCard --- src/components/CharacterDetailCard.test.tsx | 212 ++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/components/CharacterDetailCard.test.tsx diff --git a/src/components/CharacterDetailCard.test.tsx b/src/components/CharacterDetailCard.test.tsx new file mode 100644 index 0000000..9e609ef --- /dev/null +++ b/src/components/CharacterDetailCard.test.tsx @@ -0,0 +1,212 @@ +import React from "react"; +import { cleanup, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import userEvent from "@testing-library/user-event"; + +import CharacterDetailCard from "./CharacterDetailCard"; +import type { Character } from "../types/Character"; + +const mockCharacter: Character = { + id: 1, + name: "Rick Sanchez", + species: "Human", + status: "Alive", + gender: "Male", + origin: { + name: "Earth (C-137)", + url: "https://rickandmortyapi.com/api/location/1", + }, + image: "rick.jpg", + episode: [ + "https://rickandmortyapi.com/api/episode/1", + "https://rickandmortyapi.com/api/episode/2", + ], +}; + +function renderWithQueryClient(ui: React.ReactNode) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return render( + {ui}, + ); +} + +afterEach(() => { + cleanup(); +}); + +describe("CharacterDetailCard", () => { + it("displays the character name", () => { + renderWithQueryClient( + , + ); + + expect( + screen.getByRole("heading", { + name: "Rick Sanchez", + }), + ).toBeInTheDocument(); + }); + + it("displays the character image", () => { + renderWithQueryClient( + , + ); + + const image = screen.getByAltText("Rick Sanchez"); + expect(image).toHaveAttribute("src", "rick.jpg"); + }); + + it("displays the character speice", () => { + renderWithQueryClient( + , + ); + + expect(screen.getByText("Human")).toBeInTheDocument(); + }); + + it("displays the character status", () => { + renderWithQueryClient( + , + ); + + expect(screen.getByText(/Alive/)).toBeInTheDocument(); + }); + + it("displays the character's origin", () => { + renderWithQueryClient( + , + ); + + expect(screen.getByText("Earth (C-137)")).toBeInTheDocument(); + }); + + it("displays the character episodes", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => [ + { + id: 1, + name: "Pilot", + episode: "S01E01", + }, + { + id: 2, + name: "Lawnmower Dog", + episode: "S01E02", + }, + ], + }), + ); + + renderWithQueryClient( + , + ); + + expect(await screen.findByText("S01E01 - Pilot")).toBeInTheDocument(); + + expect( + await screen.findByText("S01E02 - Lawnmower Dog"), + ).toBeInTheDocument(); + }); + + it("calls onNext when next character button is clicked", async () => { + const user = userEvent.setup(); + const onNext = vi.fn(); + + renderWithQueryClient( + , + ); + + await user.click( + screen.getByRole("button", { + name: "Next character", + }), + ); + + expect(onNext).toHaveBeenCalledOnce(); + }); + + it("calls onPrevious when previous character button is clicked", async () => { + const user = userEvent.setup(); + const onPrevious = vi.fn(); + + renderWithQueryClient( + , + ); + + await user.click( + screen.getByRole("button", { + name: "Previous character", + }), + ); + + expect(onPrevious).toHaveBeenCalledOnce(); + }); +}); From c528bd0195a303bd417f7f482981e355d5049888 Mon Sep 17 00:00:00 2001 From: Emma Fredriksen Date: Thu, 17 Sep 2026 20:58:57 +0200 Subject: [PATCH 4/5] test: create tests for CharactersPage --- src/pages/CharactersPage.test.tsx | 308 ++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/pages/CharactersPage.test.tsx diff --git a/src/pages/CharactersPage.test.tsx b/src/pages/CharactersPage.test.tsx new file mode 100644 index 0000000..26a5661 --- /dev/null +++ b/src/pages/CharactersPage.test.tsx @@ -0,0 +1,308 @@ +import React from "react"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom/vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import CharactersPage from "./CharactersPage"; + +// AI was used to create this test file +const mockCharacters = [ + { + id: 1, + name: "Rick Sanchez", + species: "Human", + status: "Alive", + gender: "Male", + origin: { + name: "Earth (C-137)", + url: "", + }, + image: "rick.jpg", + episode: ["https://rickandmortyapi.com/api/episode/1"], + }, + { + id: 2, + name: "Summer Smith", + species: "Human", + status: "Alive", + gender: "Female", + origin: { + name: "Earth", + url: "", + }, + image: "summer.jpg", + episode: ["https://rickandmortyapi.com/api/episode/2"], + }, +]; + +// CharacterPage bruker React Query, +// så testen trenger en QueryClientProvider. +function renderWithQueryClient(ui: React.ReactNode) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return render( + {ui}, + ); +} + +// Mock fetch for både characters og episodes +function mockFetch() { + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation((url: string) => { + // CharactersPage henter characters + if (url.includes("/character")) { + return Promise.resolve({ + ok: true, + json: async () => ({ + results: mockCharacters, + }), + }); + } + + // CharacterDetailCard henter episodes + if (url.includes("/episode")) { + return Promise.resolve({ + ok: true, + json: async () => ({ + id: 1, + name: "Pilot", + episode: "S01E01", + }), + }); + } + + return Promise.reject(new Error("Unknown URL")); + }), + ); +} + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + + localStorage.clear(); + sessionStorage.clear(); +}); + +describe("CharactersPage", () => { + it("displays fetched characters", async () => { + mockFetch(); + + renderWithQueryClient(); + + expect(await screen.findByText("Rick Sanchez")).toBeInTheDocument(); + + expect(screen.getByText("Summer Smith")).toBeInTheDocument(); + }); + + it("opens character details", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + const detailsButtons = screen.getAllByRole("button", { + name: "Details", + }); + + await user.click(detailsButtons[0]); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + expect( + screen.getByRole("heading", { + name: "Rick Sanchez", + }), + ).toBeInTheDocument(); + }); + + it("closes character details", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + const detailsButtons = screen.getAllByRole("button", { + name: "Details", + }); + + await user.click(detailsButtons[0]); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + await user.click( + screen.getByRole("button", { + name: "Close character details", + }), + ); + + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("shows the next character", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + // Open Rick + await user.click( + screen.getAllByRole("button", { + name: "Details", + })[0], + ); + + expect( + screen.getByRole("heading", { + name: "Rick Sanchez", + }), + ).toBeInTheDocument(); + + // Go to Summer + await user.click( + screen.getByRole("button", { + name: "Next character", + }), + ); + + expect( + screen.getByRole("heading", { + name: "Summer Smith", + }), + ).toBeInTheDocument(); + }); + + it("shows the previous character", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + // Open Summer + await user.click( + screen.getAllByRole("button", { + name: "Details", + })[1], + ); + + expect( + screen.getByRole("heading", { + name: "Summer Smith", + }), + ).toBeInTheDocument(); + + // Go back to Rick + await user.click( + screen.getByRole("button", { + name: "Previous character", + }), + ); + + expect( + screen.getByRole("heading", { + name: "Rick Sanchez", + }), + ).toBeInTheDocument(); + }); + + it("filters characters based on search", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + const searchInput = screen.getByRole("searchbox"); + + await user.type(searchInput, "Rick"); + + expect(screen.getByText("Rick Sanchez")).toBeInTheDocument(); + + expect(screen.queryByText("Summer Smith")).not.toBeInTheDocument(); + }); + + it("filters characters by gender", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + await user.click( + screen.getByRole("checkbox", { + name: "Female", + }), + ); + + expect(screen.getByText("Summer Smith")).toBeInTheDocument(); + + expect(screen.queryByText("Rick Sanchez")).not.toBeInTheDocument(); + }); + + it("adds a character to favorites", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + const favoriteButton = screen.getByRole("button", { + name: "Add Rick Sanchez to favorites", + }); + + await user.click(favoriteButton); + + expect( + screen.getByRole("button", { + name: "Remove Rick Sanchez from favorites", + }), + ).toBeInTheDocument(); + }); + + it("stores favorites in localStorage", async () => { + const user = userEvent.setup(); + + mockFetch(); + + renderWithQueryClient(); + + await screen.findByText("Rick Sanchez"); + + await user.click( + screen.getByRole("button", { + name: "Add Rick Sanchez to favorites", + }), + ); + + expect( + JSON.parse(localStorage.getItem("favoriteCharacterIds") ?? "[]"), + ).toContain(1); + }); +}); From 21c7b964930b59ff09488a9230c4f58f2f41c81f Mon Sep 17 00:00:00 2001 From: Emma Fredriksen Date: Thu, 17 Sep 2026 21:01:53 +0200 Subject: [PATCH 5/5] docs: remove comments --- src/pages/CharactersPage.test.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pages/CharactersPage.test.tsx b/src/pages/CharactersPage.test.tsx index 26a5661..e17a84c 100644 --- a/src/pages/CharactersPage.test.tsx +++ b/src/pages/CharactersPage.test.tsx @@ -37,8 +37,6 @@ const mockCharacters = [ }, ]; -// CharacterPage bruker React Query, -// så testen trenger en QueryClientProvider. function renderWithQueryClient(ui: React.ReactNode) { const queryClient = new QueryClient({ defaultOptions: { @@ -53,12 +51,10 @@ function renderWithQueryClient(ui: React.ReactNode) { ); } -// Mock fetch for både characters og episodes function mockFetch() { vi.stubGlobal( "fetch", vi.fn().mockImplementation((url: string) => { - // CharactersPage henter characters if (url.includes("/character")) { return Promise.resolve({ ok: true, @@ -68,7 +64,6 @@ function mockFetch() { }); } - // CharacterDetailCard henter episodes if (url.includes("/episode")) { return Promise.resolve({ ok: true, @@ -163,7 +158,6 @@ describe("CharactersPage", () => { await screen.findByText("Rick Sanchez"); - // Open Rick await user.click( screen.getAllByRole("button", { name: "Details", @@ -176,7 +170,6 @@ describe("CharactersPage", () => { }), ).toBeInTheDocument(); - // Go to Summer await user.click( screen.getByRole("button", { name: "Next character", @@ -199,7 +192,6 @@ describe("CharactersPage", () => { await screen.findByText("Rick Sanchez"); - // Open Summer await user.click( screen.getAllByRole("button", { name: "Details", @@ -212,7 +204,6 @@ describe("CharactersPage", () => { }), ).toBeInTheDocument(); - // Go back to Rick await user.click( screen.getByRole("button", { name: "Previous character",