import React from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom/vitest"; import Top from "../../app/components/navigation/Top"; import { renderWithProviders } from "../utils/test-utils"; import { CREATE_FLOW_ANONYMOUS_KEY } from "../../app/(app)/create/utils/anonymousDraftStorage"; import { CORE_VALUE_DETAILS_STORAGE_KEY } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage"; import { componentTestSuite } from "../utils/componentTestSuite"; const { pushMock } = vi.hoisted(() => ({ pushMock: vi.fn() })); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: pushMock, replace: vi.fn(), prefetch: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), }), usePathname: () => "/", })); type TopProps = React.ComponentProps; // Test folderTop=false variant (standard header) componentTestSuite({ component: Top, name: "Top (folderTop=false)", props: { folderTop: false } as TopProps, requiredProps: [], primaryRole: "banner", testCases: { renders: true, accessibility: true, keyboardNavigation: false, disabledState: false, errorState: false, }, }); // Test folderTop=true variant (home header) // Note: Accessibility test may fail due to Next.js Script component behavior in test environment componentTestSuite({ component: Top, name: "Top (folderTop=true)", props: { folderTop: true } as TopProps, requiredProps: [], primaryRole: "banner", testCases: { renders: true, accessibility: false, // Disabled due to Next.js Script component in test environment keyboardNavigation: false, disabledState: false, errorState: false, }, }); describe('Top "Create rule" button', () => { beforeEach(() => { pushMock.mockReset(); window.localStorage.clear(); }); afterEach(() => { window.localStorage.clear(); }); /** * Guards against localStorage stickiness on the marketing homepage: hitting * the top-nav "Create rule" from anywhere outside `/create` must wipe the * in-flight anonymous draft so the wizard always starts fresh. See * handleCreateRuleClick in Top.container.tsx for the contract. */ it("clears anonymous draft + core-value-details localStorage before routing to /create/informational", async () => { window.localStorage.setItem( CREATE_FLOW_ANONYMOUS_KEY, JSON.stringify({ title: "Stale community" }), ); window.localStorage.setItem( CORE_VALUE_DETAILS_STORAGE_KEY, JSON.stringify({ "1": { meaning: "m", signals: "s" } }), ); renderWithProviders(); const btn = screen.getByRole("button", { name: /create a new rule/i, }); await userEvent.click(btn); await waitFor(() => { expect(pushMock).toHaveBeenCalledWith("/create/informational"); }); expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull(); expect( window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY), ).toBeNull(); }); it("pads the standard header to hug Create rule", () => { renderWithProviders(); const nav = screen.getByRole("navigation", { name: "Main navigation" }); expect(nav.className).toContain("py-[var(--spacing-scale-008)]"); expect(nav.className).toContain("lg:py-[var(--spacing-scale-016)]"); expect(nav.className).not.toContain("min-h-[var(--spacing-scale-040)]"); }); it("uses filled invert for Create rule", () => { for (const folderTop of [false, true]) { const { unmount } = renderWithProviders(); const button = screen.getByRole("button", { name: /create a new rule/i, }); expect(button.className).toContain( "bg-[var(--color-surface-default-primary)]", ); expect(button.className).toContain( "text-[var(--color-content-default-primary)]", ); unmount(); } }); }); describe("Top header chrome", () => { it("renders each nav control once, including display:none copies", () => { for (const folderTop of [false, true]) { const { unmount } = renderWithProviders( , ); expect( screen.getAllByRole("menuitem", { name: /navigate to use cases page/i, hidden: true, }), ).toHaveLength(1); expect( screen.getAllByRole("menuitem", { name: /navigate to learn page/i, hidden: true, }), ).toHaveLength(1); expect( screen.getAllByRole("menuitem", { name: /go to your profile/i, hidden: true, }), ).toHaveLength(1); expect( screen.getAllByRole("button", { name: /create a new rule/i, hidden: true, }), ).toHaveLength(1); unmount(); } }); it("shows Profile instead of Log in when signed in", () => { renderWithProviders(); expect( screen.getByRole("menuitem", { name: /go to your profile/i }), ).toBeInTheDocument(); expect( screen.queryByRole("button", { name: /log in to your account/i }), ).not.toBeInTheDocument(); }); it("keeps the standard logo and nav in separate columns", () => { renderWithProviders(); const logo = document.querySelector("[data-top='logo']"); const nav = document.querySelector("[data-top='nav']"); const headerNav = document.querySelector("header nav"); expect(logo?.className).not.toMatch(/\bz-20\b/); expect(nav?.className).not.toMatch(/\babsolute\b/); expect(headerNav?.className).toContain( "md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]", ); }); it("hugs the home folder tab below md and page-centers nav from md", () => { renderWithProviders(); const tab = document.querySelector(".HeaderTab"); const nav = document.querySelector("[data-top='nav']"); expect(tab?.className).toContain("w-fit"); expect(tab?.className).toContain("sm:w-auto"); expect(tab?.className).toContain("sm:flex-1"); expect(nav?.className).toContain("md:absolute"); expect(nav?.className).toContain( "md:left-[calc(50vw-var(--spacing-scale-016))]", ); expect(nav?.className).not.toContain("ml-auto"); }); it("keeps a single home nav cluster and shows the folder wordmark", () => { renderWithProviders(); expect( screen.getAllByRole("menuitem", { name: /navigate to use cases page/i, hidden: true, }), ).toHaveLength(1); const wordmark = screen.getByText("CommunityRule"); expect(wordmark.className).not.toMatch(/\bhidden\b/); expect( screen.getByRole("button", { name: /create a new rule/i }).className, ).toContain("md:text-x-small-label"); }); });