Skip to content

Fixed header Nest logo adaption to theme(light/dark)#4374

Open
pranav-871 wants to merge 1 commit intoOWASP:mainfrom
pranav-871:feature/fix-logo-theme
Open

Fixed header Nest logo adaption to theme(light/dark)#4374
pranav-871 wants to merge 1 commit intoOWASP:mainfrom
pranav-871:feature/fix-logo-theme

Conversation

@pranav-871
Copy link
Copy Markdown
Contributor

Proposed change

Making the OWASP Nest Header logo adaptable to light/dark modes
Resolves #4318

Summary

This PR fixes the header logo to adapt correctly based on the selected theme.

  • Light mode → displays dark logo
  • Dark mode → displays light logo
  • Updates dynamically when the theme changes

Changes

  • Added theme-based logic in Header.tsx using useTheme()
  • Updated logo rendering for both desktop and mobile views
  • Updated Header tests to reflect correct behavior
  • Fixed ScrollToTop test by simulating scroll behavior in the test environment

Testing

  • Verified that the logo updates correctly in both light and dark modes in the UI
  • Header-related tests are passing
  • ScrollToTop test now passes after fix

Note

Some unrelated frontend tests are failing locally due to timeouts and environment-specific issues (e.g., async handling and react-aria focus behavior). These appear to be unrelated to the changes introduced in this PR.

Checklist

  • [ x ] Required: I followed the contributing workflow
  • [ x ] Required: I verified that my code works as intended and resolves the issue as described
  • [ ] Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Summary by CodeRabbit

  • New Features

    • Header logo now dynamically switches between light and dark variants based on the currently active theme.
  • Tests

    • Updated logo rendering tests to verify correct variants display for each theme.
    • Enhanced scroll detection test with additional event simulation.

Walkthrough

The PR addresses a navbar logo visibility issue by implementing theme-aware logo selection in the Header component. The Header now uses the useTheme hook to dynamically switch between dark and light logo variants based on the active theme. Tests are updated to reflect the new expected logo source, and ScrollToTop test scroll simulation is enhanced.

Changes

Cohort / File(s) Summary
Header Component Theme-Awareness
frontend/src/components/Header.tsx, frontend/__tests__/unit/pages/Header.test.tsx
Added useTheme hook to derive logoSrc that switches between dark and light logo variants based on the active theme. Updated Header component's desktop and mobile Image components to use dynamic logoSrc. Test expectations updated from dark logo to light logo.
ScrollToTop Test Enhancement
frontend/__tests__/unit/components/ScrollToTop.test.tsx
Enhanced scroll threshold test to simulate an additional scroll trigger by setting window.scrollY to 500 and dispatching a scroll event on window alongside existing globalThis updates.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested labels

frontend, frontend-tests

Suggested reviewers

  • arkid15r
  • kasya
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing the header logo to adapt to light/dark themes, which is the primary objective of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the theme adaptation changes, updated tests, and testing verification.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #4318: theme-based logo switching in Header.tsx using useTheme(), rendering dark logo in light mode and light logo in dark mode, and updating related tests.
Out of Scope Changes check ✅ Passed All changes are directly related to resolving the linked issue #4318. The test modifications (ScrollToTop and Header tests) support the implementation and are necessary for validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Confidence score: 4/5

  • This PR looks safe to merge overall, with only a minor user-facing issue reported and no clear functional breakage.
  • In frontend/src/components/Header.tsx, deriving the logo from resolvedTheme during SSR can cause a light-to-dark logo swap on hydration for dark-theme users, leading to a brief visual flicker.
  • Given the moderate confidence and low-to-mild severity (4/10), this is a polish/regression risk rather than a merge-blocking defect.
  • Pay close attention to frontend/src/components/Header.tsx - SSR theme resolution can produce a hydration-time logo flicker for dark mode users.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/Header.tsx">

<violation number="1" location="frontend/src/components/Header.tsx:28">
P2: Theme-dependent logo is computed from `resolvedTheme` during SSR/initial render; next-themes returns `undefined` on the server, so this renders the light logo and then swaps on the client for dark users, causing flicker/hydration mismatch. Guard theme-dependent rendering until mounted or use a client-only render for the logo.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/__tests__/unit/pages/Header.test.tsx (1)

