UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

641 lines (515 loc) 24 kB
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() }) })