@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
641 lines (515 loc) • 24 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
// Import the components
import './accordion'
import './accordionitem'
// Import the component classes for type checking
import { PktAccordion } from './accordion'
import { PktAccordionItem } from './accordionitem'
const waitForCustomElements = async () => {
await Promise.all([
customElements.whenDefined('pkt-accordion'),
customElements.whenDefined('pkt-accordion-item'),
])
}
// Helper function to create accordion markup
const createAccordion = async (
accordionProps = '',
accordionItemProps = '',
content = 'Test content',
) => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-accordion ${accordionProps}>
<pkt-accordion-item id="test-item" title="Test Title" ${accordionItemProps}>
${content}
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
return container
}
// Helper function to create multiple accordion items
const createAccordionWithMultipleItems = async (accordionProps = '') => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-accordion ${accordionProps}>
<pkt-accordion-item id="item1" title="Title 1">
Content 1
</pkt-accordion-item>
<pkt-accordion-item id="item2" title="Title 2">
Content 2
</pkt-accordion-item>
<pkt-accordion-item id="item3" title="Title 3">
Content 3
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
return container
}
// Cleanup after each test
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktAccordion', () => {
describe('Rendering and basic functionality', () => {
test('renders without errors', async () => {
const container = await createAccordion()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
expect(accordion).toBeInTheDocument()
await accordion.updateComplete
expect(accordion.shadowRoot).toBeTruthy()
})
test('renders children accordion items', async () => {
const container = await createAccordionWithMultipleItems()
const accordionItems = container.querySelectorAll('pkt-accordion-item')
expect(accordionItems).toHaveLength(3)
// Verify content is rendered
expect(container.textContent).toContain('Title 1')
expect(container.textContent).toContain('Content 1')
expect(container.textContent).toContain('Title 2')
expect(container.textContent).toContain('Content 2')
expect(container.textContent).toContain('Title 3')
expect(container.textContent).toContain('Content 3')
})
test('applies default properties correctly', async () => {
const container = await createAccordion()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
await accordion.updateComplete
expect(accordion.compact).toBe(false)
expect(accordion.skin).toBe('borderless')
expect(accordion.name).toBe('')
expect(accordion.ariaLabelledBy).toBe('')
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
expect(accordionDiv).toHaveClass('pkt-accordion')
expect(accordionDiv).toHaveClass('pkt-accordion--borderless')
expect(accordionDiv).not.toHaveClass('pkt-accordion--compact')
})
})
describe('Properties and attributes', () => {
test('applies compact property correctly', async () => {
const container = await createAccordion('compact')
const accordion = container.querySelector('pkt-accordion') as PktAccordion
await accordion.updateComplete
expect(accordion.compact).toBe(true)
expect(accordion.hasAttribute('compact')).toBe(true)
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
expect(accordionDiv).toHaveClass('pkt-accordion--compact')
})
test('applies different skin properties correctly', async () => {
const skins = ['borderless', 'outlined', 'beige', 'blue']
for (const skin of skins) {
const container = await createAccordion(`skin="${skin}"`)
const accordion = container.querySelector('pkt-accordion') as PktAccordion
await accordion.updateComplete
expect(accordion.skin).toBe(skin)
expect(accordion.getAttribute('skin')).toBe(skin)
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
expect(accordionDiv).toHaveClass(`pkt-accordion--${skin}`)
// Cleanup for next iteration
container.remove()
}
})
test('applies aria-labelledby correctly', async () => {
const container = await createAccordion('aria-labelledby="test-heading"')
const accordion = container.querySelector('pkt-accordion') as PktAccordion
await accordion.updateComplete
expect(accordion.ariaLabelledBy).toBe('test-heading')
expect(accordion.getAttribute('aria-labelledby')).toBe('test-heading')
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
expect(accordionDiv?.getAttribute('aria-labelledby')).toBe('test-heading')
})
test('applies name property and updates accordion items', async () => {
const container = await createAccordionWithMultipleItems('name="test-group"')
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItems = container.querySelectorAll('pkt-accordion-item')
await accordion.updateComplete
expect(accordion.name).toBe('test-group')
expect(accordion.getAttribute('name')).toBe('test-group')
// All accordion items should inherit the name
accordionItems.forEach((item) => {
expect(item.getAttribute('name')).toBe('test-group')
})
})
test('updates accordion item names when name property changes', async () => {
const container = await createAccordionWithMultipleItems()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItems = container.querySelectorAll('pkt-accordion-item')
await accordion.updateComplete
// Initially no name
expect(accordion.name).toBe('')
accordionItems.forEach((item) => {
expect(item.getAttribute('name')).toBe(null)
})
// Update name property
accordion.name = 'updated-group'
await accordion.updateComplete
// All accordion items should now have the updated name
accordionItems.forEach((item) => {
expect(item.getAttribute('name')).toBe('updated-group')
})
})
test('does not override existing name on accordion items', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-accordion name="group-name">
<pkt-accordion-item id="item1" title="Title 1" name="existing-name">
Content 1
</pkt-accordion-item>
<pkt-accordion-item id="item2" title="Title 2">
Content 2
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItems = container.querySelectorAll('pkt-accordion-item')
await accordion.updateComplete
// First item should keep its existing name
expect(accordionItems[0].getAttribute('name')).toBe('existing-name')
// Second item should get the accordion's name
expect(accordionItems[1].getAttribute('name')).toBe('group-name')
})
})
describe('Dynamic content handling', () => {
test('handles dynamically added accordion items', async () => {
const container = await createAccordion('name="dynamic-group"')
const accordion = container.querySelector('pkt-accordion') as PktAccordion
await accordion.updateComplete
// Add a new accordion item
const newItem = document.createElement('pkt-accordion-item') as PktAccordionItem
newItem.setAttribute('id', 'dynamic-item')
newItem.setAttribute('title', 'Dynamic Title')
newItem.textContent = 'Dynamic Content'
accordion.appendChild(newItem)
// Wait for the slot change to propagate
await new Promise((resolve) => setTimeout(resolve, 100))
await accordion.updateComplete
expect(newItem.getAttribute('name')).toBe('dynamic-group')
})
})
})
describe('PktAccordionItem', () => {
describe('Rendering and basic functionality', () => {
test('renders without errors', async () => {
const container = await createAccordion()
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
expect(accordionItem).toBeInTheDocument()
await accordionItem.updateComplete
expect(accordionItem).toBeTruthy()
const details = accordionItem.querySelector('details')
expect(details).toBeInTheDocument()
})
test('renders with correct structure', async () => {
const container = await createAccordion('', '', 'Test accordion content')
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
const details = accordionItem.querySelector('details')
const summary = details?.querySelector('summary')
const content = details?.querySelector('.pkt-accordion-item__content')
const contentInner = content?.querySelector('.pkt-accordion-item__content-inner')
expect(summary).toHaveClass('pkt-accordion-item__title')
expect(summary?.textContent).toContain('Test Title')
expect(content?.getAttribute('role')).toBe('region')
expect(contentInner?.textContent).toContain('Test accordion content')
expect(contentInner).toBeInTheDocument()
})
test('renders icon correctly', async () => {
const container = await createAccordion()
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
const icon = accordionItem.querySelector('pkt-icon')
expect(icon).toBeInTheDocument()
expect(icon?.getAttribute('name')).toBe('chevron-thin-down')
expect(icon).toHaveClass('pkt-accordion-item__icon')
expect(icon?.getAttribute('aria-hidden')).toBe('true')
})
})
describe('Properties and attributes', () => {
test('applies default properties correctly', async () => {
const container = await createAccordion()
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
expect(accordionItem.defaultOpen).toBe(false)
expect(accordionItem.title).toBe('Test Title')
expect(accordionItem.skin).toBe(undefined)
const details = accordionItem.querySelector('details')
expect(details?.hasAttribute('open')).toBe(false)
})
test('applies different skin properties correctly', async () => {
const skins = ['borderless', 'outlined', 'beige', 'blue']
for (const skin of skins) {
const container = await createAccordion('', `skin="${skin}"`)
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
expect(accordionItem.skin).toBe(skin)
expect(accordionItem.getAttribute('skin')).toBe(skin)
const details = accordionItem.querySelector('details')
expect(details).toHaveClass(`pkt-accordion-item--${skin}`)
// Cleanup for next iteration
container.remove()
}
})
test('handles defaultOpen property', async () => {
const container = await createAccordion('', '')
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
// Test that setting defaultOpen to true sets isOpen to true
accordionItem.defaultOpen = true
await accordionItem.updateComplete
// Manually trigger what firstUpdated should do
if (accordionItem.defaultOpen) {
accordionItem.isOpen = true
}
await accordionItem.updateComplete
expect(accordionItem.defaultOpen).toBe(true)
// When defaultOpen is true, isOpen should be set to true
expect(accordionItem.isOpen).toBe(true)
const details = accordionItem.querySelector('details')
expect(details?.hasAttribute('open')).toBe(true)
})
test('handles title property updates', async () => {
const container = await createAccordion()
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
const summary = accordionItem.querySelector('summary')
expect(summary?.textContent).toContain('Test Title')
// Update title
accordionItem.title = 'Updated Title'
await accordionItem.updateComplete
expect(summary?.textContent).toContain('Updated Title')
})
})
describe('Interaction and state management', () => {
test('toggles open state when isOpen property changes', async () => {
const container = await createAccordion()
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordionItem.updateComplete
const details = accordionItem.querySelector('details')
expect(details?.hasAttribute('open')).toBe(false)
// Set isOpen to true
accordionItem.isOpen = true
await accordionItem.updateComplete
expect(details?.hasAttribute('open')).toBe(true)
// Set isOpen to false
accordionItem.isOpen = false
await accordionItem.updateComplete
expect(details?.hasAttribute('open')).toBe(false)
})
test('respects name attribute for grouped behavior', async () => {
const container = await createAccordionWithMultipleItems('name="test-group"')
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
const details = Array.from(accordionItems)
.map((item) => item.querySelector('details'))
.filter(Boolean) as HTMLDetailsElement[]
// Open first item programmatically
accordionItems[0].isOpen = true
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
expect(details[0].hasAttribute('open')).toBe(true)
expect(details[1].hasAttribute('open')).toBe(false)
expect(details[2].hasAttribute('open')).toBe(false)
// Open second item (should close first due to grouping)
accordionItems[1].isOpen = true
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
// Note: If grouping behavior is implemented, first item should close
// For now, let's test basic functionality
expect(details[1].hasAttribute('open')).toBe(true)
})
test('allows multiple items open when no name grouping', async () => {
const container = await createAccordionWithMultipleItems()
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
const details = Array.from(accordionItems)
.map((item) => item.querySelector('details'))
.filter(Boolean) as HTMLDetailsElement[]
// Open first and third items programmatically
accordionItems[0].isOpen = true
accordionItems[2].isOpen = true
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
// Both should remain open since there's no grouping
expect(details[0].hasAttribute('open')).toBe(true)
expect(details[1].hasAttribute('open')).toBe(false)
expect(details[2].hasAttribute('open')).toBe(true)
})
})
})
describe('Integration tests', () => {
test('accordion and accordion items work together correctly', async () => {
const container = await createAccordionWithMultipleItems(
'skin="outlined" compact name="integration-test"',
)
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
await accordion.updateComplete
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
// Verify accordion properties
expect(accordion.skin).toBe('outlined')
expect(accordion.compact).toBe(true)
expect(accordion.name).toBe('integration-test')
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
expect(accordionDiv).toHaveClass('pkt-accordion')
expect(accordionDiv).toHaveClass('pkt-accordion--outlined')
expect(accordionDiv).toHaveClass('pkt-accordion--compact')
// Verify all items have inherited name
accordionItems.forEach((item) => {
expect(item.getAttribute('name')).toBe('integration-test')
})
const details = Array.from(accordionItems)
.map((item) => item.querySelector('details'))
.filter(Boolean) as HTMLDetailsElement[]
// Open items programmatically to test functionality
accordionItems[0].isOpen = true
accordionItems[2].isOpen = true
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
// Test that properties are reflected to attributes
expect(details[0].hasAttribute('open')).toBe(true)
expect(details[2].hasAttribute('open')).toBe(true)
})
test('handles mixed open states correctly', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-accordion>
<pkt-accordion-item id="item1" title="Title 1" default-open>
Content 1
</pkt-accordion-item>
<pkt-accordion-item id="item2" title="Title 2">
Content 2
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
// Set defaultOpen and isOpen programmatically to test the functionality
accordionItems[0].defaultOpen = true
accordionItems[0].isOpen = true
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
const details = Array.from(accordionItems)
.map((item) => item.querySelector('details'))
.filter(Boolean) as HTMLDetailsElement[]
// Item 1 should be open due to defaultOpen
expect(details[0].hasAttribute('open')).toBe(true)
// Item 2 should be closed
expect(details[1].hasAttribute('open')).toBe(false)
})
})
describe('Accessibility', () => {
test('has correct ARIA attributes', async () => {
const container = document.createElement('div')
container.innerHTML = `
<h2 id="accordion-heading">Main Accordion</h2>
<pkt-accordion aria-labelledby="accordion-heading">
<pkt-accordion-item id="test-item" title="Test Title">
Test content
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordion.updateComplete
await accordionItem.updateComplete
const accordionDiv = accordion.shadowRoot?.querySelector('.pkt-accordion')
const content = accordionItem.querySelector('.pkt-accordion-item__content')
const icon = accordionItem.querySelector('pkt-icon')
expect(accordionDiv?.getAttribute('aria-labelledby')).toBe('accordion-heading')
expect(content?.getAttribute('role')).toBe('region')
expect(icon?.getAttribute('aria-hidden')).toBe('true')
})
test('renders with no WCAG errors with axe - simple accordion', async () => {
const container = await createAccordion()
// Wait for all components to be fully rendered
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItem = container.querySelector('pkt-accordion-item') as PktAccordionItem
await accordion.updateComplete
await accordionItem.updateComplete
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('renders with no WCAG errors with axe - complex accordion', async () => {
const container = document.createElement('div')
container.innerHTML = `
<h2 id="accordion-heading">Test Accordion Heading</h2>
<pkt-accordion skin="outlined" compact aria-labelledby="accordion-heading" name="test-group">
<pkt-accordion-item id="item1" title="First Item" default-open>
<p>This is the first accordion item content with <a href="#">a link</a>.</p>
</pkt-accordion-item>
<pkt-accordion-item id="item2" title="Second Item">
<div>
<h3>Nested content</h3>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
</div>
</pkt-accordion-item>
<pkt-accordion-item id="item3" title="Third Item" skin="blue">
<form>
<label for="test-input">Test Input:</label>
<input type="text" id="test-input" name="test" />
<button type="submit">Submit</button>
</form>
</pkt-accordion-item>
</pkt-accordion>
`
document.body.appendChild(container)
await waitForCustomElements()
const accordion = container.querySelector('pkt-accordion') as PktAccordion
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
await accordion.updateComplete
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('renders with no WCAG errors with axe - multiple accordions', async () => {
const container = document.createElement('div')
container.innerHTML = `
<div>
<h2 id="first-accordion-heading">First Accordion</h2>
<pkt-accordion aria-labelledby="first-accordion-heading" name="first-group">
<pkt-accordion-item id="first-item1" title="First Group Item 1">
Content for first group
</pkt-accordion-item>
<pkt-accordion-item id="first-item2" title="First Group Item 2">
More content for first group
</pkt-accordion-item>
</pkt-accordion>
<h2 id="second-accordion-heading">Second Accordion</h2>
<pkt-accordion aria-labelledby="second-accordion-heading" name="second-group" skin="beige">
<pkt-accordion-item id="second-item1" title="Second Group Item 1">
Content for second group
</pkt-accordion-item>
<pkt-accordion-item id="second-item2" title="Second Group Item 2">
More content for second group
</pkt-accordion-item>
</pkt-accordion>
</div>
`
document.body.appendChild(container)
await waitForCustomElements()
const accordions = container.querySelectorAll('pkt-accordion') as NodeListOf<PktAccordion>
const accordionItems = container.querySelectorAll(
'pkt-accordion-item',
) as NodeListOf<PktAccordionItem>
await Promise.all(Array.from(accordions).map((acc) => acc.updateComplete))
await Promise.all(Array.from(accordionItems).map((item) => item.updateComplete))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})