257-267: ⚠️ Potential issue | 🟡 Minor

Test name is inconsistent with assertion; test relies on implicit undefined behavior.

The test is named 'renders logo_dark.png image in both desktop and mobile header' but asserts logo_light.png. Additionally, per the context from jest.setup.ts, the global useTheme mock returns { theme: 'light', setTheme: jest.fn() } without resolvedTheme. This causes resolvedTheme to be undefined in tests, which makes the logo default to logo_light.png - but this is accidental, not intentional.

To properly test the theme-based logo behavior:

🔧 Suggested fix: rename test and add explicit theme testing
-    it('renders logo_dark.png image in both desktop and mobile header', () => {
+    it('renders logo_light.png when theme is not dark', () => {
       renderWithSession(<Header isGitHubAuthEnabled />)

       const logoImages = screen.getAllByRole('img', { name: /owasp logo/i })
       expect(logoImages.length).toBe(2)

       for (const logo of logoImages) {
         expect(logo).toHaveAttribute('src', '/img/logo_light.png')
         expect(logo).toBeInTheDocument()
       }
     })

Consider adding a test for dark theme by mocking resolvedTheme:

it('renders logo_dark.png when theme is dark', () => {
  const { useTheme } = jest.requireMock('next-themes')
  useTheme.mockReturnValue({ resolvedTheme: 'dark', setTheme: jest.fn() })

  renderWithSession(<Header isGitHubAuthEnabled />)

  const logoImages = screen.getAllByRole('img', { name: /owasp logo/i })
  for (const logo of logoImages) {
    expect(logo).toHaveAttribute('src', '/img/logo_dark.png')
  }
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/Header.test.tsx` around lines 257 - 267, The
test titled 'renders logo_dark.png image in both desktop and mobile header' is
misleading and relies on undefined resolvedTheme; update the existing test for
Header to explicitly mock next-themes' useTheme to return { resolvedTheme:
'light', setTheme: jest.fn() } and rename it to reflect that it expects
'/img/logo_light.png'; then add a new test that mocks useTheme to return {
resolvedTheme: 'dark', setTheme: jest.fn() } and asserts both Header img
elements have src '/img/logo_dark.png' so behavior for Header and useTheme is
deterministic.
🧹 Nitpick comments (1)
frontend/__tests__/unit/components/ScrollToTop.test.tsx (1)

27-35: Remove the dead globalThis scroll setup — test should use only window since the component does.

The ScrollToTop component reads window.scrollY and attaches its listener to window. The globalThis code at lines 27–28 is dead code; only the window dispatch triggers the component's listener. Additionally, the two different scroll values (400 vs 500) are unnecessary.

Simplify to:

♻️ Refactor
   test('The button should become visible after scrolling past the threshold', async () => {
     const { getByLabelText } = render(<ScrollToTop />)
     const button = getByLabelText(/scroll to top/i)
 
-    Object.defineProperty(globalThis, 'scrollY', { value: 400, writable: true })
-    globalThis.dispatchEvent(new Event('scroll'))
-
     Object.defineProperty(window, 'scrollY', {
-      value: 500,
+      value: 400,
       writable: true,
     })
     window.dispatchEvent(new Event('scroll'))

Note: Other tests in this file still use globalThis-only scroll simulation (lines 47, 63, 71, 87, 104, 108, 116). If this fix was necessary to make the test pass, verify whether those tests need the same adjustment or if they're passing incorrectly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ScrollToTop.test.tsx` around lines 27 -
35, Remove the dead globalThis scroll setup and use window consistently in the
ScrollToTop unit test: delete the Object.defineProperty(globalThis, 'scrollY',
...) and its dispatch, convert the window block to the single necessary action
(set window.scrollY to one value, e.g., 500, and dispatch new Event('scroll'))
so the component's window listener is actually exercised; also scan other tests
in this file that simulate scroll via globalThis (lines noted in review) and
update them to use window if they exercise the ScrollToTop component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/Header.tsx`:
- Around line 26-28: The logoSrc is computed from resolvedTheme immediately,
causing a hydration mismatch; add a mounted-check state (e.g., isMounted via
useState and set true in useEffect) and only compute or render logoSrc after
isMounted is true (or fall back to null/placeholder until mounted) so that
useTheme().resolvedTheme is stable on the client before choosing between
'/img/logo_dark.png' and '/img/logo_light.png'; update references to
resolvedTheme and logoSrc in the Header component to honor isMounted.

---

Outside diff comments:
In `@frontend/__tests__/unit/pages/Header.test.tsx`:
- Around line 257-267: The test titled 'renders logo_dark.png image in both
desktop and mobile header' is misleading and relies on undefined resolvedTheme;
update the existing test for Header to explicitly mock next-themes' useTheme to
return { resolvedTheme: 'light', setTheme: jest.fn() } and rename it to reflect
that it expects '/img/logo_light.png'; then add a new test that mocks useTheme
to return { resolvedTheme: 'dark', setTheme: jest.fn() } and asserts both Header
img elements have src '/img/logo_dark.png' so behavior for Header and useTheme
is deterministic.

---

Nitpick comments:
In `@frontend/__tests__/unit/components/ScrollToTop.test.tsx`:
- Around line 27-35: Remove the dead globalThis scroll setup and use window
consistently in the ScrollToTop unit test: delete the
Object.defineProperty(globalThis, 'scrollY', ...) and its dispatch, convert the
window block to the single necessary action (set window.scrollY to one value,
e.g., 500, and dispatch new Event('scroll')) so the component's window listener
is actually exercised; also scan other tests in this file that simulate scroll
via globalThis (lines noted in review) and update them to use window if they
exercise the ScrollToTop component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8480b194-7851-460b-97b6-e3e72caa11ef

📥 Commits

Reviewing files that changed from the base of the PR and between 2789f46 and de8a7bd.

📒 Files selected for processing (3)
  • frontend/__tests__/unit/components/ScrollToTop.test.tsx
  • frontend/__tests__/unit/pages/Header.test.tsx
  • frontend/src/components/Header.tsx

Comment on lines +26 to +28
const { resolvedTheme } = useTheme()

const logoSrc = resolvedTheme === 'dark' ? '/img/logo_dark.png' : '/img/logo_light.png'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

next-themes resolvedTheme undefined hydration mismatch

💡 Result:

The "next-themes resolvedTheme undefined hydration mismatch" error occurs because resolvedTheme from useTheme returns undefined during server-side rendering (SSR) in Next.js. This creates a mismatch between server-rendered HTML (where theme-dependent UI renders with undefined/default values) and client-side hydration (where localStorage/system preference resolves the theme). This is expected behavior documented in next-themes README: theme values like resolvedTheme are undefined on the server since localStorage and prefers-color-scheme are unavailable. Rendering UI based on them directly causes hydration warnings/errors. ## Primary Fix: Client-Only Rendering for Theme-Dependent UI Delay rendering components using resolvedTheme until client-side mount using useState + useEffect: tsx 'use client'; import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; function ThemedComponent { const { resolvedTheme, setTheme } = useTheme; const [mounted, setMounted] = useState(false); useEffect( => { setMounted(true); }, []); if (!mounted) { return

Loading...
; // Or a neutral placeholder (e.g., light theme image) } return (
Current theme: {resolvedTheme} <button onClick={ => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}> Toggle
); } For images/logos (common case): tsx function ThemedImage({ srcLight, srcDark }: { srcLight: string; srcDark: string }) { const { resolvedTheme } = useTheme; const [mounted, setMounted] = useState(false); useEffect( => setMounted(true), []); if (!mounted) { return Logo; // Fallback to light/default } return ( <Image src={resolvedTheme === 'dark' ? srcDark : srcLight} alt="Logo" /> ); } ## Additional Setup Requirements 1. ThemeProvider in layout.tsx (App Router): tsx // app/layout.tsx import { ThemeProvider } from 'next-themes'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <ThemeProvider attribute="class" // For Tailwind: 'class', else 'data-theme' defaultTheme="system" enableSystem disableTransitionOnChange > {children} ); } Critical: suppressHydrationWarning on prevents warnings from next-themes updating class/data-theme/color-scheme attributes. It only suppresses one level deep. 2. Tailwind config (if using attribute="class"): js // tailwind.config.js module.exports = { darkMode: 'class', // or 'selector' in Tailwind v3+ }; ## Why This Happens - Server renders with resolvedTheme: undefined → UI uses fallback/default. - Client hydrates, reads localStorage → resolvedTheme: 'dark'/'light' → UI changes → mismatch. - Official docs warn: "The above code is hydration unsafe... delay rendering until mounted." ## Other Workarounds (Less Recommended) - Dynamic import ThemeProvider: dynamic( => import('next-themes').then(mod => mod.ThemeProvider), { ssr: false }) → May cause FOUC (flash of unstyled content). - suppressHydrationWarning everywhere: Hides issues, doesn't fix root cause. This resolves the issue without flash in production (dev mode may flicker). Confirmed across Next.js 13-15, next-themes v0.3-0.4.

Citations:


🏁 Script executed:

find . -name "Header.tsx" -type f

Repository: OWASP/Nest

Length of output: 90


🏁 Script executed:

find . -name "layout.tsx" -o -name "layout.ts" | head -5

Repository: OWASP/Nest

Length of output: 290


🏁 Script executed:

find . -name "_app.tsx" -o -name "_app.ts" -o -name "app.tsx" | head -5

Repository: OWASP/Nest

Length of output: 36


🏁 Script executed:

cat -n ./frontend/src/components/Header.tsx

Repository: OWASP/Nest

Length of output: 11748


🏁 Script executed:

find . -name "layout.tsx" -path "*/src/app/*" | grep -E "src/app/layout\.tsx|src/app/\[.*\]/layout\.tsx" | head -1

Repository: OWASP/Nest

Length of output: 83


🏁 Script executed:

find . -path "*/src/app/layout.tsx" -type f

Repository: OWASP/Nest

Length of output: 83


🏁 Script executed:

cat -n ./frontend/src/app/layout.tsx

Repository: OWASP/Nest

Length of output: 3338


Add mounted state check before calculating logoSrc to prevent hydration mismatch.

The resolvedTheme from useTheme() is undefined until the theme resolves from localStorage on the client. Currently, logoSrc is calculated immediately (defaulting to light logo), which causes:

  1. Server renders with light logo
  2. Client hydrates with light logo
  3. Theme resolves to dark → Image switches to dark logo → hydration mismatch and flash of wrong logo

Since the file is already a client component with useState and useEffect imported, add a mounted check:

Suggested fix
export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthEnabled: boolean }) {
  const pathname = usePathname()
  const { resolvedTheme } = useTheme()
+  const [mounted, setMounted] = useState(false)

-  const logoSrc = resolvedTheme === 'dark' ? '/img/logo_dark.png' : '/img/logo_light.png'
   const [mobileMenuOpen, setMobileMenuOpen] = useState(false)

   const toggleMobileMenu = () => setMobileMenuOpen(!mobileMenuOpen)

+  useEffect(() => {
+    setMounted(true)
+  }, [])
+
+  const logoSrc = mounted && resolvedTheme === 'dark' ? '/img/logo_dark.png' : '/img/logo_light.png'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Header.tsx` around lines 26 - 28, The logoSrc is
computed from resolvedTheme immediately, causing a hydration mismatch; add a
mounted-check state (e.g., isMounted via useState and set true in useEffect) and
only compute or render logoSrc after isMounted is true (or fall back to
null/placeholder until mounted) so that useTheme().resolvedTheme is stable on
the client before choosing between '/img/logo_dark.png' and
'/img/logo_light.png'; update references to resolvedTheme and logoSrc in the
Header component to honor isMounted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Navbar logo does not adapt to light/dark theme, causing poor visibility in light mode

1 participant