Skip to content

Commit

Permalink
test: add tests for components to check rendering and expected behavi…
Browse files Browse the repository at this point in the history
…our. docs: update README. chore: move CSS modules into folders with components.

having tests and CSS modules in the same folder as the components increases ease of maintainability. this ticket fixes #15, although the subtasks will be completed separately: #25 will test the hook, which has some issues that need to be resolved. #26 will implement e2e testing. #10 is tested here, along with ensuring the helper function to randomly select Pokemon IDs do not return any ids that are duplicates (that would be bad for gameplay). README was updated to increase size of the logo, update the screenshot to have hover effect apparent, and added more explanation around using Vitest and RTL. Removed acknowledgement for an old loading image (Pokeball) as we are now just using the Masterball that Muhammad Jazman created.
  • Loading branch information
henrylin03 committed Dec 16, 2024
1 parent f049d10 commit cfee34d
Show file tree
Hide file tree
Showing 18 changed files with 200 additions and 30 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
<br />
<div align="center">
<a href="https://github.com/henrylin03/pokemems">
<img src="./public/android-chrome-512x512.png" alt="Logo" width="100" height="100">
<img src="./public/android-chrome-512x512.png" alt="Logo" width="160" height="160">
</a>

<h3 align="center">Pokémems</h3>

<p align="center">
A fun and interactive game to test your memory featuring the original 150 Pokémon!
Fun and interactive game to test your memory, featuring the original 150 Pokémon!
<br />
<br />
<a href="https://poke-mems.netlify.app/">View demo</a>
Expand All @@ -35,13 +35,22 @@ Challenge yourself to try and set a new high score!

