import '@testing-library/jest-dom' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import './header-global' import { PktHeaderGlobal } from './header-global' import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer' beforeEach(() => { Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }) global.ResizeObserver = class ResizeObserver { observe() {} unobserve() {} disconnect() {} } window.scrollTo = vi.fn() }) const waitForReady = async () => { await customElements.whenDefined('pkt-header-global') } const mount = async (props: Record = {}): Promise => { const el = document.createElement('pkt-header-global') as PktHeaderGlobal el.data = EXAMPLE_HEADER_FOOTER_DATA el.position = 'relative' Object.assign(el, props) document.body.appendChild(el) await waitForReady() await el.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await el.updateComplete return el } const clickHost = async (el: PktHeaderGlobal, selector: string) => { const host = el.querySelector(selector) host?.click() await el.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await el.updateComplete } afterEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() vi.unstubAllGlobals() }) // jsdom reports offsetWidth as 0, which puts the header in compact mode. // Force the desktop (full) layout by zeroing the breakpoints. const DESKTOP = { mobileBreakpoint: 0, tabletBreakpoint: 0 } describe('pkt-header-global', () => { test('viser logo, søkefelt, kontaktlenke og menyknapp (desktop)', async () => { const el = await mount(DESKTOP) expect(el.querySelector('.pkt-header-global__logo pkt-icon')).toBeInTheDocument() const searchInput = el.querySelector('input[name="q"]') expect(searchInput).toBeInTheDocument() const contact = el.querySelector('.pkt-header-global__contact') expect(contact).toBeInTheDocument() expect(contact).toHaveAttribute('href', 'https://www.oslo.kommune.no/kontakt') expect(el.querySelector('#pkt-header-global-menu-toggle')).toBeInTheDocument() }) test('skjuler kontaktlenken når bruker er innlogget', async () => { const el = await mount({ ...DESKTOP, user: { name: 'Aksel Web Opdahl' } }) expect(el.querySelector('.pkt-header-global__contact')).not.toBeInTheDocument() expect(el.querySelector('.pkt-header-global__user-button')).toBeInTheDocument() }) test('megameny-overlegget forankres til headerens faktiske posisjon', async () => { const el = await mount({ ...DESKTOP, position: 'relative' }) const header = el.querySelector('.pkt-header-global') as HTMLElement // Simulate a header sitting 32px down the page (relative/sticky position). header.getBoundingClientRect = () => ({ bottom: 120, top: 32, left: 16, right: 1296, width: 1280, height: 88 }) as DOMRect await clickHost(el, '#pkt-header-global-menu-toggle') const overlay = el.querySelector('#pkt-header-global-menu') // The CSS adds the design offset (104/88px) on top of this anchor. expect(overlay?.style.getPropertyValue('--pkt-header-global-top')).toBe('32px') expect(overlay?.style.getPropertyValue('--pkt-header-global-left')).toBe('16px') expect(overlay?.style.getPropertyValue('--pkt-header-global-width')).toBe('1280px') }) test('position="sticky" gir sticky-klasse uten wrapper/spacer', async () => { const el = await mount({ ...DESKTOP, position: 'sticky' }) const header = el.querySelector('.pkt-header-global') expect(header?.classList.contains('pkt-header-global--sticky')).toBe(true) expect(header?.classList.contains('pkt-header-global--fixed')).toBe(false) expect(el.querySelector('.pkt-header-global-wrapper')).toBeNull() expect(el.querySelector('.pkt-header-global-spacer')).toBeNull() }) test('logoen lenker til www.oslo.kommune.no som standard', async () => { const el = await mount(DESKTOP) const logo = el.querySelector('.pkt-header-global__logo a') expect(logo?.getAttribute('href')).toBe('https://www.oslo.kommune.no') }) test('sender logo-click-event når logoen klikkes', async () => { const el = await mount(DESKTOP) // preventDefault - jsdom kan ikke navigere const eventSpy = vi.fn() el.addEventListener('logo-click', (e: Event) => { ;(e as CustomEvent<{ originalEvent: Event }>).detail.originalEvent.preventDefault() eventSpy(e) }) el.querySelector('.pkt-header-global__logo a')?.dispatchEvent( new MouseEvent('click', { bubbles: true, composed: true, cancelable: true }), ) expect(eventSpy).toHaveBeenCalledTimes(1) expect(eventSpy.mock.calls[0][0].detail.originalEvent).toBeInstanceOf(MouseEvent) }) test('søkefeltet er en GET-form mot søke-endepunktet (desktop)', async () => { const el = await mount(DESKTOP) const form = el.querySelector('form[role="search"]') expect(form).toBeInTheDocument() expect(form).toHaveAttribute('action', 'https://www.oslo.kommune.no/sokeresultat/') expect(form).toHaveAttribute('method', 'get') }) test('mobil: søkeknappen åpner et søkefelt-overlay', async () => { // Default breakpoints + jsdom offsetWidth 0 → compact layout. const el = await mount() // Inline search is replaced by a round toggle; no search field yet. expect(el.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument() expect(el.querySelector('#pkt-header-global-search-toggle')).toBeInTheDocument() await clickHost(el, '#pkt-header-global-search-toggle') const panel = el.querySelector('.pkt-header-global__search-panel') expect(panel).toBeInTheDocument() expect(panel?.querySelector('input[name="q"]')).toBeInTheDocument() }) test('mobil: åpning av søk gir fokus til søkefeltet', async () => { const el = await mount() await clickHost(el, '#pkt-header-global-search-toggle') // Focus is moved on the next animation frame. await new Promise((resolve) => requestAnimationFrame(() => resolve(null))) const input = el.querySelector('#pkt-header-global-search-panel input') expect(input).toBeTruthy() expect(document.activeElement).toBe(input) }) test('mobil: Tab ut av søke-overlay lukker det og flytter fokus til søkeknappen', async () => { const el = await mount() await clickHost(el, '#pkt-header-global-search-toggle') const panel = el.querySelector('#pkt-header-global-search-panel') const focusables = panel?.querySelectorAll('input, button') const last = focusables?.[focusables.length - 1] last?.focus() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })) await el.updateComplete expect(el.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument() }) test('mobil: fokus utenfor søke-overlay lukker det', async () => { const el = await mount() await clickHost(el, '#pkt-header-global-search-toggle') expect(el.querySelector('.pkt-header-global__search-panel')).toBeInTheDocument() const menuToggle = el.querySelector('#pkt-header-global-menu-toggle button') menuToggle?.focus() menuToggle?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })) await el.updateComplete expect(el.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument() }) test('mobil: åpning av søk lukker megamenyen', async () => { const el = await mount() await clickHost(el, '#pkt-header-global-menu-toggle') expect(el.querySelector('.pkt-header-global__menu')).toBeInTheDocument() await clickHost(el, '#pkt-header-global-search-toggle') expect(el.querySelector('.pkt-header-global__menu')).not.toBeInTheDocument() expect(el.querySelector('.pkt-header-global__search-panel')).toBeInTheDocument() }) test('menyknappen åpner megamenyen', async () => { const el = await mount() expect(el.querySelector('.pkt-header-global__menu')).not.toBeInTheDocument() const innerToggle = el.querySelector('#pkt-header-global-menu-toggle button') expect(innerToggle).toHaveAttribute('aria-expanded', 'false') await clickHost(el, '#pkt-header-global-menu-toggle') const menu = el.querySelector('.pkt-header-global__menu pkt-header-menu') expect(menu).toBeInTheDocument() expect(menu).toHaveAttribute('open') expect(innerToggle).toHaveAttribute('aria-expanded', 'true') }) test('tastatur: Tab fra menyknappen flytter fokus inn i megamenyen', async () => { const el = await mount(DESKTOP) await clickHost(el, '#pkt-header-global-menu-toggle') const toggleButton = el.querySelector('#pkt-header-global-menu-toggle button') toggleButton?.focus() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })) const menu = el.querySelector('.pkt-header-global__menu') const firstFocusable = menu?.querySelector('a[href], button') expect(firstFocusable).toBeTruthy() expect(document.activeElement).toBe(firstFocusable) }) test('mobil: Tab fra menyknappen flytter fokus inn i megamenyen (accordion)', async () => { // Force the mega menu into its mobile accordion layout. ;(window.matchMedia as unknown) = vi.fn().mockImplementation((query) => ({ matches: true, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })) const el = await mount() await clickHost(el, '#pkt-header-global-menu-toggle') const toggleButton = el.querySelector('#pkt-header-global-menu-toggle button') toggleButton?.focus() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })) const menu = el.querySelector('.pkt-header-global__menu') const firstSummary = menu?.querySelector('summary') expect(firstSummary).toBeTruthy() expect(document.activeElement).toBe(firstSummary) }) test('tastatur: Tab ut av megamenyen lukker den og flytter fokus til menyknappen', async () => { const el = await mount(DESKTOP) await clickHost(el, '#pkt-header-global-menu-toggle') const menu = el.querySelector('.pkt-header-global__menu') const focusables = menu?.querySelectorAll('a[href], button') const last = focusables?.[focusables.length - 1] last?.focus() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })) await el.updateComplete expect(el.querySelector('.pkt-header-global__menu')).not.toBeInTheDocument() const toggleButton = el.querySelector('#pkt-header-global-menu-toggle button') expect(document.activeElement).toBe(toggleButton) }) test('tastatur: fokus utenfor megamenyen lukker den', async () => { const el = await mount(DESKTOP) await clickHost(el, '#pkt-header-global-menu-toggle') expect(el.querySelector('.pkt-header-global__menu')).toBeInTheDocument() const contact = el.querySelector('.pkt-header-global__contact') contact?.focus() contact?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })) await el.updateComplete expect(el.querySelector('.pkt-header-global__menu')).not.toBeInTheDocument() }) test('brukerknappen åpner brukermenyen', async () => { const el = await mount({ user: { name: 'Aksel Web Opdahl' } }) expect(el.querySelector('pkt-header-user-menu')).not.toBeInTheDocument() await clickHost(el, '.pkt-header-global__user-button') expect(el.querySelector('pkt-header-user-menu')).toBeInTheDocument() }) test('viser feilmelding i menyen når data ikke kan lastes', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network'))) const el = document.createElement('pkt-header-global') as PktHeaderGlobal el.position = 'relative' Object.assign(el, DESKTOP) document.body.appendChild(el) await waitForReady() await el.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await el.updateComplete await clickHost(el, '#pkt-header-global-menu-toggle') const error = el.querySelector('.pkt-header-menu__error') expect(error).toBeInTheDocument() expect(error?.textContent).toContain('Kunne ikke laste meny.') }) test('sender data-loaded når data er forhåndshentet', async () => { const onLoaded = vi.fn() const el = document.createElement('pkt-header-global') as PktHeaderGlobal el.addEventListener('data-loaded', onLoaded) el.data = EXAMPLE_HEADER_FOOTER_DATA el.position = 'relative' document.body.appendChild(el) await waitForReady() await el.updateComplete expect(onLoaded).toHaveBeenCalled() }) })