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 = ` ${content} ` document.body.appendChild(container) await waitForCustomElements() return container } const innerHeading = (host: PktHeading): HTMLHeadingElement | null => host.querySelector('h1, h2, h3, h4, h5, h6') // 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).toBeNull() }) 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 inside a semantic heading element', async () => { const container = await createHeading('', 'Custom Heading Text') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete const inner = innerHeading(heading) expect(inner).toBeInTheDocument() expect(inner?.tagName).toBe('H2') expect(inner?.textContent).toContain('Custom Heading Text') }) test('uses the matching h-tag for the given level', 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(innerHeading(heading)?.tagName).toBe(`H${level}`) document.body.innerHTML = '' } }) }) describe('Properties and attributes', () => { test('applies default reflected attributes 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 pkt-heading class to the inner heading element, not the host', async () => { const container = await createHeading() const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete expect(heading.classList.contains('pkt-heading')).toBe(false) expect(innerHeading(heading)?.classList.contains('pkt-heading')).toBe(true) }) test('applies size-specific class to the inner heading', 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(innerHeading(heading)?.classList.contains(`pkt-heading--${size}`)).toBe(true) document.body.innerHTML = '' } }) test('applies visually hidden class to the inner heading when enabled', async () => { const container = await createHeading('visuallyHidden="true"') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete expect(innerHeading(heading)?.classList.contains('pkt-sr-only')).toBe(true) }) test('applies alignment-specific class to the inner heading', 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(innerHeading(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(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(true) expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(true) heading.size = 'large' heading.align = 'end' await heading.updateComplete expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(false) expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(false) expect(innerHeading(heading)?.classList.contains('pkt-heading--large')).toBe(true) expect(innerHeading(heading)?.classList.contains('pkt-heading--end')).toBe(true) }) }) describe('Dynamic level changes', () => { test('swaps the inner heading tag when the level changes via property', async () => { const container = await createHeading('level="2"', 'Dynamic') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete expect(innerHeading(heading)?.tagName).toBe('H2') expect(innerHeading(heading)?.textContent).toContain('Dynamic') heading.level = 4 await heading.updateComplete expect(innerHeading(heading)?.tagName).toBe('H4') expect(innerHeading(heading)?.textContent).toContain('Dynamic') }) test('swaps the inner heading tag when the level attribute changes', async () => { const container = await createHeading('level="1"') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete expect(innerHeading(heading)?.tagName).toBe('H1') heading.setAttribute('level', '5') await heading.updateComplete expect(innerHeading(heading)?.tagName).toBe('H5') 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(innerHeading(heading)?.tagName).toBe(`H${level}`) document.body.innerHTML = '' } }) test('warns and falls back to h2 for invalid levels', async () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const container = await createHeading() const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete heading.level = 0 as TPktHeadingLevel await heading.updateComplete expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 0. Must be between 1 and 6.') expect(innerHeading(heading)?.tagName).toBe('H2') heading.level = 7 as TPktHeadingLevel await heading.updateComplete expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 7. Must be between 1 and 6.') expect(innerHeading(heading)?.tagName).toBe('H2') 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(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(true) heading.size = 'xlarge' await heading.updateComplete expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(false) expect(innerHeading(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(innerHeading(heading)?.classList.contains('pkt-sr-only')).toBe(false) heading.visuallyHidden = true await heading.updateComplete expect(innerHeading(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(innerHeading(heading)?.classList.contains('pkt-heading--start')).toBe(true) heading.align = 'center' await heading.updateComplete expect(innerHeading(heading)?.classList.contains('pkt-heading--start')).toBe(false) expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(true) }) }) describe('Content rendering', () => { test('renders simple text content inside the h-tag', async () => { const container = await createHeading('', 'Simple Heading') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete expect(innerHeading(heading)?.textContent).toContain('Simple Heading') }) test('renders nested HTML content inside the h-tag', async () => { const container = await createHeading('', 'Bold heading') const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete const inner = innerHeading(heading) expect(inner?.querySelector('strong')).toBeInTheDocument() expect(inner?.textContent).toContain('Bold') expect(inner?.textContent).toContain('heading') }) test('renders multiple child elements inside the h-tag', async () => { const container = document.createElement('div') container.innerHTML = ` Part 1 Part 2 ` document.body.appendChild(container) await waitForCustomElements() const heading = container.querySelector('pkt-heading') as PktHeading await heading.updateComplete const inner = innerHeading(heading) expect(inner?.querySelector('span')).toBeInTheDocument() expect(inner?.querySelector('em')).toBeInTheDocument() expect(inner?.textContent).toContain('Part 1') expect(inner?.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 = ` Main Title with emphasis ` 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 = ` Main Title Subtitle Section ` document.body.appendChild(container) await waitForCustomElements() const headings = container.querySelectorAll('pkt-heading') as NodeListOf await Promise.all([...headings].map((h) => h.updateComplete)) expect(headings[0].level).toBe(1) expect(innerHeading(headings[0])?.tagName).toBe('H1') expect(headings[1].level).toBe(2) expect(innerHeading(headings[1])?.tagName).toBe('H2') expect(headings[2].level).toBe(3) expect(innerHeading(headings[2])?.tagName).toBe('H3') }) test('maintains independence between multiple instances', async () => { const container = document.createElement('div') container.innerHTML = ` Heading 1 Heading 2 ` 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]) heading1.size = 'xlarge' heading1.align = 'center' await heading1.updateComplete expect(heading2.size).toBe('small') expect(heading2.align).toBe(undefined) expect(heading1.size).toBe('xlarge') expect(heading1.align).toBe('center') }) }) })