diff --git a/README.md b/README.md index a747b53a..eadd0129 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,20 @@ $ git clone https://github.com/yourusername/github-tracker.git ```bash $ cd github-tracker ``` - 3. Run the frontend ```bash $ npm i $ npm run dev ``` +This project utilizes [Vitest](https://vitest.dev/) and React Testing Library to ensure UI reliability. + +To run the frontend test suite, use the following command: +```bash +npm run test:client +``` + + 4. Run the backend ```bash $ npm i diff --git a/package.json b/package.json index 43ad31cc..96087c28 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "vite build", "lint": "eslint .", "test": "vitest", + "test:client": "vitest", "test:backend": "jasmine spec/**/*.spec.cjs", "preview": "vite preview", "docker:dev": "docker compose --profile dev up --build", diff --git a/src/components/ActivityFeed.test.tsx b/src/components/ActivityFeed.test.tsx new file mode 100644 index 00000000..077e179d --- /dev/null +++ b/src/components/ActivityFeed.test.tsx @@ -0,0 +1,71 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { vi } from 'vitest'; +import ActivityFeed from './ActivityFeed'; + +// 1. Capture the original global fetch to prevent side effects +const originalFetch = global.fetch; + +const mockEvents = [ + { + id: '12345', + type: 'PushEvent', + created_at: new Date().toISOString(), + repo: { name: 'GitMetricsLab/github_tracker' } + } +]; + +// Helper to generate a full Response-like object to satisfy TypeScript +const createMockResponse = (data: any): Partial => ({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => data, +}); + +describe('ActivityFeed Component', () => { + beforeAll(() => { + // Mock fetch before the suite runs + global.fetch = vi.fn(); + }); + + afterEach(() => { + // Clear mock history between individual tests + vi.clearAllMocks(); + }); + + afterAll(() => { + // 2. Restore original fetch after the suite finishes to prevent leaks + global.fetch = originalFetch; + }); + + it('displays the loading state initially', () => { + vi.mocked(global.fetch).mockResolvedValueOnce(createMockResponse([]) as Response); + + render(); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('renders activity events after successful fetch', async () => { + vi.mocked(global.fetch).mockResolvedValueOnce(createMockResponse(mockEvents) as Response); + + render(); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + expect(screen.getByText('🚀 Commit pushed')).toBeInTheDocument(); + expect(screen.getByText(/GitMetricsLab\/github_tracker/)).toBeInTheDocument(); + }); + + it('displays a fallback message when no activity is found', async () => { + vi.mocked(global.fetch).mockResolvedValueOnce(createMockResponse([]) as Response); + + render(); + + await waitFor(() => { + expect(screen.getByText('No activity found')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Navbar.test.tsx b/src/components/Navbar.test.tsx new file mode 100644 index 00000000..573c6c87 --- /dev/null +++ b/src/components/Navbar.test.tsx @@ -0,0 +1,54 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { vi } from 'vitest'; +import Navbar from './Navbar'; +import { ThemeContext } from '../context/ThemeContext'; + +// Helper function to render components with required providers +const renderWithProviders = (ui: React.ReactElement, themeMode: 'light' | 'dark' = 'light') => { + const mockToggleTheme = vi.fn(); + + return render( + + + {ui} + + + ); +}; + +describe('Navbar Component', () => { + it('renders the brand name and logo', () => { + renderWithProviders(); + expect(screen.getByText('GitHub Tracker')).toBeInTheDocument(); + expect(screen.getByAltText('CRL Icon')).toBeInTheDocument(); + }); + + it('renders desktop navigation links', () => { + renderWithProviders(); + expect(screen.getByText('Home')).toBeInTheDocument(); + expect(screen.getByText('Tracker')).toBeInTheDocument(); + expect(screen.getByText('Contributors')).toBeInTheDocument(); + expect(screen.getByText('Login')).toBeInTheDocument(); + }); + + it('renders the correct theme toggle icon based on context', () => { + // Render with dark mode + renderWithProviders(, 'dark'); + const toggleButtons = screen.getAllByLabelText('Toggle Theme'); + + // Check that the toggle buttons are present (one for desktop, one for mobile) + expect(toggleButtons.length).toBeGreaterThan(0); + }); + + it('opens mobile menu when hamburger icon is clicked', () => { + renderWithProviders(); + + const menuButton = screen.getByLabelText('Toggle Menu'); + fireEvent.click(menuButton); + + // The mobile menu should now be visible with the links + const mobileLinks = screen.getAllByText('Home'); + expect(mobileLinks.length).toBe(2); // One desktop, one mobile + }); +});