From 07a8c3e653cb0ff01a15bc1258b9d98d06c072d5 Mon Sep 17 00:00:00 2001 From: gideononiru <315517967+gideononiru@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:13:05 +0100 Subject: [PATCH 1/7] fix: resolve ESLint errors blocking the lint CI step - CallbackClient.test.tsx / DashboardShell.test.tsx / Navbar.test.tsx: replace require()-based jest mock overrides and `any` casts with proper ES imports typed against jest.Mock, and fix a leftover `usePathname.mockReturnValue` call that should have been `mockedUsePathname.mockReturnValue`. - WalletContext.tsx: replace the setState(prev => ...) functional-updater read of previous state inside the logout-clearing effect (flagged by react-hooks/set-state-in-effect) with a ref kept current after every render, preserving the original behavior without peeking at previous state from inside an effect. --- src/app/auth/callback/CallbackClient.test.tsx | 8 +++--- .../dashboard/DashboardShell.test.tsx | 26 ++++++++++++++----- src/components/layout/Navbar.test.tsx | 10 ++++--- src/context/WalletContext.tsx | 23 +++++++++++----- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/app/auth/callback/CallbackClient.test.tsx b/src/app/auth/callback/CallbackClient.test.tsx index ac5d12a..17fec29 100644 --- a/src/app/auth/callback/CallbackClient.test.tsx +++ b/src/app/auth/callback/CallbackClient.test.tsx @@ -1,13 +1,13 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { CallbackClient } from "./CallbackClient"; import { useAuth } from "@/context/AuthContext"; import type { AuthUser } from "@/types"; jest.mock("next/navigation", () => ({ useSearchParams: jest.fn(), - useRouter: () => ({ replace: jest.fn() }), + useRouter: jest.fn(() => ({ replace: jest.fn() })), })); jest.mock("@/context/AuthContext", () => ({ @@ -20,6 +20,7 @@ const mockReplace = jest.fn(); const mockedUseAuth = useAuth as jest.MockedFunction; // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockedUseSearchParams = useSearchParams as jest.MockedFunction; +const mockedUseRouter = useRouter as jest.Mock; function makeUser(roles: string[]): AuthUser { return { @@ -35,8 +36,7 @@ function makeUser(roles: string[]): AuthUser { describe("CallbackClient — role-based redirect (issue #77)", () => { beforeEach(() => { jest.clearAllMocks(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (require("next/navigation").useRouter = () => ({ replace: mockReplace })); + mockedUseRouter.mockReturnValue({ replace: mockReplace }); mockedUseSearchParams.mockReturnValue(new URLSearchParams({ token: "jwt-token" })); }); diff --git a/src/components/dashboard/DashboardShell.test.tsx b/src/components/dashboard/DashboardShell.test.tsx index 6cd5f93..0042a9c 100644 --- a/src/components/dashboard/DashboardShell.test.tsx +++ b/src/components/dashboard/DashboardShell.test.tsx @@ -7,27 +7,39 @@ * and the role-switcher section rendering the correct active role. */ +import React from "react"; import { render, screen } from "@testing-library/react"; +import { usePathname } from "next/navigation"; import { DashboardShell } from "./DashboardShell"; jest.mock("next/navigation", () => ({ usePathname: jest.fn(), })); -const { usePathname } = require("next/navigation"); +const mockedUsePathname = usePathname as jest.Mock; -// Stub next/link to render a plain so queries work in jsdom +// Stub next/link to render a plain so queries work in jsdom. +// `require("react")` (rather than a top-level import reference) is +// necessary here: jest.mock() factories are hoisted above imports and may +// only reference out-of-scope variables prefixed "mock", so this can't be +// rewritten as a normal ES import without breaking that hoisting contract. jest.mock("next/link", () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const React = require("react"); - return React.forwardRef(function Link({ href, children, ...rest }: any, ref: any) { + return React.forwardRef(function Link( + { href, children, ...rest }: { href: string; children?: React.ReactNode } & Record, + ref: React.Ref, + ) { return {children}; }); }); -function shell(role: string, pathname: string) { - usePathname.mockReturnValue(pathname); +type Role = "contributor" | "maintainer" | "sponsor"; + +function shell(role: Role, pathname: string) { + mockedUsePathname.mockReturnValue(pathname); return render( - +

child content

, ); @@ -124,7 +136,7 @@ describe("DashboardShell — content rendering", () => { }); it("renders the subtitle when provided", () => { - usePathname.mockReturnValue("/dashboard/contributor"); + mockedUsePathname.mockReturnValue("/dashboard/contributor"); render(

child

diff --git a/src/components/layout/Navbar.test.tsx b/src/components/layout/Navbar.test.tsx index 88437ab..5f5bba6 100644 --- a/src/components/layout/Navbar.test.tsx +++ b/src/components/layout/Navbar.test.tsx @@ -8,7 +8,9 @@ */ import { render, screen } from "@testing-library/react"; +import { useAuth } from "@/context/AuthContext"; import { Navbar } from "./Navbar"; +import type { AuthUser } from "@/types"; jest.mock("@/context/AuthContext", () => ({ useAuth: jest.fn(), @@ -32,10 +34,10 @@ jest.mock("@/components/ui/Button", () => ({ ), })); -const { useAuth } = require("@/context/AuthContext"); +const mockedUseAuth = useAuth as jest.Mock; -function mockAuth(overrides: Partial<{ user: any; loading: boolean; logout: jest.fn }>) { - useAuth.mockReturnValue({ +function mockAuth(overrides: Partial<{ user: AuthUser | null; loading: boolean; logout: () => void }>) { + mockedUseAuth.mockReturnValue({ user: null, loading: false, logout: jest.fn(), @@ -75,7 +77,7 @@ describe("Navbar — signed out", () => { }); describe("Navbar — signed in", () => { - const fakeUser = { + const fakeUser: AuthUser = { id: "u1", username: "octocat", displayName: "The Octocat", diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 4a1e8a3..c9e3c0e 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -102,17 +102,26 @@ export function WalletProvider({ children }: { children: React.ReactNode }) { }, []); useCrossTabStorage(WALLET_KEY, handleWalletKeyChangedElsewhere); + // Tracks the latest `address` for the logout-clearing effect below + // without making that effect depend on (and therefore re-run on) every + // address change — only an actual `user` transition should trigger a + // clear. Kept current after every render rather than read via a + // `setAddress(prev => ...)` functional updater, which is exactly the + // "peek at previous state inside an effect" shape + // react-hooks/set-state-in-effect flags. + const addressRef = useRef(address); + useEffect(() => { + addressRef.current = address; + }); + // #270: When AuthContext logs the user out (cross-tab or otherwise), // clear the wallet connection too so a stale address is never usable // in a tab where the session has ended. useEffect(() => { - if (user === null) { - setAddress((prev) => { - if (prev === null) return prev; - window.localStorage.removeItem(WALLET_KEY); - setNetwork(null); - return null; - }); + if (user === null && addressRef.current !== null) { + window.localStorage.removeItem(WALLET_KEY); + setNetwork(null); + setAddress(null); } }, [user]); From b52109450bd333d5c41ca0a47ec74b62c59ab43a Mon Sep 17 00:00:00 2001 From: gideononiru <315517967+gideononiru@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:13:14 +0100 Subject: [PATCH 2/7] fix: correct stale test expectations and an incomplete test mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tabs.test.tsx: query by role "tab", not "button" — Tabs.tsx renders