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
- 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");
+ });
+});