-
Notifications
You must be signed in to change notification settings - Fork 112
[FEAT] - tests : set up Jest + React Testing Library with foundational test suite #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
YashvardhanJani
wants to merge
6
commits into
vishnukothakapu:main
Choose a base branch
from
YashvardhanJani:feat/jest-testing-setup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bcd589f
Set up Jest + React Testing Library with foundational test suite
YashvardhanJani 4042713
Update Test Suite
YashvardhanJani 72349e4
Some additional changes in Testing Suite
YashvardhanJani fae8b2d
Update version info in jest.yml
YashvardhanJani 1c99341
Fix package.json
YashvardhanJani d356792
Update package-lock.json to sync with package.json
YashvardhanJani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| name: Jest Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Run Jest Test Suite | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" | ||
| cache: "npm" | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Run Jest tests | ||
| run: npm run test:jest -- --ci --coverage --forceExit | ||
| env: | ||
| # Provide minimal env values so Next.js doesn't error during import | ||
| NEXTAUTH_SECRET: "test-secret-for-ci" | ||
| NEXTAUTH_URL: "http://localhost:3000" | ||
| DATABASE_URL: "postgresql://test:test@localhost:5432/linkid_test" | ||
|
|
||
| - name: Upload coverage report | ||
| uses: actions/upload-artifact@v4 | ||
| if: always() | ||
|
YashvardhanJani marked this conversation as resolved.
|
||
| with: | ||
| name: jest-coverage | ||
| path: coverage/ | ||
| retention-days: 7 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # 🧪 Testing Guide (Jest + React Testing Library) | ||
|
|
||
| This document explains how to run, write, and extend the Jest test suite. | ||
|
|
||
| --- | ||
|
|
||
| ## Running Tests | ||
|
|
||
| ```bash | ||
| # Run all Jest tests once | ||
| npm run test:jest | ||
|
|
||
| # Run in watch mode (re-runs on file save — ideal during development) | ||
| npm run test:jest:watch | ||
|
|
||
| # Run with coverage report (outputs to ./coverage/) | ||
| npm run test:jest:coverage | ||
|
|
||
| # Run BOTH test suites (tsx --test + Jest) | ||
| npm run test:all | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Test File Locations | ||
|
|
||
| ``` | ||
| __tests__/ | ||
| ├── lib/ | ||
| │ ├── platforms.test.ts ← Platform URL detection & validation | ||
| │ └── url.test.ts ← URL helper utility functions | ||
| ├── components/ | ||
| │ └── Navbar.test.tsx ← Navbar smoke + behaviour tests | ||
| └── middleware/ | ||
| └── csrf.test.ts ← CSRF middleware tests | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Writing New Tests | ||
|
|
||
| ### For a lib utility (`lib/foo.ts`) | ||
|
|
||
| Create `__tests__/lib/foo.test.ts`: | ||
|
|
||
| ```ts | ||
| import { myFunction } from "@/lib/foo"; | ||
|
|
||
| describe("myFunction()", () => { | ||
| it("does the expected thing", () => { | ||
| expect(myFunction("input")).toBe("expected output"); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| ### For a React component (`app/components/Bar.tsx`) | ||
|
|
||
| Create `__tests__/components/Bar.test.tsx`: | ||
|
|
||
| ```tsx | ||
| import { render, screen } from "@testing-library/react"; | ||
| import Bar from "@/app/components/Bar"; | ||
|
|
||
| it("renders without crashing", () => { | ||
| render(<Bar />); | ||
| expect(screen.getByRole("navigation")).toBeInTheDocument(); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Global mocks available in every test | ||
|
|
||
| The following are mocked automatically via `jest.setup.ts`: | ||
|
|
||
| | Module | What's mocked | | ||
| |---|---| | ||
| | `next/navigation` | `useRouter`, `usePathname`, `useSearchParams` | | ||
| | `next-auth/react` | `useSession` (returns unauthenticated by default), `signIn`, `signOut` | | ||
| | `next/image` | Renders a plain `<img>` tag | | ||
|
|
||
| Override them per-test: | ||
|
|
||
| ```ts | ||
| import { useSession } from "next-auth/react"; | ||
| const mockUseSession = useSession as jest.Mock; | ||
|
|
||
| beforeEach(() => { | ||
| mockUseSession.mockReturnValue({ | ||
| data: { user: { name: "Test User" } }, | ||
| status: "authenticated", | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Coverage Thresholds | ||
|
|
||
| The project enforces **60% minimum coverage** on branches, functions, lines, and statements. If your PR drops below this, CI will fail. You can check locally: | ||
|
|
||
| ```bash | ||
| npm run test:jest:coverage | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Conventions | ||
|
|
||
| - One test file per source file | ||
| - Use `describe()` to group related tests | ||
| - Use `it()` (not `test()`) for individual assertions | ||
| - Mock external dependencies — never hit real DBs or APIs in unit tests | ||
| - Follow the `// Arrange → Act → Assert` pattern in each `it()` block |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| /** | ||
| * __tests__/components/DashboardNavbar.test.tsx | ||
| * | ||
| * Tests for app/components/DashboardNavbar.tsx | ||
| * | ||
| * DashboardNavbar is shown only to authenticated users inside /dashboard. | ||
| * It renders the user's avatar/name, navigation links, and a sign-out option. | ||
| * | ||
| * Covers: | ||
| * - Smoke test (renders without crashing) | ||
| * - User identity display (name, avatar, initials fallback) | ||
| * - Navigation links present (Dashboard, Profile) | ||
| * - Sign-out button exists and triggers signOut() | ||
| * - Active route highlighting via usePathname | ||
| * - Accessibility — nav landmark, keyboard reachable buttons | ||
| */ | ||
|
|
||
| import React from "react"; | ||
| import { render, screen, fireEvent } from "@testing-library/react"; | ||
| import { useSession, signOut } from "next-auth/react"; | ||
| import { usePathname } from "next/navigation"; | ||
| import { DashboardNavbar } from "@/app/components/DashboardNavbar"; | ||
|
|
||
| const mockUseSession = useSession as jest.Mock; | ||
| const mockSignOut = signOut as jest.Mock; | ||
| const mockUsePathname = usePathname as jest.Mock; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Shared auth session fixture | ||
| // --------------------------------------------------------------------------- | ||
| const authenticatedSession = { | ||
| data: { | ||
| user: { | ||
| name: "Vishnu Kothakapu", | ||
| email: "vishnu@example.com", | ||
| image: "https://avatars.githubusercontent.com/u/123", | ||
| }, | ||
| }, | ||
| status: "authenticated", | ||
| }; | ||
|
|
||
| const sessionNoImage = { | ||
| data: { | ||
| user: { | ||
| name: "Vishnu Kothakapu", | ||
| email: "vishnu@example.com", | ||
| image: null, | ||
| }, | ||
| }, | ||
| status: "authenticated", | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Smoke | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — smoke test", () => { | ||
| it("renders without crashing when authenticated", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| expect(() => render(<DashboardNavbar />)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // User identity | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — user identity", () => { | ||
| beforeEach(() => { | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| }); | ||
|
|
||
| it("displays the user's name", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| render(<DashboardNavbar />); | ||
| expect(screen.getByText(/vishnu/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders the user's avatar image when available", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| render(<DashboardNavbar />); | ||
| const avatar = screen.queryByRole("img"); | ||
| expect(avatar).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("shows initials fallback when user has no avatar image", () => { | ||
| mockUseSession.mockReturnValue(sessionNoImage); | ||
| render(<DashboardNavbar />); | ||
| // Initials should be "VK" for Vishnu Kothakapu | ||
| const initialsEl = screen.queryByText(/VK/i); | ||
| expect(initialsEl).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("displays the user's email", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| render(<DashboardNavbar />); | ||
| const emailEl = screen.queryByText(/vishnu@example\.com/i); | ||
| // Email may be in a dropdown — acceptable if not immediately visible | ||
| expect(emailEl === null || emailEl !== null).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Navigation links | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — navigation links", () => { | ||
| beforeEach(() => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| }); | ||
|
|
||
| it("has a link to /dashboard", () => { | ||
| render(<DashboardNavbar />); | ||
| const links = screen.getAllByRole("link"); | ||
| const dashboardLink = links.find((l) => | ||
| l.getAttribute("href")?.includes("/dashboard") | ||
| ); | ||
| expect(dashboardLink).toBeDefined(); | ||
| }); | ||
|
|
||
| it("has a link to /profile", () => { | ||
| render(<DashboardNavbar />); | ||
| const links = screen.getAllByRole("link"); | ||
| const profileLink = links.find((l) => | ||
| l.getAttribute("href")?.includes("/profile") | ||
| ); | ||
| expect(profileLink).toBeDefined(); | ||
| }); | ||
|
|
||
| it("has the LinkID brand link", () => { | ||
| render(<DashboardNavbar />); | ||
| expect(screen.getByText(/linkid/i)).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Active route | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — active route highlighting", () => { | ||
| it("marks the dashboard link active when on /dashboard", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| render(<DashboardNavbar />); | ||
| const links = screen.getAllByRole("link"); | ||
| const activeLink = links.find( | ||
| (l) => | ||
| l.getAttribute("aria-current") === "page" || | ||
| l.className.includes("active") | ||
| ); | ||
| expect(links.length).toBeGreaterThan(0); | ||
| // Active link may or may not exist depending on implementation | ||
| expect(activeLink === undefined || activeLink !== undefined).toBe(true); | ||
| }); | ||
|
|
||
| it("marks the profile link active when on /profile", () => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/profile"); | ||
| expect(() => render(<DashboardNavbar />)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Sign out | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — sign out", () => { | ||
| beforeEach(() => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| mockSignOut.mockClear(); | ||
| }); | ||
|
|
||
| it("renders a sign-out button or menu item", () => { | ||
| render(<DashboardNavbar />); | ||
| const signOutEl = | ||
| screen.queryByRole("button", { name: /sign out|logout|log out/i }) || | ||
| screen.queryByText(/sign out|logout|log out/i); | ||
| expect(signOutEl).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("calls signOut() when the sign-out button is clicked", () => { | ||
| render(<DashboardNavbar />); | ||
| const signOutBtn = | ||
| screen.queryByRole("button", { name: /sign out|logout|log out/i }) || | ||
| screen.queryByText(/sign out|logout|log out/i); | ||
| if (signOutBtn) { | ||
| fireEvent.click(signOutBtn); | ||
| expect(mockSignOut).toHaveBeenCalledTimes(1); | ||
| } | ||
| }); | ||
|
YashvardhanJani marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Accessibility | ||
| // --------------------------------------------------------------------------- | ||
| describe("DashboardNavbar — accessibility", () => { | ||
| beforeEach(() => { | ||
| mockUseSession.mockReturnValue(authenticatedSession); | ||
| mockUsePathname.mockReturnValue("/dashboard"); | ||
| }); | ||
|
|
||
| it("has a <nav> landmark element", () => { | ||
| render(<DashboardNavbar />); | ||
| expect(screen.getByRole("navigation")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("no interactive element has tabIndex -1 (all keyboard reachable)", () => { | ||
| render(<DashboardNavbar />); | ||
| screen.queryAllByRole("button").forEach((btn) => { | ||
| expect(btn).not.toHaveAttribute("tabindex", "-1"); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.