React Puzzle Games - Memory Puzzle
@@ -31,8 +31,4 @@ AppHeader.propTypes = {
onRestart: PropTypes.func.isRequired,
};
-AppHeader.defaultProps = {
- moves: 0,
-};
-
export default AppHeader;
diff --git a/src/AppHeader.test.js b/src/AppHeader.test.js
new file mode 100644
index 0000000..433db69
--- /dev/null
+++ b/src/AppHeader.test.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import AppHeader from './AppHeader';
+
+describe('AppHeader', () => {
+ const mockOnNewGame = jest.fn();
+ const mockOnRestart = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders header with title', () => {
+ render(
+
+ );
+ expect(screen.getByText('React Puzzle Games - Memory Puzzle')).toBeInTheDocument();
+ });
+
+ test('renders NEW GAME button', () => {
+ render(
+
+ );
+ expect(screen.getByText('NEW GAME')).toBeInTheDocument();
+ });
+
+ test('renders RESET GAME button', () => {
+ render(
+
+ );
+ expect(screen.getByText('RESET GAME')).toBeInTheDocument();
+ });
+
+ test('displays move count', () => {
+ render(
+
+ );
+ expect(screen.getByText('5')).toBeInTheDocument();
+ });
+
+ test('displays 0 moves by default', () => {
+ render(
+
+ );
+ expect(screen.getByText('0')).toBeInTheDocument();
+ });
+
+ test('calls onNewGame when NEW GAME button is clicked', () => {
+ render(
+
+ );
+ fireEvent.click(screen.getByText('NEW GAME'));
+ expect(mockOnNewGame).toHaveBeenCalledTimes(1);
+ });
+
+ test('calls onRestart when RESET GAME button is clicked', () => {
+ render(
+
+ );
+ fireEvent.click(screen.getByText('RESET GAME'));
+ expect(mockOnRestart).toHaveBeenCalledTimes(1);
+ });
+
+ test('updates displayed moves when prop changes', () => {
+ const { rerender } = render(
+
+ );
+ expect(screen.getByText('3')).toBeInTheDocument();
+
+ rerender(
+
+ );
+ expect(screen.getByText('10')).toBeInTheDocument();
+ });
+});
diff --git a/src/Footer.test.js b/src/Footer.test.js
new file mode 100644
index 0000000..1920a13
--- /dev/null
+++ b/src/Footer.test.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import Footer from './Footer';
+
+describe('Footer', () => {
+ test('renders footer element', () => {
+ const { container } = render(
);
+ expect(container.querySelector('footer')).toBeInTheDocument();
+ });
+
+ test('renders GitHub link', () => {
+ render(
);
+ const githubLink = screen.getByLabelText('View on GitHub');
+ expect(githubLink).toBeInTheDocument();
+ expect(githubLink).toHaveAttribute(
+ 'href',
+ 'https://github.com/ovidiubute/react-memory-puzzle'
+ );
+ });
+
+ test('GitHub link opens in new tab', () => {
+ render(
);
+ const githubLink = screen.getByLabelText('View on GitHub');
+ expect(githubLink).toHaveAttribute('target', '_blank');
+ expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+
+ test('renders React Puzzle Games link', () => {
+ render(
);
+ const orgLink = screen.getByText('React Puzzle Games');
+ expect(orgLink).toBeInTheDocument();
+ expect(orgLink).toHaveAttribute(
+ 'href',
+ 'https://github.com/react-puzzle-games'
+ );
+ });
+
+ test('React Puzzle Games link opens in new tab', () => {
+ render(
);
+ const orgLink = screen.getByText('React Puzzle Games');
+ expect(orgLink).toHaveAttribute('target', '_blank');
+ expect(orgLink).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+
+ test('renders "Made with" text', () => {
+ render(
);
+ expect(screen.getByText(/Made with/i)).toBeInTheDocument();
+ });
+
+ test('renders heart symbol', () => {
+ const { container } = render(
);
+ const heart = container.querySelector('.footerheart');
+ expect(heart).toBeInTheDocument();
+ expect(heart).toHaveTextContent('♥');
+ });
+
+ test('renders GitHubIcon component', () => {
+ const { container } = render(
);
+ const svg = container.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ expect(svg).toHaveAttribute('viewBox', '0 0 435.549 435.549');
+ });
+
+ test('GitHubIcon is inside GitHub link', () => {
+ const { container } = render(
);
+ const githubLink = container.querySelector('.footer-github');
+ const svg = githubLink.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ });
+
+ test('renders complete footer text', () => {
+ const { container } = render(
);
+ const footerText = container.querySelector('.footer-text');
+ expect(footerText).toBeInTheDocument();
+ });
+});
diff --git a/src/GameStats.test.js b/src/GameStats.test.js
new file mode 100644
index 0000000..09969d3
--- /dev/null
+++ b/src/GameStats.test.js
@@ -0,0 +1,147 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import GameStats from './GameStats';
+import { GAME_STARTED, GAME_WON } from './game-states';
+
+describe('GameStats', () => {
+ const mockOnRestart = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders GameStats container', () => {
+ const { container } = render(
+
+ );
+ expect(container.querySelector('.GameStats')).toBeInTheDocument();
+ });
+
+ test('shows nothing when game is in GAME_STARTED state', () => {
+ const { container } = render(
+
+ );
+ const wrapper = container.querySelector('.GameStats-Wrapper');
+ expect(wrapper).not.toBeInTheDocument();
+ });
+
+ test('shows congratulations message when game is won', () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(/Congrats! You completed the puzzle in 10 moves!/i)
+ ).toBeInTheDocument();
+ });
+
+ test('shows restart button when game is won', () => {
+ render(
+
+ );
+ expect(screen.getByText('Play again')).toBeInTheDocument();
+ });
+
+ test('calls onRestart when Play again button is clicked', () => {
+ render(
+
+ );
+ fireEvent.click(screen.getByText('Play again'));
+ expect(mockOnRestart).toHaveBeenCalledTimes(1);
+ });
+
+ test('displays correct move count in win message', () => {
+ const { rerender } = render(
+
+ );
+ expect(
+ screen.getByText(/completed the puzzle in 5 moves/i)
+ ).toBeInTheDocument();
+
+ rerender(
+
+ );
+ expect(
+ screen.getByText(/completed the puzzle in 20 moves/i)
+ ).toBeInTheDocument();
+ });
+
+ test('uses default moves value of 0 when not provided', () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(/completed the puzzle in 0 moves/i)
+ ).toBeInTheDocument();
+ });
+
+ test('shows win message with singular move', () => {
+ render(
+
+ );
+ // The component uses "moves" regardless of count, but let's verify the number
+ expect(
+ screen.getByText(/completed the puzzle in 1 move/i)
+ ).toBeInTheDocument();
+ });
+
+ test('transitions from GAME_STARTED to GAME_WON', () => {
+ const { rerender, container } = render(
+
+ );
+
+ let wrapper = container.querySelector('.GameStats-Wrapper');
+ expect(wrapper).not.toBeInTheDocument();
+
+ rerender(
+
+ );
+
+ wrapper = container.querySelector('.GameStats-Wrapper');
+ expect(wrapper).toBeInTheDocument();
+ expect(screen.getByText(/completed the puzzle in 5 moves/i)).toBeInTheDocument();
+ });
+});
diff --git a/src/GitHubIcon.test.js b/src/GitHubIcon.test.js
index a3a0f68..4d86479 100644
--- a/src/GitHubIcon.test.js
+++ b/src/GitHubIcon.test.js
@@ -1,8 +1,11 @@
-import renderer from 'react-test-renderer';
import React from 'react';
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
import GitHubIcon from './GitHubIcon';
test('GitHubIcon renders correctly', () => {
- const tree = renderer.create(
).toJSON();
- expect(tree).toMatchSnapshot();
+ const { container } = render(
);
+ const svg = container.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ expect(svg).toHaveAttribute('viewBox', '0 0 435.549 435.549');
});
diff --git a/src/RestartButton.test.js b/src/RestartButton.test.js
new file mode 100644
index 0000000..6558e59
--- /dev/null
+++ b/src/RestartButton.test.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import RestartButton from './RestartButton';
+
+describe('RestartButton', () => {
+ const mockOnClick = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders restart button', () => {
+ render(
);
+ expect(screen.getByText('Play again')).toBeInTheDocument();
+ });
+
+ test('renders button inside Game-Restart container', () => {
+ const { container } = render(
);
+ expect(container.querySelector('.Game-Restart')).toBeInTheDocument();
+ });
+
+ test('calls onClick when button is clicked', () => {
+ render(
);
+ fireEvent.click(screen.getByText('Play again'));
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ test('calls onClick multiple times on multiple clicks', () => {
+ render(
);
+ const button = screen.getByText('Play again');
+
+ fireEvent.click(button);
+ fireEvent.click(button);
+ fireEvent.click(button);
+
+ expect(mockOnClick).toHaveBeenCalledTimes(3);
+ });
+
+ test('button is a clickable element', () => {
+ render(
);
+ const button = screen.getByText('Play again');
+ expect(button.tagName).toBe('BUTTON');
+ });
+});
diff --git a/src/Tile.test.js b/src/Tile.test.js
new file mode 100644
index 0000000..631c896
--- /dev/null
+++ b/src/Tile.test.js
@@ -0,0 +1,110 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import Tile from './Tile';
+
+describe('Tile', () => {
+ const mockOnClick = jest.fn();
+ const mockOnFlip = jest.fn();
+
+ const defaultProps = {
+ id: 1,
+ name: 'React',
+ logo: 'react.png',
+ flipped: false,
+ onClick: mockOnClick,
+ onFlip: mockOnFlip,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ test('renders tile container', () => {
+ const { container } = render(
);
+ expect(container.querySelector('.Tile-container')).toBeInTheDocument();
+ });
+
+ test('renders with question mark on front face when not flipped', () => {
+ const { container } = render(
);
+ expect(container.querySelector('.question-mark')).toHaveTextContent('?');
+ });
+
+ test('renders with correct logo background', () => {
+ const { container } = render(
);
+ const backFace = container.querySelector('.back > div');
+ expect(backFace).toHaveStyle({ backgroundImage: "url('assets/test-logo.png')" });
+ });
+
+ test('applies flipped class when flipped prop is true', () => {
+ const { container } = render(
);
+ expect(container.querySelector('.Tile-card')).toHaveClass('flipped');
+ });
+
+ test('does not apply flipped class when flipped prop is false', () => {
+ const { container } = render(
);
+ const card = container.querySelector('.Tile-card');
+ expect(card).not.toHaveClass('flipped');
+ });
+
+ test('calls onClick when tile is clicked', () => {
+ const { container } = render(
);
+ const tile = container.querySelector('.Tile-container');
+ fireEvent.click(tile);
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ test('flips tile temporarily when clicked and not permanently flipped', async () => {
+ const { container } = render(
);
+ const tile = container.querySelector('.Tile-container');
+
+ fireEvent.click(tile);
+
+ await waitFor(() => {
+ expect(mockOnFlip).toHaveBeenCalled();
+ });
+ });
+
+ test('applies no-transition class initially', () => {
+ const { container } = render(
);
+ const card = container.querySelector('.Tile-card');
+ expect(card).toHaveClass('no-transition');
+ });
+
+ test('removes no-transition class after mount', async () => {
+ const { container } = render(
);
+ const card = container.querySelector('.Tile-card');
+
+ await waitFor(() => {
+ expect(card).not.toHaveClass('no-transition');
+ });
+ });
+
+ test('updates flipped state when flipped prop changes', () => {
+ const { container, rerender } = render(
);
+ let card = container.querySelector('.Tile-card');
+ expect(card).not.toHaveClass('flipped');
+
+ rerender(
);
+ card = container.querySelector('.Tile-card');
+ expect(card).toHaveClass('flipped');
+ });
+
+ test('uses default logo when logo prop is empty', () => {
+ const { container } = render(
);
+ const backFace = container.querySelector('.back > div');
+ expect(backFace).toHaveStyle({ backgroundImage: "url('assets/default.png')" });
+ });
+
+ test('renders with correct tile id', () => {
+ render(
);
+ // The id is used internally, test it's passed correctly by checking component renders
+ expect(mockOnClick).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/TileGrid.test.js b/src/TileGrid.test.js
new file mode 100644
index 0000000..a59ee5a
--- /dev/null
+++ b/src/TileGrid.test.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import TileGrid from './TileGrid';
+
+describe('TileGrid', () => {
+ const mockOnClick = jest.fn();
+ const mockOnFlip = jest.fn();
+
+ const mockTiles = [
+ { id: 1, name: 'React', logo: 'react.png', flipped: false },
+ { id: 2, name: 'Vue', logo: 'vue.png', flipped: false },
+ { id: 3, name: 'Angular', logo: 'angular.png', flipped: true },
+ { id: 4, name: 'React', logo: 'react.png', flipped: false },
+ ];
+
+ const defaultProps = {
+ tiles: mockTiles,
+ gameId: 0,
+ onClick: mockOnClick,
+ onFlip: mockOnFlip,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders TileGrid container', () => {
+ const { container } = render(
);
+ expect(container.querySelector('.TileGrid')).toBeInTheDocument();
+ });
+
+ test('renders correct number of tiles', () => {
+ const { container } = render(
);
+ const tiles = container.querySelectorAll('.Tile-container');
+ expect(tiles).toHaveLength(mockTiles.length);
+ });
+
+ test('renders tiles with correct props', () => {
+ const { container } = render(
);
+ const tiles = container.querySelectorAll('.Tile-container');
+ expect(tiles.length).toBe(4);
+ });
+
+ test('calls onClick with correct tile when a tile is clicked', () => {
+ const { container } = render(
);
+ const tiles = container.querySelectorAll('.Tile-container');
+
+ fireEvent.click(tiles[0]);
+ expect(mockOnClick).toHaveBeenCalledWith(mockTiles[0]);
+ });
+
+ test('handles empty tiles array', () => {
+ const { container } = render(
+
+ );
+ const tiles = container.querySelectorAll('.Tile-container');
+ expect(tiles).toHaveLength(0);
+ });
+
+ test('renders tiles with unique keys based on gameId', () => {
+ const { container, rerender } = render(
+
+ );
+ const firstRender = container.innerHTML;
+
+ rerender(
);
+ const secondRender = container.innerHTML;
+
+ // Component should re-render with different keys
+ expect(firstRender).toBeDefined();
+ expect(secondRender).toBeDefined();
+ });
+
+ test('renders grid with many tiles', () => {
+ const manyTiles = Array.from({ length: 16 }, (_, i) => ({
+ id: i,
+ name: `Tile${i}`,
+ logo: `logo${i}.png`,
+ flipped: false,
+ }));
+
+ const { container } = render(
+
+ );
+ const tiles = container.querySelectorAll('.Tile-container');
+ expect(tiles).toHaveLength(16);
+ });
+
+ test('passes onFlip handler to all tiles', () => {
+ render(
);
+ // If tiles render successfully with onFlip prop, this test passes
+ expect(mockOnFlip).not.toHaveBeenCalled();
+ });
+
+ test('tiles reflect flipped state from props', () => {
+ const { container } = render(
);
+ const flippedCards = container.querySelectorAll('.Tile-card.flipped');
+
+ // Only one tile in mockTiles has flipped: true (index 2)
+ expect(flippedCards.length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/__snapshots__/GitHubIcon.test.js.snap b/src/__snapshots__/GitHubIcon.test.js.snap
deleted file mode 100644
index 0c09c1e..0000000
--- a/src/__snapshots__/GitHubIcon.test.js.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GitHubIcon renders correctly 1`] = `
-
-`;