import type { ReactNode } from 'react' import type { Mock } from 'vitest' import { useSuspenseQuery } from '@tanstack/react-query' import { fireEvent, screen } from '@testing-library/react' import { useStore as useAppStore } from '@/app/components/app/store' import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag' import { usePathname } from '@/next/navigation' import { render } from '@/test/console/render' import MainNavLayout from '../layout' const mockConsoleState = vi.hoisted(() => ({ current: { isCurrentWorkspaceDatasetOperator: false, isCurrentWorkspaceEditor: true, }, })) vi.mock('@/app/components/header', () => ({ default: () =>
Header
, })) vi.mock('@/app/components/header/header-wrapper', () => ({ default: ({ children }: { children: ReactNode }) => (
{children}
), })) vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useSuspenseQuery: vi.fn(), } }) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState.current) }) vi.mock('@/features/agent-v2/feature-flag', () => ({ isAgentV2Enabled: vi.fn(), })) vi.mock('@/next/navigation', async (importOriginal) => { const actual = await importOriginal() return { ...actual, usePathname: vi.fn(), } }) vi.mock('../index', () => ({ MainNav: ({ className }: { className?: string }) => ( ), })) describe('MainNavLayout', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() useAppStore.getState().setAppDetail() ;(usePathname as Mock).mockReturnValue('/apps') mockConsoleState.current = { isCurrentWorkspaceDatasetOperator: false, isCurrentWorkspaceEditor: true, } ;(useSuspenseQuery as Mock).mockReturnValue({ data: { enable_app_deploy: true, }, }) ;(isAgentV2Enabled as Mock).mockReturnValue(true) }) it('renders desktop main nav instead of the desktop header', () => { render(
content
, ) expect(screen.getByTestId('main-nav')).toBeInTheDocument() expect(screen.queryByTestId('desktop-header')).not.toBeInTheDocument() expect(screen.getByText('content')).toBeInTheDocument() }) it('uses the main nav without the desktop header wrapper', () => { render(
content
, ) expect(screen.getByTestId('main-nav')).toBeInTheDocument() expect(screen.queryByTestId('header-wrapper')).not.toBeInTheDocument() expect(screen.queryByTestId('desktop-header')).not.toBeInTheDocument() }) it('renders one main landmark as the skip navigation target', () => { render(
content
, ) const main = screen.getByRole('main') expect(screen.getAllByRole('main')).toHaveLength(1) expect(main).toHaveAttribute('id', 'main-content') expect(main).toHaveAttribute('tabIndex', '-1') expect(main).toHaveClass( 'outline-hidden', 'focus:outline-hidden', 'focus-visible:outline-hidden', ) expect(main).toHaveTextContent('content') }) it('renders skip navigation before the repeated main navigation', () => { const { container } = render(
content
, ) const skipLink = screen.getByRole('link', { name: /(?:^|\.)navigation\.skipToMain(?=$|:)/ }) expect(skipLink).toHaveAttribute('href', '#main-content') expect(skipLink).toHaveClass( 'outline-hidden', 'focus-visible:ring-2', 'focus-visible:ring-state-accent-solid', ) expect( container.querySelector( 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])', ), ).toBe(skipLink) }) it('moves focus to the main content when skip navigation is activated', () => { render(
content
, ) const skipLink = screen.getByRole('link', { name: /(?:^|\.)navigation\.skipToMain(?=$|:)/ }) const main = screen.getByRole('main') fireEvent.click(skipLink) expect(main).toHaveFocus() }) it.each(['/datasets/dataset-1/documents', '/datasets/dataset-1/documents/document-1/settings'])( 'renders the detail sidebar slot outside the single skip navigation target on route %s', (pathname) => { ;(usePathname as Mock).mockReturnValue(pathname) render( Detail sidebar}>
dataset detail
, ) const main = screen.getByRole('main') const detailSidebar = screen.getByRole('complementary', { name: 'Detail sidebar' }) expect(screen.queryByTestId('main-nav')).not.toBeInTheDocument() expect(screen.getAllByRole('main')).toHaveLength(1) expect(main).toHaveAttribute('id', 'main-content') expect(main).toHaveTextContent('dataset detail') expect(main).not.toContainElement(detailSidebar) }, ) it.each(['/datasets/create', '/datasets/dataset-1/documents/create', '/deployments/create'])( 'keeps the global main nav on collection and creation route %s', (pathname) => { ;(usePathname as Mock).mockReturnValue(pathname) render( Detail sidebar}>
content
, ) expect(screen.getByTestId('main-nav')).toBeInTheDocument() expect( screen.queryByRole('complementary', { name: 'Detail sidebar' }), ).not.toBeInTheDocument() }, ) it.each([ { label: 'agent detail route for dataset operators', pathname: '/agents/agent-1/configure', consoleState: { isCurrentWorkspaceDatasetOperator: true, isCurrentWorkspaceEditor: true, }, systemFeatures: { enable_app_deploy: true, }, }, { label: 'deployment detail route for non-editor workspaces', pathname: '/deployments/app-instance-1/overview', consoleState: { isCurrentWorkspaceDatasetOperator: false, isCurrentWorkspaceEditor: false, }, systemFeatures: { enable_app_deploy: true, }, }, { label: 'deployment detail route when deployment is disabled', pathname: '/deployments/app-instance-1/overview', consoleState: { isCurrentWorkspaceDatasetOperator: false, isCurrentWorkspaceEditor: true, }, systemFeatures: { enable_app_deploy: false, }, }, ])('keeps the global main nav on $label', ({ pathname, consoleState, systemFeatures }) => { ;(usePathname as Mock).mockReturnValue(pathname) mockConsoleState.current = consoleState ;(useSuspenseQuery as Mock).mockReturnValue({ data: systemFeatures, }) render( Detail sidebar}>
detail route content
, ) expect(screen.getByTestId('main-nav')).toBeInTheDocument() expect(screen.queryByRole('complementary', { name: 'Detail sidebar' })).not.toBeInTheDocument() expect(screen.getAllByRole('main')).toHaveLength(1) expect(screen.getByRole('main')).toHaveTextContent('detail route content') }) it('clears app detail state after leaving app routes', () => { useAppStore .getState() .setAppDetail({ id: 'app-1' } as ReturnType['appDetail']) ;(usePathname as Mock).mockReturnValue('/datasets') render(
content
, ) expect(useAppStore.getState().appDetail).toBeUndefined() }) })