UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

459 lines (361 loc) 16.8 kB
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') }) }) })