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 'aria-hidden'?: string 'aria-label'?: 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 = '' 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(' { 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('custom'), }) 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('aria-hidden', () => { test('defaults to true on the SVG', async () => { const { icon } = await createIconTest({ name: 'arrow-right' }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('true') expect(svg?.getAttribute('aria-label')).toBeNull() }) test('aria-hidden="false" attribute renders aria-hidden="false" on the SVG with name fallback as aria-label', async () => { const { icon } = await createIconTest({ name: 'arrow-right', 'aria-hidden': 'false', }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('false') expect(svg?.getAttribute('aria-label')).toBe('arrow-right') }) test('aria-hidden="true" attribute renders aria-hidden="true" on the SVG', async () => { const { icon } = await createIconTest({ name: 'arrow-right', 'aria-hidden': 'true', }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('true') expect(svg?.getAttribute('aria-label')).toBeNull() }) test('updates the SVG when the ariaHidden property changes', async () => { const { icon } = await createIconTest({ name: 'arrow-right' }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete icon.ariaHidden = 'false' await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('false') }) }) describe('aria-label', () => { test('aria-label alone makes aria-hidden default to false on the SVG', async () => { const { icon } = await createIconTest({ name: 'arrow-right', 'aria-label': 'Gå til forsiden', }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('false') expect(svg?.getAttribute('aria-label')).toBe('Gå til forsiden') }) test('aria-label plus explicit aria-hidden="true" keeps the icon hidden and omits the label', async () => { const { icon } = await createIconTest({ name: 'arrow-right', 'aria-label': 'Lukk', 'aria-hidden': 'true', }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('true') expect(svg?.getAttribute('aria-label')).toBeNull() }) test('setting ariaLabel via JS flips aria-hidden to false on the SVG', async () => { const { icon } = await createIconTest({ name: 'arrow-right' }) await icon.updateComplete await new Promise((resolve) => setTimeout(resolve, 0)) await icon.updateComplete icon.ariaLabel = 'Lukk' await icon.updateComplete const svg = icon.querySelector('svg') expect(svg?.getAttribute('aria-hidden')).toBe('false') expect(svg?.getAttribute('aria-label')).toBe('Lukk') }) }) }) describe('Integration scenarios', () => { test('works with multiple icons simultaneously', async () => { const container = document.createElement('div') container.innerHTML = ` ` 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') }) }) })