@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
552 lines (426 loc) • 18.1 kB
text/typescript
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()
})
})
})