@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
405 lines (347 loc) • 15.2 kB
text/typescript
import '@testing-library/jest-dom'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import './header-service'
// Nested components are imported by header-service itself, but we need to wait for them
const waitForNestedElements = async () => {
await Promise.all([
customElements.whenDefined('pkt-button'),
customElements.whenDefined('pkt-icon'),
customElements.whenDefined('pkt-link'),
customElements.whenDefined('pkt-textinput'),
customElements.whenDefined('pkt-header-user-menu'),
])
}
export interface HeaderServiceTestConfig extends BaseTestConfig {
'service-name'?: string
'service-link'?: string
'logo-link'?: string
compact?: boolean
'hide-logo'?: boolean
position?: 'fixed' | 'relative'
'scroll-behavior'?: 'hide' | 'none'
'show-search'?: boolean
'search-placeholder'?: string
'search-value'?: string
'log-out-button-placement'?: 'userMenu' | 'header' | 'both' | 'none'
'can-change-representation'?: boolean
'opened-menu'?: 'none' | 'slot' | 'search' | 'user'
'mobile-breakpoint'?: number
'tablet-breakpoint'?: number
}
const createHeaderServiceTest = async (config: HeaderServiceTestConfig = {}) => {
const result = await createElementTest<
CustomElementFor<'pkt-header-service'>,
HeaderServiceTestConfig
>('pkt-header-service', config)
await waitForNestedElements()
await result.element.updateComplete
return result
}
describe('pkt-header-service', () => {
beforeEach(() => {
// Mock window.matchMedia for responsive behavior
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(),
})),
})
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
// Mock scrollTo
window.scrollTo = vi.fn()
})
afterEach(() => {
document.body.innerHTML = ''
})
describe('basic rendering', () => {
it('renders with service name', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'My Service' })
const serviceName = element.querySelector('.pkt-header-service__service-name')
expect(serviceName).toBeTruthy()
expect(serviceName?.textContent).toContain('My Service')
})
it('applies compact class when compact attribute is set', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc', compact: true })
const header = element.querySelector('.pkt-header-service')
expect(header?.classList.contains('pkt-header-service--compact')).toBe(true)
})
it('shows logo by default', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const logo = element.querySelector('.pkt-header-service__logo')
expect(logo).toBeTruthy()
})
it('hides logo when hide-logo attribute is set', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'hide-logo': true,
})
const logo = element.querySelector('.pkt-header-service__logo')
expect(logo).toBeNull()
})
})
describe('logoLink attribute', () => {
it('renders logo as link when logo-link is provided', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'logo-link': 'https://oslo.kommune.no',
})
const logoLink = element.querySelector('.pkt-header-service__logo a')
expect(logoLink).toBeTruthy()
expect(logoLink?.getAttribute('href')).toBe('https://oslo.kommune.no')
})
it('dispatches logo-click event when logo button is clicked', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const eventSpy = vi.fn()
element.addEventListener('logo-click', eventSpy)
const logoButton = element.querySelector('.pkt-header-service__logo button')
if (logoButton) {
logoButton.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }))
expect(eventSpy).toHaveBeenCalled()
}
})
it('renders logo without link when logo-link is not provided', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const logoLink = element.querySelector('.pkt-header-service__logo a')
expect(logoLink).toBeNull()
})
})
describe('serviceLink and service-click event', () => {
it('renders service name as link when service-link is provided', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'My Service',
'service-link': 'https://example.com',
})
// Check that serviceLink property is set correctly
expect(element.serviceLink).toBe('https://example.com')
// Check that pkt-link element exists (may not fully render in test env)
const linkElement = element.querySelector('pkt-link')
expect(linkElement).toBeTruthy()
})
it('dispatches service-click event when service button is clicked', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'My Service' })
const eventSpy = vi.fn()
element.addEventListener('service-click', eventSpy)
const serviceButton = element.querySelector('button.pkt-header-service__service-link')
if (serviceButton) {
serviceButton.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }))
expect(eventSpy).toHaveBeenCalled()
}
})
it('renders service name as span when service-link is not provided', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'My Service' })
const span = element.querySelector('span.pkt-header-service__service-link')
expect(span).toBeTruthy()
})
})
describe('position and scrollBehavior attributes', () => {
it('applies fixed class by default (position="fixed")', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const header = element.querySelector('.pkt-header-service')
expect(header?.classList.contains('pkt-header-service--fixed')).toBe(true)
})
it('does not apply fixed class when position="relative"', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
position: 'relative',
})
const header = element.querySelector('.pkt-header-service')
expect(header?.classList.contains('pkt-header-service--fixed')).toBe(false)
})
it('applies scroll-to-hide class by default (scroll-behavior="hide")', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const header = element.querySelector('.pkt-header-service')
expect(header?.classList.contains('pkt-header-service--scroll-to-hide')).toBe(true)
})
it('does not apply scroll-to-hide class when scroll-behavior="none"', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'scroll-behavior': 'none',
})
const header = element.querySelector('.pkt-header-service')
expect(header?.classList.contains('pkt-header-service--scroll-to-hide')).toBe(false)
})
})
describe('search functionality', () => {
it('does not render search input by default', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
const search = element.querySelector('.pkt-header-service__search-input')
expect(search).toBeNull()
})
it('renders search container when show-search is true', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'show-search': true,
})
const searchContainer = element.querySelector('.pkt-header-service__search-container')
expect(searchContainer).toBeTruthy()
})
it('dispatches search event when Enter is pressed in search input', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'show-search': true,
})
const eventSpy = vi.fn()
element.addEventListener('search', eventSpy)
const searchInput = element.querySelector(
'.pkt-header-service__search-input input',
) as HTMLInputElement
if (searchInput) {
searchInput.value = 'test query'
searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
await element.updateComplete
expect(eventSpy).toHaveBeenCalled()
}
})
it('dispatches search-change event when search input value changes', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'show-search': true,
})
const eventSpy = vi.fn()
element.addEventListener('search-change', eventSpy)
const searchInput = element.querySelector(
'.pkt-header-service__search-input input',
) as HTMLInputElement
if (searchInput) {
searchInput.value = 'test'
searchInput.dispatchEvent(new Event('input', { bubbles: true }))
await element.updateComplete
expect(eventSpy).toHaveBeenCalled()
}
})
})
describe('user menu', () => {
it('renders user container when user is provided', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
element.user = { name: 'Aksel Olsen' }
await element.updateComplete
// Check that user container exists
const userContainer = element.querySelector('.pkt-header-service__user-container')
expect(userContainer).toBeTruthy()
// Check that user property is passed correctly
expect(element.user.name).toBe('Aksel Olsen')
})
it('passes user data to user menu component', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
element.user = { name: 'Aksel Olsen' }
await element.updateComplete
// Verify user property is set
expect(element.user).toEqual({ name: 'Aksel Olsen' })
})
it('passes representing data to user menu component', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
element.user = { name: 'Aksel' }
element.representing = { name: 'Oslo Kommune' }
await element.updateComplete
// Verify representing property is set
expect(element.representing).toEqual({ name: 'Oslo Kommune' })
})
it('passes canChangeRepresentation prop when set', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'can-change-representation': true,
})
element.user = { name: 'Aksel' }
element.representing = { name: 'Oslo Kommune' }
await element.updateComplete
// Verify canChangeRepresentation property is set
expect(element.canChangeRepresentation).toBe(true)
})
})
describe('logout button placement', () => {
it('shows logout button in user area when log-out-button-placement="header"', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'log-out-button-placement': 'header',
})
element.user = { name: 'Aksel' }
await element.updateComplete
const userArea = element.querySelector('.pkt-header-service__user')
const logoutBtn = userArea?.querySelector('pkt-button')
expect(logoutBtn).toBeTruthy()
})
it('sets up logout button with correct properties', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'log-out-button-placement': 'header',
})
element.user = { name: 'Aksel' }
await element.updateComplete
// Verify logOutButtonPlacement property is set
expect(element.logOutButtonPlacement).toBe('header')
// Verify logout button component exists in DOM
const logoutBtn = element.querySelector('.pkt-header-service__user pkt-button')
expect(logoutBtn).toBeTruthy()
})
it('does not show logout button in header areas when log-out-button-placement="userMenu"', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'log-out-button-placement': 'userMenu',
})
element.user = { name: 'Aksel' }
await element.updateComplete
// Logout should not be in header areas
const userArea = element.querySelector('.pkt-header-service__user')
const contentArea = element.querySelector('.pkt-header-service__content')
const userAreaLogout = userArea?.querySelectorAll('pkt-button[icon-name="exit"]')
const contentAreaLogout = contentArea?.querySelectorAll('pkt-button[icon-name="exit"]')
expect(userAreaLogout?.length || 0).toBe(0)
expect(contentAreaLogout?.length || 0).toBe(0)
})
})
describe('user menu items', () => {
it('passes user menu items to user menu component', async () => {
const { element } = await createHeaderServiceTest({ 'service-name': 'Svc' })
element.user = { name: 'Aksel' }
element.userMenu = [
{ title: 'Mine bookinger', iconName: 'heart', target: '/bookinger' },
{ title: 'Innstillinger', iconName: 'cogwheel', target: () => {} },
]
await element.updateComplete
// Verify userMenu property is set correctly
expect(element.userMenu).toHaveLength(2)
expect(element.userMenu[0].title).toBe('Mine bookinger')
expect(element.userMenu[1].title).toBe('Innstillinger')
})
})
describe('events', () => {
it('dispatches change-representation event when change button is clicked', async () => {
const { element } = await createHeaderServiceTest({
'service-name': 'Svc',
'can-change-representation': true,
})
element.user = { name: 'Aksel' }
element.representing = { name: 'Oslo Kommune' }
await element.updateComplete
const eventSpy = vi.fn()
element.addEventListener('change-representation', eventSpy)
// Open user menu and click change button
const userButton = element.querySelector('.pkt-user-menu__button') as HTMLElement
userButton?.click()
await element.updateComplete
const allButtons = element.querySelectorAll('button')
const changeButton = Array.from(allButtons || []).find((btn) =>
btn.textContent?.includes('Endre organisasjon'),
)
if (changeButton) {
changeButton.click()
await element.updateComplete
expect(eventSpy).toHaveBeenCalled()
}
})
})
})