@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
370 lines (297 loc) • 10.8 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { vi } from 'vitest'
import {
createElementTest,
BaseTestConfig,
setupConsoleMocking,
restoreConsoleMocking,
} from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import './icon'
import { PktIcon } from './icon'
expect.extend(toHaveNoViolations)
export interface IconTestConfig extends BaseTestConfig {
name?: string
path?: string
}
// Use shared framework
export const createIconTest = async (config: IconTestConfig = {}) => {
const { container, element } = await createElementTest<
CustomElementFor<'pkt-icon'>,
IconTestConfig
>('pkt-icon', config)
return {
container,
icon: element,
}
}
// Cleanup after each test
afterEach(() => {
document.body.innerHTML = ''
// Clean up sessionStorage after tests
sessionStorage.clear()
// Reset global variables
delete (window as any).pktFetch
delete (window as any).pktIconPath
// Restore console mocking
restoreConsoleMocking()
})
// Mock fetch for icon loading
const mockFetch = vi.fn()
const mockSvgContent =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="test-path"></path></svg>'
beforeEach(() => {
// Setup console mocking to suppress error logs during tests
setupConsoleMocking()
// Setup default mocks
mockFetch.mockResolvedValue({
ok: true,
text: () => Promise.resolve(mockSvgContent),
})
window.pktFetch = mockFetch
window.pktIconPath = 'https://test-cdn.example.com/icons/'
})
describe('PktIcon', () => {
describe('Rendering and basic functionality', () => {
test('renders without errors', async () => {
const { icon } = await createIconTest()
expect(icon).toBeInTheDocument()
await icon.updateComplete
expect(icon.classList.contains('pkt-icon')).toBe(true)
})
test('renders with default structure', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
expect(icon).toBeInTheDocument()
expect(icon.classList.contains('pkt-icon')).toBe(true)
})
test('renders nothing when no name is provided', async () => {
const { icon } = await createIconTest()
await icon.updateComplete
// Should render nothing meaningful when no name is provided (only Lit template comments)
expect(icon.innerHTML).not.toContain('<svg')
expect(icon.name).toBe('')
})
})
describe('Properties and attributes', () => {
test('applies default properties correctly', async () => {
const { icon } = await createIconTest()
await icon.updateComplete
expect(icon.name).toBe('')
expect(icon.path).toBe('https://test-cdn.example.com/icons/')
})
test('sets name property correctly', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
expect(icon.name).toBe('arrow-right')
expect(icon.getAttribute('name')).toBe('arrow-right')
})
test('sets path property correctly', async () => {
const customPath = 'https://custom-cdn.example.com/icons/'
const { icon } = await createIconTest({
path: customPath,
name: 'arrow-right',
})
await icon.updateComplete
expect(icon.path).toBe(customPath)
})
test('uses global pktIconPath when path not specified', async () => {
window.pktIconPath = 'https://global-cdn.example.com/icons/'
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
expect(icon.path).toBe('https://global-cdn.example.com/icons/')
})
})
describe('Icon loading functionality', () => {
test('fetches icon from CDN when name is provided', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
// Allow some time for async icon loading
await new Promise((resolve) => setTimeout(resolve, 0))
expect(mockFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/arrow-right.svg')
})
test('caches loaded icons in sessionStorage', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
// Wait for icon to load
await new Promise((resolve) => setTimeout(resolve, 0))
const cachedIcon = sessionStorage.getItem('https://test-cdn.example.com/icons/arrow-right.svg')
expect(cachedIcon).toBe(mockSvgContent)
})
test('uses cached icon when available', async () => {
// Pre-populate cache
sessionStorage.setItem('https://test-cdn.example.com/icons/cached-icon.svg', mockSvgContent)
const { icon } = await createIconTest({
name: 'cached-icon',
})
await icon.updateComplete
// Should not fetch since it's cached
expect(mockFetch).not.toHaveBeenCalledWith(
'https://test-cdn.example.com/icons/cached-icon.svg',
)
})
test('handles fetch errors gracefully', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
text: () => Promise.resolve(''),
})
const { icon } = await createIconTest({
name: 'missing-icon',
})
await icon.updateComplete
// Should log error and use error SVG
expect(mockFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/missing-icon.svg')
})
})
describe('Dynamic updates', () => {
test('updates icon when name changes', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
// Change the name
icon.name = 'arrow-left'
await icon.updateComplete
expect(icon.name).toBe('arrow-left')
expect(icon.getAttribute('name')).toBe('arrow-left')
})
test('updates icon when path changes', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
const newPath = 'https://new-cdn.example.com/icons/'
icon.path = newPath
await icon.updateComplete
expect(icon.path).toBe(newPath)
})
test('re-fetches icon when path changes', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
// Allow initial load to complete
await new Promise((resolve) => setTimeout(resolve, 0))
mockFetch.mockClear()
// Setup mock again for the new path
mockFetch.mockResolvedValue({
ok: true,
text: () => Promise.resolve(mockSvgContent),
})
const newPath = 'https://new-cdn.example.com/icons/'
// Try setting attribute to trigger attributeChangedCallback
icon.setAttribute('path', newPath)
await icon.updateComplete
// Allow some time for async icon loading
await new Promise((resolve) => setTimeout(resolve, 0))
expect(mockFetch).toHaveBeenCalledWith('https://new-cdn.example.com/icons/arrow-right.svg')
})
})
describe('CSS classes and styling', () => {
test('applies pkt-icon class', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
expect(icon.classList.contains('pkt-icon')).toBe(true)
})
test('maintains pkt-icon class after updates', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
icon.name = 'arrow-left'
await icon.updateComplete
expect(icon.classList.contains('pkt-icon')).toBe(true)
})
})
describe('Global configuration', () => {
test('uses custom pktFetch function when provided', async () => {
const customFetch = vi.fn().mockResolvedValue({
ok: true,
text: () => Promise.resolve('<svg>custom</svg>'),
})
window.pktFetch = customFetch
const { icon } = await createIconTest({
name: 'custom-icon',
})
await icon.updateComplete
// Allow some time for async icon loading
await new Promise((resolve) => setTimeout(resolve, 0))
expect(customFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/custom-icon.svg')
})
test('falls back to error SVG when pktFetch is not available', async () => {
delete (window as any).pktFetch
const { icon } = await createIconTest({
name: 'fallback-icon',
})
await icon.updateComplete
// Allow some time for async icon loading
await new Promise((resolve) => setTimeout(resolve, 0))
// Should render error SVG in light DOM when fetch is not available
expect(icon.innerHTML).toContain('viewBox="0 0 32 32"')
})
})
describe('Accessibility', () => {
test('basic icon is accessible', async () => {
const { container } = await createIconTest({
name: 'arrow-right',
})
await new Promise((resolve) => setTimeout(resolve, 0))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('icon with custom path is accessible', async () => {
const { container } = await createIconTest({
name: 'arrow-right',
path: 'https://custom-cdn.example.com/icons/',
})
await new Promise((resolve) => setTimeout(resolve, 0))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
describe('Integration scenarios', () => {
test('works with multiple icons simultaneously', async () => {
const container = document.createElement('div')
container.innerHTML = `
<pkt-icon name="arrow-right"></pkt-icon>
<pkt-icon name="arrow-left"></pkt-icon>
<pkt-icon name="close"></pkt-icon>
`
document.body.appendChild(container)
// Wait for elements to be defined
await customElements.whenDefined('pkt-icon')
const icons = container.querySelectorAll('pkt-icon')
expect(icons).toHaveLength(3)
for (const icon of icons) {
await (icon as PktIcon).updateComplete
expect(icon.classList.contains('pkt-icon')).toBe(true)
}
})
test('handles rapid name changes correctly', async () => {
const { icon } = await createIconTest({
name: 'arrow-right',
})
await icon.updateComplete
// Rapidly change names
icon.name = 'arrow-left'
icon.name = 'close'
icon.name = 'menu'
await icon.updateComplete
expect(icon.name).toBe('menu')
expect(icon.getAttribute('name')).toBe('menu')
})
})
})