UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

370 lines (297 loc) 10.8 kB
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') }) }) })