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')
})
})
})