[![Screenshot](./docs/screenshot.png)](https://poke-mems.netlify.app/)

This project is part of [The Odin Project's](https://www.theodinproject.com/) "Full Stack JavaScript" course. Built in ReactJS, this project focuses on practising React's `useEffect` hook for handling side effects and API calls. All Pokémon IDs, names, and images are fetched from the RESTful [PokéAPI](https://pokeapi.co/).
This project is part of [The Odin Project's](https://www.theodinproject.com/) "Full Stack JavaScript" course. Built in ReactJS, this project focuses on practising:

1. React's [`useEffect`](https://react.dev/reference/react/useEffect) hook for consuming RESTful APIs - all Pokémon IDs, names, and images are fetched from the RESTful [PokéAPI](https://pokeapi.co/).
2. Styling React applications using [CSS modules](https://github.com/css-modules/css-modules).
3. Test-driven development (TDD) using [Vitest](https://vitest.dev/) and [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro/).

### Built with

[![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/)
[![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=fbc924)](https://vite.dev/)

### Tested with

[![Vitest](https://img.shields.io/badge/-Vitest-252529?style=for-the-badge&logo=vitest&logoColor=FCC72B)](https://vitest.dev/)
[![Testing-Library](https://img.shields.io/badge/-RTL-%23E33332?style=for-the-badge&logo=testing-library&logoColor=white)](https://testing-library.com/docs/react-testing-library/intro/)

<!-- CONTRIBUTING -->

## Contributing
Expand All @@ -57,9 +66,8 @@ Distributed under the MIT License. See `LICENSE.txt` for more information.
## Acknowledgements

- [PokéAPI](https://pokeapi.co/) for providing the free Pokémon dataset through a RESTful API
- [Vincenzo Bianco](https://codepen.io/vinztt/pen/XjEyvZ) for the spinning Pokeball animation when a card is being loaded
- [Muhammad Jazman](https://www.iconfinder.com/icons/1703899/ball_master_pocket_pocket_monster_icon) for the static Masterball image file when a card is being loaded
- [draw.io](https://app.diagrams.net/) for its free wireframing tools
- [draw.io](https://app.diagrams.net/) for its free wireframing tool
- [Coolors](https://coolors.co/) for comprehensive colour palettes
- [favicon.io](https://favicon.io) for generating the favicons
- The present README was heavily influenced by the ["Best-README-Template"](https://github.com/othneildrew/Best-README-Template)
Expand Down
Binary file modified docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import Header from "./components/Header";
import Gameboard from "./components/Gameboard";
import styles from "./styles/app.module.css"
import styles from "./app.module.css"

function App() {
const [currentScore, setCurrentScore] = useState(0);
Expand Down
File renamed without changes.
47 changes: 47 additions & 0 deletions src/components/Card/Card.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Card from ".";

describe("Card component", () => {
const MOCK_POKEMON_DATA = { id: 404, name: "Pokemon", imageUrl: "" };

it("should render a clickable card with an image and text", () => {
render(
<Card
pokemonData={MOCK_POKEMON_DATA}
handleCardSelection={() => {}}
isLoading={false}
/>
);

const cardElement = screen.getByRole("button", { name: /Pokemon/i });
const pokemonNameElement = screen.getByText(/Pokemon/i);
const pokemonImageElement = screen.getByRole("img", { name: /Pokemon/i });

expect(cardElement).toBeInTheDocument();
expect(pokemonNameElement).toBeInTheDocument();
expect(pokemonImageElement).toBeInTheDocument();
});

/* NB: I have opted not to test that event handler _has_ been clicked on event, because this will handled in testing functionality once clicked at the App.test.jsx level */

it("should not call event handler prop if mousedown event has not happened, nor when Card component is loading", async () => {
const mockHandlerFunction = vi.fn();
const user = userEvent.setup();

render(
<Card
handleCardSelection={mockHandlerFunction}
pokemonData={MOCK_POKEMON_DATA}
isLoading={true}
/>
);

expect(mockHandlerFunction).not.toHaveBeenCalled();

const cardElement = screen.getByRole("button", { name: /Pokemon/i });
await user.click(cardElement);
expect(mockHandlerFunction).not.toHaveBeenCalled();
});
});
File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/Card.jsx → src/components/Card/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import styles from "../styles/card.module.css";
import styles from "./card.module.css";

const Card = ({ handleCardSelection, pokemonData, isLoading }) => {
return (
Expand Down
69 changes: 69 additions & 0 deletions src/components/Gameboard/Gameboard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, vi, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import Gameboard from ".";
import useAllPokemon from "../../hooks/useAllPokemon";

vi.mock("../../hooks/useAllPokemon", () => ({ default: vi.fn() }));

describe("Gameboard component", () => {
it("displays the loading screen when data is loading", () => {
useAllPokemon.mockReturnValue({
pokemons: [],
error: null,
isLoading: true,
});

render(<Gameboard incrementScore={() => {}} resetScore={() => {}} />);

const loadingScreenElement = screen.getByRole("status", {
name: /Loading/i,
});

expect(loadingScreenElement).toBeInTheDocument();
});

it("does not display Card components when data is loading", () => {
useAllPokemon.mockReturnValue({
pokemons: [],
error: null,
isLoading: true,
});

render(<Gameboard incrementScore={() => {}} resetScore={() => {}} />);

const cardElements = screen.queryByRole("button", {
name: /Select Pokemon/i,
});

expect(cardElements).toBeNull();
});

it("displays Card components of Pokemons once useAllPokemon hook has finished loading", () => {
const MOCK_POKEMON_DATA_ARRAY = [
{ id: 1, name: "Pokemon1", imageUrl: "https://pokemon1.png" },
{ id: 2, name: "Pokemon2", imageUrl: "https://pokemon2.jpg" },
];
useAllPokemon.mockReturnValue({
pokemons: MOCK_POKEMON_DATA_ARRAY,
error: null,
isLoading: false,
});

render(<Gameboard incrementScore={() => {}} resetScore={() => {}} />);

expect(
screen.getAllByRole("button", { name: /Select Pokemon/i })
).toHaveLength(MOCK_POKEMON_DATA_ARRAY.length);
expect(
screen.getByRole("button", { name: /Pokemon1/i })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Pokemon2/i })
).toBeInTheDocument();
});
});

// selecting Pokemon card should update state and regenerate Pokemon displayed
// selecting previously-selected Pokemon should reset score and board
// verify error handling logic
// mock useAllPokemon hook behaviour (simulate various states)
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useState } from "react";
import PropTypes from "prop-types";
import Card from "./Card";
import LoadingScreen from "./LoadingScreen";
import useAllPokemon from "../hooks/useAllPokemon";
import Card from "../Card";
import LoadingScreen from "../LoadingScreen";
import useAllPokemon from "../../hooks/useAllPokemon";
import {
getRandomId,
getRandomPokemonIds,
shuffleElements,
randomlySelectElements,
} from "../helpers";
import styles from "../styles/gameboard.module.css";
} from "../../helpers";
import styles from "./gameboard.module.css";

const TOTAL_IDS = 10;

Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/components/Header.jsx → src/components/Header/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import styles from "../styles/header.module.css";
import styles from "./header.module.css";

const Header = ({ currentScore, highScore }) => (
<header className={styles.header}>
Expand All @@ -10,7 +10,7 @@ const Header = ({ currentScore, highScore }) => (
</p>
</div>

<article className={styles.scoreboard}>
<article className={styles.scoreboard} aria-label="Scoreboard">
<p className={styles.label}>score:</p>
<p className={styles.score}>{currentScore}</p>
<p className={styles.label}>high score:</p>
Expand Down
15 changes: 0 additions & 15 deletions src/components/LoadingScreen.jsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/components/LoadingScreen/LoadingScreen.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import LoadingScreen from ".";

describe("Loading screen component", () => {
it("renders the container article", () => {
render(<LoadingScreen />);
expect(
screen.getByRole("status", { name: /Loading/i })
).toBeInTheDocument();
});

it("renders the loading image", () => {
render(<LoadingScreen />);

const loadingImage = screen.getByRole("img", {
name: "Loading animation",
});

expect(loadingImage).toBeInTheDocument();
});

it("renders text to advise loading state", () => {
render(<LoadingScreen />);
expect(screen.getByText(/.../)).toBeInTheDocument();
});
});
20 changes: 20 additions & 0 deletions src/components/LoadingScreen/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import masterBall from "../../assets/masterball.png";
import styles from "./loadingScreen.module.css";

const LoadingScreen = () => (
<article
className={styles.loadingScreen}
role="status"
aria-busy="true"
aria-label="Loading Pokémon data"
>
<img
className={styles.masterball}
src={masterBall}
alt="Loading animation"
/>
<p className={styles.text}>Catching them all...</p>
</article>
);

export default LoadingScreen;
File renamed without changes.
14 changes: 14 additions & 0 deletions src/helpers/helpers.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { getRandomPokemonIds } from ".";

describe.each(Array.from({ length: 5 }, () => getRandomPokemonIds()))(
"Helper function to generate random Pokemon IDs",
() => {
it("never generates any duplicate IDs", (randomPokemonIds) => {
const isArrayWithUniqueElements = (arr) =>
Array.isArray(arr) && new Set(arr).size === arr.length;

expect(isArrayWithUniqueElements(randomPokemonIds)).toBeTruthy;
});
}
);
2 changes: 1 addition & 1 deletion src/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const getRandomId = (maxId = 150) => Math.floor(Math.random() * maxId + 1);

const getRandomPokemonIds = (totalIds) => {
const getRandomPokemonIds = (totalIds = 10) => {
let ids = [];
for (let i = 0; i < totalIds; i++) {
let randomId = getRandomId();
Expand Down

0 comments on commit cfee34d

Please sign in to comment.