UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

552 lines (426 loc) 18.1 kB
import '@testing-library/jest-dom' import { axe, toHaveNoViolations } from 'jest-axe' import { fireEvent } from '@testing-library/dom' import { vi } from 'vitest' import { createElementTest, BaseTestConfig } from '../../tests/test-framework' import { CustomElementFor } from '../../tests/component-registry' import './button' expect.extend(toHaveNoViolations) export interface ButtonTestConfig extends BaseTestConfig { size?: string skin?: string variant?: string color?: string type?: string disabled?: boolean isLoading?: boolean iconName?: string iconNameSecond?: string iconPath?: string secondIconPath?: string iconPosition?: string mode?: string form?: string content?: string } // Use shared framework export const createButtonTest = async (config: ButtonTestConfig = {}) => { const { container, element } = await createElementTest< CustomElementFor<'pkt-button'>, ButtonTestConfig >('pkt-button', config) return { container, button: element, } } // Cleanup after each test afterEach(() => { document.body.innerHTML = '' }) describe('PktButton', () => { describe('Rendering and basic functionality', () => { test('renders without errors', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) expect(button).toBeInTheDocument() expect(button).toBeTruthy() const nativeButton = button.querySelector('button') expect(nativeButton).toBeInTheDocument() }) test('renders with correct structure', async () => { const { button } = await createButtonTest({ variant: 'icon-left', iconName: 'user', content: 'Click Me', }) const nativeButton = button.querySelector('button') const icon = nativeButton?.querySelector('pkt-icon') const textSpan = nativeButton?.querySelector('.pkt-btn__text') expect(nativeButton).toHaveClass('pkt-btn') expect(icon).toHaveClass('pkt-btn__icon') expect(textSpan).toHaveClass('pkt-btn__text') expect(textSpan?.textContent?.trim()).toContain('Click Me') }) test('renders text correctly', async () => { const { button } = await createButtonTest({ content: 'Button Text Content' }) const textSpan = button.querySelector('.pkt-btn__text') expect(textSpan).toBeInTheDocument() expect(textSpan?.textContent?.trim()).toContain('Button Text Content') }) }) describe('Properties and attributes', () => { test('applies default properties correctly', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) await button.updateComplete expect(button.size).toBe('medium') expect(button.skin).toBe('primary') expect(button.variant).toBe('label-only') expect(button.type).toBe('button') expect(button.mode).toBe('light') expect(button.disabled).toBe(false) expect(button.isLoading).toBe(false) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass('pkt-btn') expect(buttonEl).toHaveClass('pkt-btn--medium') expect(buttonEl).toHaveClass('pkt-btn--primary') expect(buttonEl).toHaveClass('pkt-btn--label-only') }) test('applies different size properties correctly', async () => { const sizes = ['small', 'medium', 'large'] as const for (const size of sizes) { const { button } = await createButtonTest({ size, content: 'Test Button' }) await button.updateComplete expect(button.size).toBe(size) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass(`pkt-btn--${size}`) } }) test('applies different skin properties correctly', async () => { const skins = ['primary', 'secondary', 'tertiary'] as const for (const skin of skins) { const { button } = await createButtonTest({ skin, content: 'Test Button' }) await button.updateComplete expect(button.skin).toBe(skin) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass(`pkt-btn--${skin}`) } }) test('applies different variant properties correctly', async () => { const variants = [ 'label-only', 'icon-left', 'icon-right', 'icon-only', 'icons-right-and-left', ] as const for (const variant of variants) { const { button } = await createButtonTest({ variant, iconName: 'user', iconNameSecond: 'star', content: 'Test Button', }) await button.updateComplete expect(button.variant).toBe(variant) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass(`pkt-btn--${variant}`) // Check icon rendering based on variant const icons = buttonEl?.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)') if (variant === 'label-only') { expect(icons).toHaveLength(0) } else if (variant === 'icons-right-and-left') { expect(icons).toHaveLength(2) } else { expect(icons).toHaveLength(1) } } }) test('applies different color properties correctly', async () => { const colors = ['blue', 'green', 'red', 'yellow'] as const for (const color of colors) { const { button } = await createButtonTest({ color, content: 'Test Button' }) await button.updateComplete expect(button.color).toBe(color) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass(`pkt-btn--${color}`) } }) test('handles type property correctly', async () => { const types = ['button', 'submit', 'reset'] as const for (const type of types) { const { button } = await createButtonTest({ type, content: 'Test Button' }) await button.updateComplete expect(button.type).toBe(type) const buttonEl = button.querySelector('button') expect(buttonEl?.getAttribute('type')).toBe(type) } }) test('handles icon properties correctly', async () => { const { button } = await createButtonTest({ variant: 'icon-left', iconName: 'user', content: 'Test Button', }) await button.updateComplete expect(button.iconName).toBe('user') const icon = button.querySelector('pkt-icon:not(.pkt-btn__spinner)') expect(icon?.getAttribute('name')).toBe('user') expect(icon).toHaveClass('pkt-btn__icon') }) test('handles second icon for icons-right-and-left variant', async () => { const { button } = await createButtonTest({ variant: 'icons-right-and-left', content: 'Test Button', }) // Set both icon names as properties button.iconName = 'home' button.secondIconName = 'star' await button.updateComplete expect(button.iconName).toBe('home') expect(button.secondIconName).toBe('star') const icons = button.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)') expect(icons).toHaveLength(2) expect(icons[0]?.getAttribute('name')).toBe('home') expect(icons[1]?.getAttribute('name')).toBe('star') }) test('handles iconPath property correctly', async () => { const customPath = 'https://custom-cdn.example.com/icons/' const { button } = await createButtonTest({ variant: 'icon-left', iconName: 'user', iconPath: customPath, content: 'Test Button', }) await button.updateComplete expect(button.iconPath).toBe(customPath) const icon = button.querySelector('pkt-icon:not(.pkt-btn__spinner)') expect(icon?.getAttribute('path')).toBe(customPath) }) test('does not set path attribute when iconPath is not provided', async () => { const { button } = await createButtonTest({ variant: 'icon-left', iconName: 'user', content: 'Test Button', }) await button.updateComplete expect(button.iconPath).toBeUndefined() const icon = button.querySelector('pkt-icon:not(.pkt-btn__spinner)') expect(icon?.hasAttribute('path')).toBe(false) }) test('handles secondIconPath property correctly', async () => { const customPath = 'https://custom-cdn.example.com/icons/' const { button } = await createButtonTest({ variant: 'icons-right-and-left', iconName: 'home', secondIconPath: customPath, content: 'Test Button', }) button.secondIconName = 'star' await button.updateComplete expect(button.secondIconPath).toBe(customPath) const icons = button.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)') expect(icons).toHaveLength(2) expect(icons[1]?.getAttribute('path')).toBe(customPath) }) test('handles both iconPath and secondIconPath independently', async () => { const iconPath = 'https://custom-cdn.example.com/icons/' const secondIconPath = 'https://another-cdn.example.com/icons/' const { button } = await createButtonTest({ variant: 'icons-right-and-left', iconName: 'home', iconPath: iconPath, secondIconPath: secondIconPath, content: 'Test Button', }) button.secondIconName = 'star' await button.updateComplete expect(button.iconPath).toBe(iconPath) expect(button.secondIconPath).toBe(secondIconPath) const icons = button.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)') expect(icons).toHaveLength(2) expect(icons[0]?.getAttribute('path')).toBe(iconPath) expect(icons[1]?.getAttribute('path')).toBe(secondIconPath) }) }) describe('Disabled state', () => { test('handles disabled property correctly', async () => { const { button } = await createButtonTest({ disabled: true, content: 'Test Button' }) await button.updateComplete expect(button.disabled).toBe(true) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass('pkt-btn--disabled') expect(buttonEl?.hasAttribute('disabled')).toBe(true) expect(buttonEl?.getAttribute('aria-disabled')).toBe('true') }) test('prevents click events when disabled', async () => { const { button } = await createButtonTest({ disabled: true, content: 'Test Button' }) const clickSpy = vi.fn() await button.updateComplete button.addEventListener('click', clickSpy) const buttonEl = button.querySelector('button') fireEvent.click(buttonEl!) expect(clickSpy).not.toHaveBeenCalled() }) test('prevents keyboard events when disabled', async () => { const { button } = await createButtonTest({ disabled: true, content: 'Test Button' }) const clickSpy = vi.fn() await button.updateComplete button.addEventListener('click', clickSpy) const buttonEl = button.querySelector('button') fireEvent.keyDown(buttonEl!, { key: 'Enter' }) fireEvent.keyDown(buttonEl!, { key: ' ' }) expect(clickSpy).not.toHaveBeenCalled() }) test('converts string "false" to boolean false for disabled', async () => { const { button } = await createButtonTest({ disabled: false, content: 'Test Button' }) await button.updateComplete expect(button.disabled).toBe(false) const buttonEl = button.querySelector('button') expect(buttonEl).not.toHaveClass('pkt-btn--disabled') expect(buttonEl?.hasAttribute('disabled')).toBe(false) }) }) describe('Loading state', () => { test('handles isLoading property correctly', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) // Set isLoading as a property button.isLoading = true await button.updateComplete expect(button.isLoading).toBe(true) const buttonEl = button.querySelector('button') expect(buttonEl).toHaveClass('pkt-btn--isLoading') expect(buttonEl?.getAttribute('aria-busy')).toBe('true') expect(buttonEl?.getAttribute('aria-disabled')).toBe('true') }) test('renders loading spinner when isLoading is true', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) // Set isLoading as a property button.isLoading = true await button.updateComplete const spinner = button.querySelector('.pkt-btn__spinner') expect(spinner).toBeInTheDocument() expect(spinner?.getAttribute('name')).toBe('spinner-blue') }) test('prevents click events when loading', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) const clickSpy = vi.fn() // Set isLoading as a property button.isLoading = true await button.updateComplete button.addEventListener('click', clickSpy) const buttonEl = button.querySelector('button') fireEvent.click(buttonEl!) expect(clickSpy).not.toHaveBeenCalled() }) test('uses custom loading animation path', async () => { const customPath = 'https://custom.example.com/animations/' const { button } = await createButtonTest({ isLoading: true, content: 'Test Button' }) button.loadingAnimationPath = customPath await button.updateComplete expect(button.loadingAnimationPath).toBe(customPath) const spinner = button.querySelector('.pkt-btn__spinner') expect(spinner?.getAttribute('path')).toBe(customPath) }) test('converts string "false" to boolean false for isLoading', async () => { const { button } = await createButtonTest({ isLoading: false, content: 'Test Button' }) await button.updateComplete expect(button.isLoading).toBe(false) const buttonEl = button.querySelector('button') expect(buttonEl).not.toHaveClass('pkt-btn--isLoading') }) }) describe('Form integration', () => { test('handles form attribute correctly', async () => { const { button } = await createButtonTest({ form: 'test-form', type: 'submit', content: 'Test Button', }) await button.updateComplete expect(button.form).toBe('test-form') const buttonEl = button.querySelector('button') expect(buttonEl?.getAttribute('form')).toBe('test-form') }) test('works as submit button', async () => { const { button } = await createButtonTest({ type: 'submit', content: 'Test Button' }) await button.updateComplete const buttonEl = button.querySelector('button') expect(buttonEl?.getAttribute('type')).toBe('submit') }) }) describe('Click functionality', () => { test('allows click events when not disabled or loading', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) const clickSpy = vi.fn() await button.updateComplete button.addEventListener('click', clickSpy) const buttonEl = button.querySelector('button') fireEvent.click(buttonEl!) expect(clickSpy).toHaveBeenCalledTimes(1) }) test('allows keyboard activation when not disabled or loading', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) const clickSpy = vi.fn() await button.updateComplete button.addEventListener('click', clickSpy) const buttonEl = button.querySelector('button') fireEvent.keyDown(buttonEl!, { key: 'Enter' }) // Note: Native button handles Enter key, so we just test that events aren't prevented // The actual click event would be triggered by the browser }) }) describe('Accessibility', () => { test('has correct ARIA attributes', async () => { const { button } = await createButtonTest({ disabled: true, content: 'Test Button' }) // Set isLoading as a property button.isLoading = true await button.updateComplete const buttonEl = button.querySelector('button') expect(buttonEl?.getAttribute('aria-disabled')).toBe('true') expect(buttonEl?.getAttribute('aria-busy')).toBe('true') expect(buttonEl?.hasAttribute('disabled')).toBe(true) }) test('provides semantic button structure', async () => { const { button } = await createButtonTest({ content: 'Test Button' }) await button.updateComplete const buttonEl = button.querySelector('button') expect(buttonEl).toBeInTheDocument() expect(buttonEl?.tagName.toLowerCase()).toBe('button') expect(buttonEl?.getAttribute('type')).toBe('button') }) test('renders with no WCAG errors with axe - default button', async () => { const { container } = await createButtonTest({ content: 'Click me' }) const results = await axe(container) expect(results).toHaveNoViolations() }) test('renders with no WCAG errors with axe - icon button', async () => { const { container } = await createButtonTest({ variant: 'icon-left', iconName: 'user', skin: 'secondary', content: 'User Profile', }) const results = await axe(container) expect(results).toHaveNoViolations() }) test('renders with no WCAG errors with axe - disabled button', async () => { const { container } = await createButtonTest({ disabled: true, content: 'Disabled Button' }) const results = await axe(container) expect(results).toHaveNoViolations() }) test('renders with no WCAG errors with axe - loading button', async () => { const { container } = await createButtonTest({ isLoading: true, content: 'Loading...' }) const results = await axe(container) expect(results).toHaveNoViolations() }) test('renders with no WCAG errors with axe - submit button', async () => { const { container } = await createButtonTest({ type: 'submit', color: 'green', content: 'Submit Form', }) const results = await axe(container) expect(results).toHaveNoViolations() }) }) })