diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..d98345b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,12 +5,12 @@ import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, - // Override default ignores of eslint-config-next. + // Ignores build outputs and test coverage reports (#252) globalIgnores([ - // Default ignores of eslint-config-next: ".next/**", "out/**", "build/**", + "coverage/**", "next-env.d.ts", ]), ]); diff --git a/jest.config.ts b/jest.config.ts index 981cdc9..f633ab8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -23,12 +23,11 @@ const config: Config = { { plugins: ["@babel/plugin-transform-modules-commonjs"] }, ], }, - // react-markdown's dependency tree (remark/rehype/unified/hast/mdast/ - // micromark, and each of *their* sub-dependencies) is too deep to name - // exhaustively — new transitive packages kept surfacing one at a time. - // Transform all of node_modules rather than maintain a brittle allowlist; - // this only affects Jest's test run, never the production build. - transformIgnorePatterns: [], + // Targeted negative lookahead: only transform ESM packages that actually require + // CommonJS transpilation, leaving standard CJS packages untouched (#251). + transformIgnorePatterns: [ + "node_modules/(?!(react-markdown|remark-.*|rehype-.*|unified|hast-.*|mdast-.*|micromark.*|vfile.*|unist-.*|bail|is-plain-obj|trough|zwitch|longest-streak|ccount|escape-string-regexp|markdown-table|trim-lines|decode-named-character-reference|character-entities.*|devlop|comma-separated-tokens|space-separated-tokens|property-information|html-void-elements|html-url-attributes|estree-util-is-identifier-name)/)", + ], }; export default config; diff --git a/jest.setup.ts b/jest.setup.ts index e9e0a1c..93b8c61 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,3 +1,21 @@ // Extend Jest's built-in matchers with jest-dom's DOM-specific matchers // (e.g. toBeInTheDocument, toHaveAttribute, toHaveClass) import "@testing-library/jest-dom"; + +// Ensure environment variables required by src/lib/config.ts are preset in test env +process.env.NEXT_PUBLIC_STELLAR_NETWORK = "TESTNET"; + +// Mock window.matchMedia for JSDOM +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); diff --git a/src/components/bounty/BountyCard.tsx b/src/components/bounty/BountyCard.tsx index f9e9fd5..d5459ce 100644 --- a/src/components/bounty/BountyCard.tsx +++ b/src/components/bounty/BountyCard.tsx @@ -18,6 +18,7 @@ export function BountyCard({ bounty }: { bounty: Bounty }) { return (
diff --git a/src/components/bounty/BountyDescription.test.tsx b/src/components/bounty/BountyDescription.test.tsx index 50c1b54..aac6f86 100644 --- a/src/components/bounty/BountyDescription.test.tsx +++ b/src/components/bounty/BountyDescription.test.tsx @@ -1,5 +1,5 @@ /** - * Tests for BountyDescription (#89). + * Tests for BountyDescription (#89, #272). * * bounty.description is raw, untrusted, third-party GitHub issue Markdown * with no backend sanitization guaranteed anywhere in this pipeline. These @@ -47,6 +47,40 @@ describe("BountyDescription — content fidelity", () => { expect(screen.getByText("first step").closest("li")).toBeInTheDocument(); expect(screen.getByText("second step").closest("li")).toBeInTheDocument(); }); + + it("renders an ordered list as real
  1. elements (#272)", () => { + render(); + + const first = screen.getByText("first numbered item"); + expect(first.closest("ol")).toBeInTheDocument(); + expect(first.closest("li")).toBeInTheDocument(); + }); + + it("renders blockquotes with quote styling (#272)", () => { + render( This is a quoted note"} />); + + const quote = screen.getByText("This is a quoted note"); + expect(quote.closest("blockquote")).toBeInTheDocument(); + expect(quote.closest("blockquote")).toHaveClass("border-l-2"); + }); + + it("remaps h1, h2, h3 markdown headers to h2, h3, h4 DOM elements (#272)", () => { + render( + , + ); + + const h2 = screen.getByRole("heading", { level: 2, name: "Top Level Section" }); + const h3 = screen.getByRole("heading", { level: 3, name: "Sub Section" }); + const h4 = screen.getByRole("heading", { level: 4, name: "Sub-sub Section" }); + + expect(h2).toBeInTheDocument(); + expect(h3).toBeInTheDocument(); + expect(h4).toBeInTheDocument(); + // No h1 should exist in the description output + expect(screen.queryByRole("heading", { level: 1 })).not.toBeInTheDocument(); + }); }); describe("BountyDescription — untrusted-content hardening", () => { diff --git a/src/components/dashboard/DashboardShell.test.tsx b/src/components/dashboard/DashboardShell.test.tsx new file mode 100644 index 0000000..a49d93b --- /dev/null +++ b/src/components/dashboard/DashboardShell.test.tsx @@ -0,0 +1,90 @@ +import { render, screen } from "@testing-library/react"; +import { DashboardShell } from "./DashboardShell"; +import * as navigation from "next/navigation"; + +jest.mock("next/navigation", () => ({ + usePathname: jest.fn(), +})); + +describe("DashboardShell Component (#275)", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders contributor role nav items and highlights active route", () => { + (navigation.usePathname as jest.Mock).mockReturnValue("/dashboard/contributor"); + + render( + +
    Content Area
    +
    , + ); + + expect(screen.getByText("Contributor Dashboard")).toBeInTheDocument(); + expect(screen.getByText("Content Area")).toBeInTheDocument(); + + const overviewLink = screen.getByRole("link", { name: /Overview/i }); + expect(overviewLink).toBeInTheDocument(); + expect(overviewLink).toHaveAttribute("href", "/dashboard/contributor"); + // Check active class styling + expect(overviewLink).toHaveClass("bg-indigo-50"); + + const browseLink = screen.getByRole("link", { name: /Browse bounties/i }); + expect(browseLink).toBeInTheDocument(); + expect(browseLink).toHaveAttribute("href", "/issues"); + expect(browseLink).not.toHaveClass("bg-indigo-50"); + + expect(screen.getByRole("link", { name: /Reputation/i })).toBeInTheDocument(); + }); + + it("renders maintainer role nav items", () => { + (navigation.usePathname as jest.Mock).mockReturnValue("/milestones"); + + render( + +
    Maintainer View
    +
    , + ); + + expect(screen.getByText("Maintainer Dashboard")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /Bounty pipeline/i })).toHaveAttribute("href", "/issues"); + + const milestoneLink = screen.getByRole("link", { name: /Milestones/i }); + expect(milestoneLink).toHaveAttribute("href", "/milestones"); + expect(milestoneLink).toHaveClass("bg-indigo-50"); + + expect(screen.getByRole("link", { name: /Team/i })).toHaveAttribute("href", "/dashboard/maintainer"); + }); + + it("renders sponsor role nav items", () => { + (navigation.usePathname as jest.Mock).mockReturnValue("/dashboard/sponsor"); + + render( + +
    Sponsor View
    +
    , + ); + + expect(screen.getByText("Sponsor Dashboard")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /Bounties funded/i })).toHaveAttribute("href", "/issues"); + expect(screen.getByRole("link", { name: /Payments/i })).toHaveAttribute("href", "/dashboard/sponsor"); + }); + + it("renders role switcher links with active role emphasized", () => { + (navigation.usePathname as jest.Mock).mockReturnValue("/dashboard/contributor"); + + render( + +
    Content
    +
    , + ); + + const contributorSwitcher = screen.getByRole("link", { name: "Contributor" }); + const maintainerSwitcher = screen.getByRole("link", { name: "Maintainer" }); + const sponsorSwitcher = screen.getByRole("link", { name: "Sponsor" }); + + expect(contributorSwitcher).toHaveClass("font-medium"); + expect(maintainerSwitcher).toHaveClass("text-slate-500"); + expect(sponsorSwitcher).toHaveClass("text-slate-500"); + }); +}); diff --git a/src/components/layout/Navbar.test.tsx b/src/components/layout/Navbar.test.tsx new file mode 100644 index 0000000..a0c11f2 --- /dev/null +++ b/src/components/layout/Navbar.test.tsx @@ -0,0 +1,96 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { Navbar } from "./Navbar"; +import * as AuthContext from "@/context/AuthContext"; +import { ThemeProvider } from "@/context/ThemeContext"; + +jest.mock("@/context/AuthContext", () => ({ + useAuth: jest.fn(), +})); + +function renderNavbar() { + return render( + + + , + ); +} + +describe("Navbar Component (#276)", () => { + const mockLogout = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders loading state (no auth buttons or user info mounted)", () => { + (AuthContext.useAuth as jest.Mock).mockReturnValue({ + user: null, + loading: true, + logout: mockLogout, + }); + + renderNavbar(); + + expect(screen.getByText("MergeFi")).toBeInTheDocument(); + expect(screen.getByText("Bounties")).toBeInTheDocument(); + expect(screen.getByText("Milestones")).toBeInTheDocument(); + expect(screen.queryByText("Sign in")).not.toBeInTheDocument(); + expect(screen.queryByText("Connect GitHub")).not.toBeInTheDocument(); + expect(screen.queryByTitle("Sign out")).not.toBeInTheDocument(); + expect(screen.queryByText("Reputation")).not.toBeInTheDocument(); + }); + + it("renders signed-out state with Sign in and Connect GitHub buttons", () => { + (AuthContext.useAuth as jest.Mock).mockReturnValue({ + user: null, + loading: false, + logout: mockLogout, + }); + + renderNavbar(); + + expect(screen.getByText("Sign in")).toBeInTheDocument(); + expect(screen.getByText("Connect GitHub")).toBeInTheDocument(); + expect(screen.queryByTitle("Sign out")).not.toBeInTheDocument(); + expect(screen.queryByText("Reputation")).not.toBeInTheDocument(); + }); + + it("renders signed-in user state with display name, avatar link, reputation and logout button", () => { + (AuthContext.useAuth as jest.Mock).mockReturnValue({ + user: { + username: "alice_dev", + displayName: "Alice Developer", + avatarUrl: "https://example.com/alice.png", + }, + loading: false, + logout: mockLogout, + }); + + renderNavbar(); + + expect(screen.getByText("Alice Developer")).toBeInTheDocument(); + expect(screen.getByText("Reputation")).toBeInTheDocument(); + expect(screen.queryByText("Sign in")).not.toBeInTheDocument(); + expect(screen.queryByText("Connect GitHub")).not.toBeInTheDocument(); + + const signOutBtn = screen.getByTitle("Sign out"); + expect(signOutBtn).toBeInTheDocument(); + + fireEvent.click(signOutBtn); + expect(mockLogout).toHaveBeenCalledTimes(1); + }); + + it("renders dashboard links inside dropdown", () => { + (AuthContext.useAuth as jest.Mock).mockReturnValue({ + user: null, + loading: false, + logout: mockLogout, + }); + + renderNavbar(); + + expect(screen.getByText("Contributor")).toHaveAttribute("href", "/dashboard/contributor"); + expect(screen.getByText("Maintainer")).toHaveAttribute("href", "/dashboard/maintainer"); + expect(screen.getByText("Sponsor")).toHaveAttribute("href", "/dashboard/sponsor"); + }); +});