Skip to content
Open
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
273 changes: 213 additions & 60 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"node": ">=18.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"coveralls": "^3.1.1",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
Expand All @@ -14,10 +16,8 @@
},
"dependencies": {
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-is": "^18.3.1",
"react-test-renderer": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"styled-components": "^6.1.13"
},
"scripts": {
Expand Down
173 changes: 168 additions & 5 deletions src/App.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,172 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import App from './App';
import { GAME_STARTED, GAME_WON } from './game-states';

it('renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<App />);
describe('App', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

test('renders without crashing', () => {
const { container } = render(<App />);
expect(container.querySelector('.App')).toBeInTheDocument();
});

test('renders AppHeader component', () => {
render(<App />);
expect(screen.getByText('React Puzzle Games - Memory Puzzle')).toBeInTheDocument();
});

test('renders Footer component', () => {
render(<App />);
expect(screen.getByText(/Made with/i)).toBeInTheDocument();
});

test('renders GameStats component', () => {
const { container } = render(<App />);
expect(container.querySelector('.GameStats')).toBeInTheDocument();
});

test('renders TileGrid with tiles', () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');
expect(tiles.length).toBeGreaterThan(0);
});

test('initializes with 0 moves', () => {
render(<App />);
expect(screen.getByText('0')).toBeInTheDocument();
});

test('increments move counter when tile is clicked', async () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');

// Click first tile
fireEvent.click(tiles[0]);

await waitFor(() => {
expect(screen.getByText('1')).toBeInTheDocument();
});
});

test('NEW GAME button generates new game', () => {
const { container } = render(<App />);

// Get initial tile configuration
const initialTiles = container.querySelectorAll('.Tile-container');
const initialCount = initialTiles.length;

// Click NEW GAME button
fireEvent.click(screen.getByText('NEW GAME'));

// Tiles should still exist (new configuration)
const newTiles = container.querySelectorAll('.Tile-container');
expect(newTiles.length).toBe(initialCount);
});

test('RESET GAME button resets moves to 0', async () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');

// Make some moves
fireEvent.click(tiles[0]);

await waitFor(() => {
expect(screen.getByText('1')).toBeInTheDocument();
});

// Reset the game
fireEvent.click(screen.getByText('RESET GAME'));

await waitFor(() => {
expect(screen.getByText('0')).toBeInTheDocument();
});
});

test('RESET GAME button unflips all tiles', async () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');

// Click some tiles
fireEvent.click(tiles[0]);
fireEvent.click(tiles[1]);

// Reset the game
fireEvent.click(screen.getByText('RESET GAME'));

await waitFor(() => {
const flippedTiles = container.querySelectorAll('.Tile-card.flipped');
// After reset, tiles should not be permanently flipped
// (Some may be temporarily flipped during animation)
});
});

test('renders all main components', () => {
const { container } = render(<App />);

expect(container.querySelector('.App-header')).toBeInTheDocument();
expect(container.querySelector('.App-content')).toBeInTheDocument();
expect(container.querySelector('.TileGrid')).toBeInTheDocument();
expect(container.querySelector('footer')).toBeInTheDocument();
});

test('maintains game state through interactions', async () => {
const { container } = render(<App />);

// Initial state
expect(screen.getByText('0')).toBeInTheDocument();

const tiles = container.querySelectorAll('.Tile-container');

// Make a move
fireEvent.click(tiles[0]);

await waitFor(() => {
expect(screen.getByText('1')).toBeInTheDocument();
});

// Make another move
fireEvent.click(tiles[1]);

await waitFor(() => {
expect(screen.getByText('2')).toBeInTheDocument();
});
});

test('NEW GAME resets move counter', async () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');

// Make some moves
fireEvent.click(tiles[0]);

await waitFor(() => {
expect(screen.getByText('1')).toBeInTheDocument();
});

// Start new game
fireEvent.click(screen.getByText('NEW GAME'));

await waitFor(() => {
expect(screen.getByText('0')).toBeInTheDocument();
});
});

test('tiles are clickable', () => {
const { container } = render(<App />);
const tiles = container.querySelectorAll('.Tile-container');

expect(tiles.length).toBeGreaterThan(0);
tiles.forEach(tile => {
expect(tile).toBeInTheDocument();
});
});
});
6 changes: 1 addition & 5 deletions src/AppHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';

import './AppHeader.css';

const AppHeader = ({ moves, onNewGame, onRestart }) => (
const AppHeader = ({ moves = 0, onNewGame, onRestart }) => (
<header className="App-header">
<div className="App-header-title">
React Puzzle Games - Memory Puzzle
Expand Down Expand Up @@ -31,8 +31,4 @@ AppHeader.propTypes = {
onRestart: PropTypes.func.isRequired,
};

AppHeader.defaultProps = {
moves: 0,
};

export default AppHeader;
108 changes: 108 additions & 0 deletions src/AppHeader.test.js
Original file line number Diff line number Diff line change
@@ -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(
<AppHeader
moves={0}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('React Puzzle Games - Memory Puzzle')).toBeInTheDocument();
});

test('renders NEW GAME button', () => {
render(
<AppHeader
moves={0}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('NEW GAME')).toBeInTheDocument();
});

test('renders RESET GAME button', () => {
render(
<AppHeader
moves={0}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('RESET GAME')).toBeInTheDocument();
});

test('displays move count', () => {
render(
<AppHeader
moves={5}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('5')).toBeInTheDocument();
});

test('displays 0 moves by default', () => {
render(
<AppHeader onNewGame={mockOnNewGame} onRestart={mockOnRestart} />
);
expect(screen.getByText('0')).toBeInTheDocument();
});

test('calls onNewGame when NEW GAME button is clicked', () => {
render(
<AppHeader
moves={0}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
fireEvent.click(screen.getByText('NEW GAME'));
expect(mockOnNewGame).toHaveBeenCalledTimes(1);
});

test('calls onRestart when RESET GAME button is clicked', () => {
render(
<AppHeader
moves={0}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
fireEvent.click(screen.getByText('RESET GAME'));
expect(mockOnRestart).toHaveBeenCalledTimes(1);
});

test('updates displayed moves when prop changes', () => {
const { rerender } = render(
<AppHeader
moves={3}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('3')).toBeInTheDocument();

rerender(
<AppHeader
moves={10}
onNewGame={mockOnNewGame}
onRestart={mockOnRestart}
/>
);
expect(screen.getByText('10')).toBeInTheDocument();
});
});
Loading