import '@testing-library/jest-dom' import { act, render, screen, waitFor } from '@testing-library/react' import { axe, toHaveNoViolations } from 'jest-axe' import { Truncate } from './Truncate' expect.extend(toHaveNoViolations) describe('MiddleTruncate', () => { let resizeObserverCallback: ResizeObserverCallback let observedElements: Set let originalGetComputedStyle: typeof window.getComputedStyle beforeEach(() => { observedElements = new Set() originalGetComputedStyle = window.getComputedStyle // Stable line-height/font-size for overflow calculations in tests window.getComputedStyle = ((...args: any[]) => { const style = originalGetComputedStyle(...(args as [Element])) // Keep CSSStyleDeclaration methods (like getPropertyValue) intact. return new Proxy(style, { get(target, prop) { if (prop === 'lineHeight') return '16px' return (target as any)[prop] }, }) }) as any // Mock ResizeObserver global.ResizeObserver = class ResizeObserver { constructor(callback: ResizeObserverCallback) { resizeObserverCallback = callback } observe(element: Element) { observedElements.add(element) } unobserve(element: Element) { observedElements.delete(element) } disconnect() { observedElements.clear() } } as any }) afterEach(() => { observedElements.clear() window.getComputedStyle = originalGetComputedStyle }) const triggerResize = (element: HTMLElement) => { resizeObserverCallback( [ { target: element, contentRect: {} as DOMRectReadOnly, borderBoxSize: [] as ResizeObserverSize[], contentBoxSize: [] as ResizeObserverSize[], devicePixelContentBoxSize: [] as ResizeObserverSize[], }, ], {} as ResizeObserver, ) } test('renders text content correctly', () => { render(short-filename.txt) expect( screen.getByText( (content, element) => content === 'short-filename.txt' && element?.getAttribute('data-pkt-truncate-part') === 'first', ), ).toBeInTheDocument() }) test('renders with custom tail length', () => { const longText = 'very-long-filename-that-needs-truncation.txt' const { container } = render({longText}) // Check that the full text is in the first part const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') const firstPart = container.querySelector('[data-pkt-truncate-part="first"]') expect(wrapper).toBeTruthy() expect(firstPart).toBeTruthy() expect(firstPart).toHaveTextContent(longText) expect(wrapper as HTMLElement).toContainElement(firstPart as HTMLElement) // Check that the tail part contains the last 3 characters const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveTextContent('txt') }) test('uses default tail length of 5', () => { const longText = 'very-long-filename.txt' const { container } = render({longText}) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveTextContent('e.txt') // last 5 characters }) test('does not show tail for very short strings', () => { const shortText = 'file.txt' // 8 characters, tail=5, so 5+3=8, no tail shown const { container } = render({shortText}) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveTextContent('') // no tail content (secondPart is null) }) test('tail is hidden when not overflowing', () => { const { container } = render(some-filename.txt) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveStyle({ display: 'none' }) }) test('tail becomes visible when overflowing', async () => { const { container } = render(very-long-filename-that-overflows.txt) const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') as HTMLElement const measure = container.querySelector('[data-pkt-truncate-part="measure"]') as HTMLElement const tail = container.querySelector('[data-pkt-truncate-part="tail"]') as HTMLElement expect(wrapper).toBeTruthy() expect(measure).toBeTruthy() expect(tail).toBeTruthy() // Simulate > 2 lines (lineHeight=16px -> maxHeight=32px) measure.getBoundingClientRect = () => ({ height: 40 } as any) // Trigger overflow recalculation (ResizeObserver observes wrapper) act(() => triggerResize(wrapper)) await waitFor(() => { expect(tail).toHaveStyle({ display: 'inline' }) }) }) test('observes the wrapper element with ResizeObserver', () => { const { container } = render(filename.txt) const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') expect(observedElements.has(wrapper!)).toBe(true) }) test('disconnects ResizeObserver on unmount', () => { const { unmount } = render(filename.txt) expect(observedElements.size).toBe(1) unmount() expect(observedElements.size).toBe(0) }) test('forwards additional props to wrapper span', () => { render( filename.txt , ) const wrapper = screen.getByTestId('test-truncate') expect(wrapper).toHaveClass('custom-class') expect(wrapper.tagName).toBe('SPAN') }) test('merges custom styles with default styles', () => { const { container } = render( filename.txt, ) const wrapper = container.firstChild as HTMLElement expect(wrapper).toHaveStyle({ display: 'grid', color: 'rgb(255, 0, 0)', fontSize: '14px', }) }) test('tail span has aria-hidden attribute', () => { const { container } = render(very-long-filename.txt) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveAttribute('aria-hidden', 'true') }) test('applies correct clamp styles to first part', () => { const { container } = render(filename.txt) const firstPart = container.querySelector('[data-pkt-truncate-part="first"]') expect(firstPart).toHaveStyle({ display: '-webkit-box', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'normal', overflowWrap: 'anywhere', }) expect(firstPart).toHaveStyle('-webkit-line-clamp: 2') }) test('applies correct styles to tail span', () => { const { container } = render(very-long-filename.txt) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveStyle({ whiteSpace: 'nowrap', paddingRight: '0.5rem', }) }) test('handles empty string', () => { const { container } = render({''}) const firstPart = container.querySelector('[data-pkt-truncate-part="first"]') expect(firstPart).toHaveTextContent('') }) test('handles string exactly equal to tail + 3', () => { const text = '12345678' // 8 characters, tail=5, so 5+3=8 const { container } = render({text}) const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]') expect(tailSpan).toHaveTextContent('') // secondPart should be null }) describe('accessibility', () => { test('renders with no wcag errors', async () => { const { container } = render(filename-with-accessibility.txt) const results = await axe(container) expect(results).toHaveNoViolations() }) test('renders with no wcag errors when overflowing', async () => { const { container } = render( very-long-filename-that-definitely-overflows-the-container.txt, ) const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') as HTMLElement const measure = container.querySelector('[data-pkt-truncate-part="measure"]') as HTMLElement measure.getBoundingClientRect = () => ({ height: 40 } as any) act(() => triggerResize(wrapper)) await waitFor(async () => { const results = await axe(container) expect(results).toHaveNoViolations() }) }) }) })