// @vitest-environment jsdom import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, cleanup, screen } from '@testing-library/react'; // Mock next/router so useRouter() does not crash without a RouterContext. // Layout reads `router.pathname` only; the rest of the surface (`prefetch`, // `events`, `push`) is for next/link's internals — provide stubs so prefetch // does not throw when mounts. vi.mock('next/router', () => ({ useRouter: () => ({ pathname: '/', asPath: '/', query: {}, push: vi.fn(), replace: vi.fn(), prefetch: vi.fn().mockResolvedValue(undefined), events: { on: vi.fn(), off: vi.fn(), emit: vi.fn() }, }), })); // Mock next/link to a plain . The real next/link triggers prefetch on // mount via the router; bypassing it removes a class of jsdom flake without // changing the rendered DOM that the assertions inspect. vi.mock('next/link', () => ({ __esModule: true, default: ({ href, children, ...rest }) => { return ( {children} ); }, })); // Mock the theme context so useTheme() does not require a ThemeProvider. vi.mock('../../lib/theme-context', () => ({ useTheme: () => ({ theme: 'light', toggleTheme: vi.fn() }), })); import Layout from '../../components/Layout'; describe('Layout — logged-out rendering (regression: P0 #7)', () => { afterEach(() => cleanup()); it('does NOT render the maintainer email when no user prop is passed', () => { const { container } = render(page body); expect(container.textContent).not.toContain('me@randallstillwell.com'); }); it('does NOT render the maintainer email when user is null', () => { const { container } = render(page body); expect(container.textContent).not.toContain('me@randallstillwell.com'); }); it('renders a Sign-in link to /login when user is null', () => { render(page body); const links = screen.getAllByRole('link', { name: /sign in/i }); expect(links.length).toBeGreaterThanOrEqual(1); // Both desktop sidebar + mobile drawer render UserProfileDropdown, // so we expect TWO Sign-in links (one per copy). for (const link of links) { expect(link.getAttribute('href')).toBe('/login'); } }); it('flows the supplied user through to the rendered surface (TopSearchBar chip)', () => { // Pre-refinements (2026-06-04), the UserProfileDropdown in the // sidebar displayed the full email. The redesign-v2 refinements // moved the user-menu to the TopSearchBar's compact chip in the // top-right, which displays the username (or the email's local // part as a fallback) rather than the full address. The // maintainer-email regression-lock from P0 #7 still passes via // the three "does NOT render me@randallstillwell.com" cases // above; this case continues to assert that the user prop FLOWS // through, just against the new render surface. const { container } = render( page body ); expect(container.textContent).toContain('foo'); expect(container.textContent).not.toContain('me@randallstillwell.com'); }); it('does NOT render a "Guest" placeholder when logged out', () => { // Decision A says the logged-out copy is "Sign in", not "Guest". // This test prevents a future revert that ships "Guest" as the default // (which would still hide the maintainer email but skip the CTA). const { container } = render(page body); expect(container.textContent).not.toContain('Guest'); }); }); describe('Layout — floating popovers use .glass-panel-strong (regression: Brief 3 of unify-glass-panel-surfaces)', () => { afterEach(() => cleanup()); // The sidebar UserProfileDropdown and the mobile drawer are floating // glass surfaces. Brief 3 migrated them from inline // `background: var(--glass-surface-*)` + `backdropFilter` to the // canonical `.glass-panel-strong` className. If a future PR reverts // either to the inline pattern, the bespoke-glass-surface CI gate // (Brief 7) will catch the regression — these unit tests catch the // SAME regression at the component-render level, before the gate // would fire. it('mobile drawer panel carries .glass-panel-strong when isMobileMenuOpen', () => { const { container } = render( page body ); // The drawer is rendered (translated off-screen via `-translate-x-full`) // even when closed, so we can assert on it from a default mount. const drawer = container.querySelector( '.glass-panel-strong.md\\:hidden.fixed.inset-y-0.left-0' ); expect(drawer).not.toBeNull(); expect(drawer.className).toContain('glass-panel-strong'); expect(drawer.className).toContain('w-64'); }); it('mobile drawer panel does NOT carry an inline `background: var(--glass-surface-*)` style after Brief 3', () => { // Regression lock against reverting Brief 3 to handrolled inline // glass styles on the mobile drawer specifically. The drawer is // always in the DOM (translated off-screen when closed), so we can // query it from a default mount. const { container } = render( page body ); const drawer = container.querySelector( '.glass-panel-strong.md\\:hidden.fixed.inset-y-0.left-0' ); expect(drawer).not.toBeNull(); const inlineStyle = drawer.getAttribute('style') ?? ''; expect(inlineStyle).not.toMatch(/var\(--glass-surface-/); expect(inlineStyle).not.toMatch(/backdrop-filter/i); expect(inlineStyle).toContain('box-shadow'); expect(inlineStyle).toContain('--elevation-pronounced'); }); });