diff --git a/package.json b/package.json index f2d89f5..e5b4fe1 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite --host", "build": "vite build", "lint": "eslint .", + "test": "vitest", "preview": "vite preview", "docker:dev": "docker compose --profile dev up --build", "docker:prod": "docker compose --profile prod up -d --build" @@ -32,6 +33,9 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/jasmine": "^5.1.8", "@types/node": "^22.10.1", "@types/react": "^18.3.23", @@ -48,9 +52,11 @@ "express-session": "^1.18.2", "globals": "^15.11.0", "jasmine": "^5.9.0", + "jsdom": "^29.1.1", "passport": "^0.7.0", "passport-local": "^1.0.0", "supertest": "^7.1.4", - "vite": "^5.4.10" + "vite": "^5.4.10", + "vitest": "^4.1.6" } } diff --git a/src/components/__test__/Navbar.test.tsx b/src/components/__test__/Navbar.test.tsx new file mode 100644 index 0000000..780b9bb --- /dev/null +++ b/src/components/__test__/Navbar.test.tsx @@ -0,0 +1,91 @@ +// src/components/__tests__/Navbar.test.tsx +import { render, screen, fireEvent } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { MemoryRouter } from 'react-router-dom' +import { ThemeContext } from "../../context/ThemeContext"; +import Navbar from '../Navbar.tsx' + +// Helper to render Navbar with a mock ThemeContext +const renderNavbar = (mode: 'light' | 'dark' = 'light') => { + const toggleTheme = vi.fn() + render( + + + + + + ) + return { toggleTheme } +} + +describe('Navbar', () => { + // --- Rendering --- + it('renders the GitHub Tracker logo link', () => { + renderNavbar() + expect(screen.getByText('GitHub Tracker')).toBeInTheDocument() + }) + + it('renders all desktop nav links', () => { + renderNavbar() + expect(screen.getByRole('link', { name: /home/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /^tracker$/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /contributors/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /login/i })).toBeInTheDocument() + }) + + // --- Theme toggle --- + it('shows Moon icon in light mode', () => { + renderNavbar('light') + // Lucide renders an — check the button exists and toggleTheme is wired + const themeBtn = screen.getAllByRole('button')[0] + expect(themeBtn).toBeInTheDocument() + }) + + it('calls toggleTheme when the theme button is clicked', () => { + const { toggleTheme } = renderNavbar('light') + const themeBtn = screen.getAllByRole('button')[0] + fireEvent.click(themeBtn) + expect(toggleTheme).toHaveBeenCalledTimes(1) + }) + + // --- Mobile menu --- + it('mobile menu is hidden by default', () => { + renderNavbar() + expect(screen.queryByText('About')).not.toBeInTheDocument() + }) + + it('opens mobile menu when hamburger is clicked', () => { + renderNavbar() + const hamburger = screen.getAllByRole('button')[1] // second button = hamburger + fireEvent.click(hamburger) + expect(screen.getByText('About')).toBeInTheDocument() + expect(screen.getByText('Contact')).toBeInTheDocument() + }) + + it('closes mobile menu when a nav link is clicked', () => { + renderNavbar() + const hamburger = screen.getAllByRole('button')[1] + fireEvent.click(hamburger) // open + const homeLinks = screen.getAllByRole('link', { name: /home/i }) + fireEvent.click(homeLinks[homeLinks.length - 1]) // click the mobile one + expect(screen.queryByText('About')).not.toBeInTheDocument() // closed + }) + + it('calls toggleTheme from the mobile menu button', () => { + const { toggleTheme } = renderNavbar('dark') + const hamburger = screen.getAllByRole('button')[1] + fireEvent.click(hamburger) + fireEvent.click(screen.getByText(/light/i)) + expect(toggleTheme).toHaveBeenCalledTimes(1) + }) + + // --- Returns null when ThemeContext is missing --- + it('renders nothing if ThemeContext is not provided', () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + }) +}) \ No newline at end of file diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..010b0b5 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..f2366b8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], -}) + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + }, +}) \ No newline at end of file