@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
209 lines (167 loc) • 8.25 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { fireEvent } from '@testing-library/dom'
import { vi } from 'vitest'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import type { CustomElementFor } from '../../tests/component-registry'
import './breadcrumbs'
expect.extend(toHaveNoViolations)
interface BreadcrumbsTestConfig extends BaseTestConfig {
breadcrumbs?: { text: string; href: string }[]
}
const defaultBreadcrumbs = [
{ text: 'Hjem', href: '/' },
{ text: 'Produkter', href: '/produkter' },
{ text: 'Detaljer', href: '/produkter/detaljer' },
]
const createBreadcrumbsTest = async (config: BreadcrumbsTestConfig = {}) => {
const { container, element } = await createElementTest<
CustomElementFor<'pkt-breadcrumbs'>,
BreadcrumbsTestConfig
>('pkt-breadcrumbs', config)
if (config.breadcrumbs) {
element.breadcrumbs = config.breadcrumbs
await element.updateComplete
}
return { container, breadcrumbs: element }
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktBreadcrumbs', () => {
describe('Rendering', () => {
test('renders without errors', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
expect(breadcrumbs).toBeInTheDocument()
})
test('renders all breadcrumb items', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const items = breadcrumbs.querySelectorAll('.pkt-breadcrumbs__item')
expect(items).toHaveLength(3)
})
test('renders nothing when breadcrumbs array is empty', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: [] })
const nav = breadcrumbs.querySelector('nav')
expect(nav).not.toBeInTheDocument()
})
test('renders correctly when breadcrumbs are set as JSON attribute', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
container.innerHTML = `<pkt-breadcrumbs breadcrumbs='${JSON.stringify(defaultBreadcrumbs)}'></pkt-breadcrumbs>`
const element = container.querySelector('pkt-breadcrumbs') as CustomElementFor<'pkt-breadcrumbs'>
await customElements.whenDefined('pkt-breadcrumbs')
await element.updateComplete
const items = element.querySelectorAll('.pkt-breadcrumbs__item')
expect(items).toHaveLength(3)
const links = element.querySelectorAll('.pkt-breadcrumbs__link')
expect(links).toHaveLength(2)
expect(links[0]).toHaveAttribute('href', '/')
})
test('last item has aria-current and no link', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const items = breadcrumbs.querySelectorAll('.pkt-breadcrumbs__item')
const lastItem = items[items.length - 1]
const span = lastItem.querySelector('[aria-current="true"]')
expect(span).toBeInTheDocument()
const link = lastItem.querySelector('a')
expect(link).not.toBeInTheDocument()
})
test('non-last items render as links with correct href', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const links = breadcrumbs.querySelectorAll('.pkt-breadcrumbs__link')
expect(links).toHaveLength(2)
expect(links[0]).toHaveAttribute('href', '/')
expect(links[1]).toHaveAttribute('href', '/produkter')
})
test('renders breadcrumb text correctly', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const texts = breadcrumbs.querySelectorAll('.pkt-breadcrumbs__text')
// 3 desktop + 1 mobile back-link
expect(texts).toHaveLength(4)
expect(texts[0]).toHaveTextContent('Hjem')
expect(texts[1]).toHaveTextContent('Produkter')
expect(texts[2]).toHaveTextContent('Detaljer')
})
test('renders desktop list with correct classes', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const list = breadcrumbs.querySelector('.pkt-breadcrumbs--desktop')
expect(list).toBeInTheDocument()
})
test('renders mobile back-link pointing to second-to-last item', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const mobileLink = breadcrumbs.querySelector('.pkt-breadcrumbs--mobile')
expect(mobileLink).toBeInTheDocument()
expect(mobileLink).toHaveAttribute('href', '/produkter')
expect(mobileLink).toHaveTextContent('Produkter')
})
test('renders chevron-right icons on desktop links', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const desktopIcons = breadcrumbs.querySelectorAll(
'.pkt-breadcrumbs--desktop pkt-icon[name="chevron-thin-right"]',
)
expect(desktopIcons).toHaveLength(2)
})
test('renders chevron-left icon on mobile back-link', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const mobileIcon = breadcrumbs.querySelector(
'.pkt-breadcrumbs--mobile pkt-icon[name="chevron-thin-left"]',
)
expect(mobileIcon).toBeInTheDocument()
})
})
describe('Events', () => {
test('dispatches navigate event on link click', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const navigateSpy = vi.fn()
breadcrumbs.addEventListener('navigate', navigateSpy)
const link = breadcrumbs.querySelector('.pkt-breadcrumbs__link') as HTMLAnchorElement
fireEvent.click(link)
expect(navigateSpy).toHaveBeenCalledTimes(1)
expect(navigateSpy.mock.calls[0][0].detail.item).toEqual({ text: 'Hjem', href: '/' })
expect(navigateSpy.mock.calls[0][0].detail.index).toBe(0)
})
test('dispatches navigate event on mobile back-link click', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const navigateSpy = vi.fn()
breadcrumbs.addEventListener('navigate', navigateSpy)
const mobileLink = breadcrumbs.querySelector('.pkt-breadcrumbs--mobile') as HTMLAnchorElement
fireEvent.click(mobileLink)
expect(navigateSpy).toHaveBeenCalledTimes(1)
expect(navigateSpy.mock.calls[0][0].detail.item).toEqual({
text: 'Produkter',
href: '/produkter',
})
expect(navigateSpy.mock.calls[0][0].detail.index).toBe(1)
})
test('preventDefault on navigate event prevents default link behavior', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
breadcrumbs.addEventListener('navigate', (e: Event) => {
e.preventDefault()
})
const link = breadcrumbs.querySelector('.pkt-breadcrumbs__link') as HTMLAnchorElement
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
link.dispatchEvent(clickEvent)
expect(preventDefaultSpy).toHaveBeenCalled()
})
})
describe('Accessibility', () => {
test('has nav with aria-label', async () => {
const { breadcrumbs } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const nav = breadcrumbs.querySelector('nav')
expect(nav).toHaveAttribute('aria-label', 'brødsmulemeny')
})
test('has no WCAG violations', async () => {
// Suppress jsdom "Not implemented: navigation" error triggered by axe-core
const originalError = console.error
console.error = (...args: unknown[]) => {
if (typeof args[0] === 'string' && args[0].includes('Not implemented: navigation')) return
originalError(...args)
}
const { container } = await createBreadcrumbsTest({ breadcrumbs: defaultBreadcrumbs })
const results = await axe(container)
expect(results).toHaveNoViolations()
console.error = originalError
})
})
})