import '@testing-library/jest-dom' import { fireEvent, render, screen } from '@testing-library/react' import { vi } from 'vitest' //mock element width to control mobile/desktop behavior let mockedWidth = 1280 vi.mock('../../hooks/useElementWidth', async () => { return { useElementWidth: () => mockedWidth, } }) import { PktHeaderService } from './HeaderService' describe('PktHeaderService', () => { // jsdom does not implement scrollTo; our scroll lock hook uses it beforeAll(() => { window.scrollTo = vi.fn() }) beforeEach(() => { mockedWidth = 1280 vi.clearAllMocks() }) it('renders service name and link when provided', () => { const { container } = render() const host = container.querySelector('.pkt-header-service__service-link') as HTMLElement | null expect(host).toBeTruthy() expect(screen.getByText('My Service')).toBeInTheDocument() }) it('applies compact class when compact=true', () => { const { container } = render() const header = container.querySelector('.pkt-header-service') expect(header).toHaveClass('pkt-header-service--compact') }) it('shows children inline on desktop, menu button on mobile', () => { // Desktop mockedWidth = 1440 const { rerender } = render( Link , ) expect(screen.getByText('Link')).toBeInTheDocument() // Mobile mockedWidth = 500 rerender( Link , ) expect(screen.getByRole('button', { name: 'Åpne meny' })).toBeInTheDocument() }) it('renders desktop search input when showSearch is true (desktop)', () => { mockedWidth = 1400 render() const search = screen.getByRole('searchbox', { name: 'Søk' }) expect(search).toBeInTheDocument() expect(search).toHaveAttribute('id', 'header-service-search') }) it('renders mobile search toggle and opens search field when clicked (mobile)', () => { mockedWidth = 500 render() const btn = screen.getByRole('button', { name: 'Åpne søkefelt' }) fireEvent.click(btn) // After opening, the mobile menu should show the search input expect(screen.getByRole('searchbox', { name: 'Søk' })).toBeInTheDocument() expect(screen.getByRole('searchbox')).toHaveAttribute('id', 'mobile-search-menu') }) it('renders user button on desktop and opens user menu on click', () => { mockedWidth = 1400 render() // Button shows both fullname and shortname (both 'Aksel' when no shortname provided) const userBtn = screen.getByRole('button', { name: /Aksel/ }) expect(userBtn).toBeInTheDocument() fireEvent.click(userBtn) // User menu should be mounted const menu = screen.getByRole('navigation') expect(menu).toHaveClass('pkt-user-menu') }) // New tests for hideLogo prop describe('hideLogo prop', () => { it('shows oslo logo by default (hideLogo=false)', () => { const { container } = render() const logos = container.querySelectorAll('.pkt-header-service__logo') expect(logos.length).toBe(1) }) it('hides oslo logo when hideLogo=true', () => { const { container } = render() const logos = container.querySelectorAll('.pkt-header-service__logo') expect(logos.length).toBe(0) }) }) // New tests for logoLink prop describe('logoLink prop', () => { it('renders logo as link when logoLink is a string', () => { const { container } = render() const logoLink = container.querySelector('.pkt-header-service__logo a') expect(logoLink).toBeTruthy() expect(logoLink).toHaveAttribute('href', 'https://oslo.kommune.no') }) it('renders logo without link/button when logoLink is not provided', () => { const { container } = render() const logoLink = container.querySelector('.pkt-header-service__logo a') const logoButton = container.querySelector('.pkt-header-service__logo button') expect(logoLink).toBeNull() expect(logoButton).toBeNull() }) }) // New tests for position and scrollBehavior props describe('position and scrollBehavior props', () => { it('applies fixed class by default (position="fixed")', () => { const { container } = render() const header = container.querySelector('.pkt-header-service') expect(header).toHaveClass('pkt-header-service--fixed') }) it('does not apply fixed class when position="relative"', () => { const { container } = render() const header = container.querySelector('.pkt-header-service') expect(header).not.toHaveClass('pkt-header-service--fixed') }) it('applies scroll-to-hide class by default (scrollBehavior="hide")', () => { const { container } = render() const header = container.querySelector('.pkt-header-service') expect(header).toHaveClass('pkt-header-service--scroll-to-hide') }) it('does not apply scroll-to-hide class when scrollBehavior="none"', () => { const { container } = render() const header = container.querySelector('.pkt-header-service') expect(header).not.toHaveClass('pkt-header-service--scroll-to-hide') }) }) // New tests for user with lastLoggedIn describe('user props', () => { it('displays user name in button', () => { mockedWidth = 1400 render() const userBtn = screen.getByRole('button', { name: /Aksel Olsen/ }) expect(userBtn).toBeInTheDocument() }) it('logs deprecation warning when shortname is provided', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockedWidth = 1400 render() expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('shortname')) consoleSpy.mockRestore() }) it('displays formatted lastLoggedIn in user menu', () => { mockedWidth = 1400 render() const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) // Should format the date in Norwegian expect(screen.getByText(/Sist pålogget:/)).toBeInTheDocument() }) it('displays lastLoggedIn string as-is in user menu', () => { mockedWidth = 1400 render() const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) expect(screen.getByText('19. september 2025')).toBeInTheDocument() }) }) // New tests for representing props describe('representing props', () => { it('displays representing name in user button when representing is provided', () => { mockedWidth = 1400 render() const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ }) expect(userBtn).toBeInTheDocument() }) it('logs deprecation warning when representing.shortname is provided', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockedWidth = 1400 render( , ) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('shortname')) consoleSpy.mockRestore() }) it('displays orgNumber in user menu when representing has orgNumber', () => { mockedWidth = 1400 render( , ) const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ }) fireEvent.click(userBtn) expect(screen.getByText('Org.nr. 911738066')).toBeInTheDocument() }) }) // New tests for canChangeRepresentation describe('canChangeRepresentation prop', () => { it('shows change representation button when canChangeRepresentation=true', () => { mockedWidth = 1400 const handleChange = vi.fn() render( , ) const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ }) fireEvent.click(userBtn) const changeBtn = screen.getByRole('button', { name: 'Endre organisasjon' }) expect(changeBtn).toBeInTheDocument() fireEvent.click(changeBtn) expect(handleChange).toHaveBeenCalledTimes(1) }) it('does not show change representation button when canChangeRepresentation=false', () => { mockedWidth = 1400 render( , ) const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ }) fireEvent.click(userBtn) expect(screen.queryByRole('button', { name: 'Endre organisasjon' })).not.toBeInTheDocument() }) }) // New tests for userMenu prop describe('userMenu prop', () => { it('renders userMenu items in user menu dropdown', () => { mockedWidth = 1400 const userMenu = [ { title: 'Mine bookinger', iconName: 'heart', target: '/bookinger' }, { title: 'Innstillinger', iconName: 'cogwheel', target: () => {} }, ] render() const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) expect(screen.getByText('Mine bookinger')).toBeInTheDocument() expect(screen.getByText('Innstillinger')).toBeInTheDocument() }) it('renders userMenu link items as links', () => { mockedWidth = 1400 const userMenu = [{ title: 'Min profil', target: '/profil' }] const { container } = render() const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) // PktLink renders as an tag with class pkt-link const linkElement = container.querySelector('a.pkt-link[href="/profil"]') expect(linkElement).toBeTruthy() expect(screen.getByText('Min profil')).toBeInTheDocument() }) it('calls userMenu button item onClick when clicked', () => { mockedWidth = 1400 const handleClick = vi.fn() const userMenu = [{ title: 'Custom Action', target: handleClick }] render() const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) const actionBtn = screen.getByRole('button', { name: 'Custom Action' }) fireEvent.click(actionBtn) expect(handleClick).toHaveBeenCalledTimes(1) }) }) // New tests for logOut callback and placement describe('logOut callback', () => { it('shows logout button in header when logOutButtonPlacement="header"', () => { mockedWidth = 1400 const handleLogout = vi.fn() render( , ) const logoutBtn = screen.getByRole('button', { name: 'Logg ut' }) expect(logoutBtn).toBeInTheDocument() fireEvent.click(logoutBtn) expect(handleLogout).toHaveBeenCalledTimes(1) }) it('shows logout button in userMenu when logOutButtonPlacement="userMenu"', () => { mockedWidth = 1400 const handleLogout = vi.fn() render( , ) const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) const logoutBtn = screen.getByRole('button', { name: 'Logg ut' }) expect(logoutBtn).toBeInTheDocument() fireEvent.click(logoutBtn) expect(handleLogout).toHaveBeenCalledTimes(1) }) it('shows logout button in both header and userMenu when logOutButtonPlacement="both"', () => { mockedWidth = 1400 const handleLogout = vi.fn() render( , ) // Header logout const headerLogoutBtn = screen.getByRole('button', { name: 'Logg ut' }) expect(headerLogoutBtn).toBeInTheDocument() // Open user menu const userBtn = screen.getByRole('button', { name: /Aksel/ }) fireEvent.click(userBtn) // Should have logout in menu too const logoutBtns = screen.getAllByRole('button', { name: 'Logg ut' }) expect(logoutBtns.length).toBe(2) }) }) // Test for userMenuFooter deprecation warning describe('userMenuFooter deprecation', () => { it('logs deprecation warning when userMenuFooter is provided', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockedWidth = 1400 render( , ) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('userMenuFooter')) consoleSpy.mockRestore() }) }) // Test for userOptions "no longer available" warning describe('userOptions warning', () => { it('logs warning when userOptions is provided', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockedWidth = 1400 render( , ) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('userOptions')) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no longer available')) consoleSpy.mockRestore() }) }) // Tests for serviceLink and serviceClick props describe('serviceLink and serviceClick props', () => { it('renders service name as link when serviceLink is a string', () => { const { container } = render() const link = container.querySelector('a.pkt-link.pkt-header-service__service-link') expect(link).toBeTruthy() expect(link).toHaveAttribute('href', 'https://example.com') }) it('renders service name as button when serviceClick is provided', () => { const handleClick = vi.fn() const { container } = render() const button = container.querySelector('button.pkt-header-service__service-link') expect(button).toBeTruthy() fireEvent.click(button!) expect(handleClick).toHaveBeenCalledTimes(1) }) it('renders service name as span when neither serviceLink nor serviceClick is provided', () => { const { container } = render() const span = container.querySelector('span.pkt-header-service__service-link') expect(span).toBeTruthy() expect(span?.tagName).toBe('SPAN') }) it('prioritizes serviceLink over serviceClick when both are provided', () => { const handleClick = vi.fn() const { container } = render( , ) const link = container.querySelector('a.pkt-link.pkt-header-service__service-link') const button = container.querySelector('button.pkt-header-service__service-link') expect(link).toBeTruthy() expect(button).toBeNull() }) }) // Tests for logoClick prop describe('logoClick prop', () => { it('renders logo as button when logoClick is provided', () => { const handleClick = vi.fn() const { container } = render() const logoButton = container.querySelector('.pkt-header-service__logo button') expect(logoButton).toBeTruthy() fireEvent.click(logoButton!) expect(handleClick).toHaveBeenCalledTimes(1) }) it('prioritizes logoLink over logoClick when both are provided', () => { const handleClick = vi.fn() const { container } = render( , ) const logoLink = container.querySelector('.pkt-header-service__logo a') const logoButton = container.querySelector('.pkt-header-service__logo button') expect(logoLink).toBeTruthy() expect(logoButton).toBeNull() }) }) // Tests for logout button placement on mobile vs desktop describe('logout button placement mobile vs desktop', () => { it('shows logout button in user area on desktop when logOutButtonPlacement="header"', () => { mockedWidth = 1400 const handleLogout = vi.fn() render( , ) // On desktop, logout button should be in user area const logoutBtn = screen.getByRole('button', { name: 'Logg ut' }) expect(logoutBtn).toBeInTheDocument() }) it('shows logout button in content area on mobile when logOutButtonPlacement="header"', () => { mockedWidth = 500 const handleLogout = vi.fn() const { container } = render( , ) const contentArea = container.querySelector('.pkt-header-service__content') const logoutBtn = contentArea?.querySelector('button') expect(logoutBtn).toBeTruthy() expect(screen.getByRole('button', { name: 'Logg ut' })).toBeInTheDocument() }) it('does not show logout button in header areas when logOutButtonPlacement="userMenu"', () => { mockedWidth = 1400 const handleLogout = vi.fn() render( , ) // When logOutButtonPlacement is userMenu, logout button should not be visible outside the menu // Only the user menu button should be visible, not a separate logout button const logoutButtons = screen.queryAllByRole('button', { name: 'Logg ut' }) expect(logoutButtons.length).toBe(0) }) }) })