@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
459 lines (361 loc) • 16.8 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { vi } from 'vitest'
expect.extend(toHaveNoViolations)
import './heading'
import { PktHeading, TPktHeadingLevel, TPktHeadingSize } from './heading'
const waitForCustomElements = async () => {
await customElements.whenDefined('pkt-heading')
}
// Helper function to create heading element
const createHeading = async (headingProps = '', content = 'Test Heading') => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-heading ${headingProps}>${content}</pkt-heading>
`
document.body.appendChild(container)
await waitForCustomElements()
return container
}
// Cleanup after each test
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktHeading', () => {
describe('Rendering and basic functionality', () => {
test('renders without errors', async () => {
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
expect(heading).toBeInTheDocument()
expect(heading.shadowRoot).toBeTruthy()
})
test('renders with default properties', async () => {
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.level).toBe(2)
expect(heading.visuallyHidden).toBe(false)
expect(heading.align).toBe(undefined)
})
test('renders content in shadow DOM slot', async () => {
const container = await createHeading('', 'Custom Heading Text')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const slot = heading.shadowRoot?.querySelector('slot')
expect(slot).toBeInTheDocument()
expect(heading.textContent).toContain('Custom Heading Text')
})
})
describe('Properties and attributes', () => {
test('applies default properties correctly', async () => {
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.getAttribute('size')).toBe('large')
expect(heading.getAttribute('level')).toBe('2')
expect(heading.getAttribute('visually-hidden')).toBe(null)
expect(heading.getAttribute('align')).toBe(null)
})
test('sets size property correctly', async () => {
const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']
for (const size of sizes) {
const container = await createHeading(`size="${size}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.size).toBe(size)
expect(heading.getAttribute('size')).toBe(size)
document.body.innerHTML = ''
}
})
test('sets level property correctly', async () => {
const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
for (const level of levels) {
const container = await createHeading(`level="${level}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.level).toBe(level)
expect(heading.getAttribute('level')).toBe(String(level))
document.body.innerHTML = ''
}
})
test('sets visuallyHidden property correctly', async () => {
const container = await createHeading('visuallyHidden="true"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.visuallyHidden).toBe(true)
expect(heading.hasAttribute('visuallyHidden')).toBe(true)
})
test('sets align property correctly', async () => {
const alignments = ['start', 'center', 'end'] as const
for (const align of alignments) {
const container = await createHeading(`align="${align}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.align).toBe(align)
expect(heading.getAttribute('align')).toBe(align)
document.body.innerHTML = ''
}
})
})
describe('CSS classes and styling', () => {
test('applies default CSS classes', async () => {
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-heading')).toBe(true)
expect(heading.classList.contains('pkt-heading--medium')).toBe(false)
expect(heading.classList.contains('pkt-heading--start')).toBe(false)
})
test('applies size-specific CSS classes', async () => {
const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']
for (const size of sizes) {
const container = await createHeading(`size="${size}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains(`pkt-heading--${size}`)).toBe(true)
document.body.innerHTML = ''
}
})
test('applies visually hidden class when enabled', async () => {
const container = await createHeading('visuallyHidden="true"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-sr-only')).toBe(true)
})
test('applies alignment-specific CSS classes', async () => {
const alignments = ['start', 'center', 'end'] as const
for (const align of alignments) {
const container = await createHeading(`align="${align}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains(`pkt-heading--${align}`)).toBe(true)
document.body.innerHTML = ''
}
})
test('removes old classes when properties change', async () => {
const container = await createHeading('size="small" align="center"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--small')).toBe(true)
expect(heading.classList.contains('pkt-heading--center')).toBe(true)
// Change properties
heading.size = 'large'
heading.align = 'end'
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--small')).toBe(false)
expect(heading.classList.contains('pkt-heading--center')).toBe(false)
expect(heading.classList.contains('pkt-heading--large')).toBe(true)
expect(heading.classList.contains('pkt-heading--end')).toBe(true)
})
})
describe('ARIA and accessibility attributes', () => {
test('sets role and aria-level attributes on connection', async () => {
const container = await createHeading('level="3"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.getAttribute('role')).toBe('heading')
expect(heading.getAttribute('aria-level')).toBe('3')
})
test('updates aria-level when level property changes', async () => {
const container = await createHeading('level="2"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.getAttribute('aria-level')).toBe('2')
heading.level = 4
await heading.updateComplete
expect(heading.getAttribute('aria-level')).toBe('4')
})
test('updates aria-level when level attribute changes', async () => {
const container = await createHeading('level="1"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.getAttribute('aria-level')).toBe('1')
heading.setAttribute('level', '5')
await heading.updateComplete
expect(heading.getAttribute('aria-level')).toBe('5')
expect(heading.level).toBe(5)
})
})
describe('Level validation', () => {
test('accepts valid heading levels (1-6)', async () => {
const validLevels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
for (const level of validLevels) {
const container = await createHeading(`level="${level}"`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.level).toBe(level)
expect(heading.getAttribute('aria-level')).toBe(String(level))
document.body.innerHTML = ''
}
})
test('handles invalid levels gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
// Test invalid level via property
heading.level = 0 as TPktHeadingLevel
await heading.updateComplete
expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 0. Must be between 1 and 6.')
heading.level = 7 as TPktHeadingLevel
await heading.updateComplete
expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 7. Must be between 1 and 6.')
consoleSpy.mockRestore()
})
})
describe('Property updates and lifecycle', () => {
test('updates classes when size property changes', async () => {
const container = await createHeading('size="small"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--small')).toBe(true)
heading.size = 'xlarge'
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--small')).toBe(false)
expect(heading.classList.contains('pkt-heading--xlarge')).toBe(true)
})
test('updates classes when visuallyHidden property changes', async () => {
const container = await createHeading()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-sr-only')).toBe(false)
heading.visuallyHidden = true
await heading.updateComplete
expect(heading.classList.contains('pkt-sr-only')).toBe(true)
})
test('updates classes when align property changes', async () => {
const container = await createHeading('align="start"')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--start')).toBe(true)
heading.align = 'center'
await heading.updateComplete
expect(heading.classList.contains('pkt-heading--start')).toBe(false)
expect(heading.classList.contains('pkt-heading--center')).toBe(true)
})
})
describe('Content rendering', () => {
test('renders simple text content', async () => {
const container = await createHeading('', 'Simple Heading')
const heading = container.querySelector('pkt-heading') as PktHeading
expect(heading.textContent).toContain('Simple Heading')
})
test('renders HTML content safely', async () => {
const container = await createHeading('', '<strong>Bold</strong> heading')
const heading = container.querySelector('pkt-heading') as PktHeading
expect(heading.innerHTML).toContain('<strong>Bold</strong>')
expect(heading.innerHTML).toContain('heading')
expect(heading.querySelector('strong')).toBeInTheDocument()
})
test('renders multiple child elements', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-heading>
<span>Part 1</span>
<em>Part 2</em>
</pkt-heading>
`
document.body.appendChild(container)
await waitForCustomElements()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
expect(heading.querySelector('span')).toBeInTheDocument()
expect(heading.querySelector('em')).toBeInTheDocument()
expect(heading.textContent).toContain('Part 1')
expect(heading.textContent).toContain('Part 2')
})
})
describe('Accessibility', () => {
test('basic heading is accessible', async () => {
const container = await createHeading('', 'Accessible Heading')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const results = await axe(heading)
expect(results).toHaveNoViolations()
})
test('heading with different levels is accessible', async () => {
const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
for (const level of levels) {
const container = await createHeading(`level="${level}"`, `Level ${level} Heading`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const results = await axe(heading)
expect(results).toHaveNoViolations()
document.body.innerHTML = ''
}
})
test('visually hidden heading is accessible', async () => {
const container = await createHeading('visually-hidden="true"', 'Hidden Heading')
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const results = await axe(heading)
expect(results).toHaveNoViolations()
})
test('heading with different alignments is accessible', async () => {
const alignments = ['start', 'center', 'end'] as const
for (const align of alignments) {
const container = await createHeading(`align="${align}"`, `${align} aligned heading`)
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const results = await axe(heading)
expect(results).toHaveNoViolations()
document.body.innerHTML = ''
}
})
test('complex heading content is accessible', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-heading level="1" size="large" align="center">
<span>Main</span> <em>Title</em> with <strong>emphasis</strong>
</pkt-heading>
`
document.body.appendChild(container)
await waitForCustomElements()
const heading = container.querySelector('pkt-heading') as PktHeading
await heading.updateComplete
const results = await axe(heading)
expect(results).toHaveNoViolations()
})
})
describe('Integration scenarios', () => {
test('works correctly with multiple headings', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-heading level="1" size="xlarge">Main Title</pkt-heading>
<pkt-heading level="2" size="large">Subtitle</pkt-heading>
<pkt-heading level="3" size="medium">Section</pkt-heading>
`
document.body.appendChild(container)
await waitForCustomElements()
const headings = container.querySelectorAll('pkt-heading') as NodeListOf<PktHeading>
await Promise.all([...headings].map((h) => h.updateComplete))
expect(headings[0].level).toBe(1)
expect(headings[0].size).toBe('xlarge')
expect(headings[1].level).toBe(2)
expect(headings[1].size).toBe('large')
expect(headings[2].level).toBe(3)
expect(headings[2].size).toBe('medium')
})
test('maintains independence between multiple instances', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-heading id="h1" level="1" size="large">Heading 1</pkt-heading>
<pkt-heading id="h2" level="2" size="small">Heading 2</pkt-heading>
`
document.body.appendChild(container)
await waitForCustomElements()
const heading1 = container.querySelector('#h1') as PktHeading
const heading2 = container.querySelector('#h2') as PktHeading
await Promise.all([heading1.updateComplete, heading2.updateComplete])
// Change properties on first heading
heading1.size = 'xlarge'
heading1.align = 'center'
await heading1.updateComplete
// Verify second heading is unaffected
expect(heading2.size).toBe('small')
expect(heading2.align).toBe(undefined)
expect(heading1.size).toBe('xlarge')
expect(heading1.align).toBe('center')
})
})
})