import React from 'react'; import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent } from '@testing-library/react'; import { PixelGlitch } from '../../animations/PixelGlitch'; import { mockMatchMedia } from './matchmedia-mock'; /** * jsdom lacks native CSS animation support, so React registers the * vendor-prefixed `webkitAnimationEnd` event there. Dispatch both variants — * exactly one is registered, so the handler fires exactly once. */ function dispatchAnimationEnd(el: Element) { fireEvent.animationEnd(el); el.dispatchEvent(new Event('webkitAnimationEnd', { bubbles: true })); } describe('PixelGlitch', () => { it('renders three copies of children when active (2 ghost layers + main)', () => { const { getAllByText } = render(glitchy); expect(getAllByText('glitchy')).toHaveLength(3); }); it('ghost layers are aria-hidden and run the R/C ghost keyframes', () => { const { container } = render(x); const ghosts = container.querySelectorAll('[aria-hidden]'); expect(ghosts).toHaveLength(2); expect((ghosts[0] as HTMLElement).style.animation).toContain('pxl-glitch-r'); expect((ghosts[1] as HTMLElement).style.animation).toContain('pxl-glitch-c'); }); it('main layer runs pxl-glitch with the default duration', () => { const { container } = render(x); const wrapper = container.firstElementChild as HTMLElement; const main = wrapper.lastElementChild as HTMLElement; expect(main.style.animation).toContain('pxl-glitch'); expect(main.style.animation).toContain('3000ms'); }); it('intensity prop drives the --pxl-glitch-x custom property', () => { const { container } = render(x); const wrapper = container.firstElementChild as HTMLElement; const main = wrapper.lastElementChild as HTMLElement; expect(main.style.getPropertyValue('--pxl-glitch-x')).toBe('9px'); }); it('trigger={false} renders no ghost layers and no main animation', () => { const { container, getAllByText } = render( x, ); expect(container.querySelectorAll('[aria-hidden]')).toHaveLength(0); expect(getAllByText('x')).toHaveLength(1); const wrapper = container.firstElementChild as HTMLElement; const main = wrapper.lastElementChild as HTMLElement; expect(main.style.animation).toBe(''); }); it('fires onComplete when the main layer animation ends', () => { const onComplete = vi.fn(); const { container } = render(x); const wrapper = container.firstElementChild as HTMLElement; const main = wrapper.lastElementChild as HTMLElement; dispatchAnimationEnd(main); expect(onComplete).toHaveBeenCalledTimes(1); }); it('renders a single static copy (no ghost layers, no animation) when the user prefers reduced motion', () => { const ctl = mockMatchMedia(true); try { const { container, getAllByText } = render(calm); expect(container.querySelectorAll('[aria-hidden]')).toHaveLength(0); expect(getAllByText('calm')).toHaveLength(1); const wrapper = container.firstElementChild as HTMLElement; const main = wrapper.lastElementChild as HTMLElement; expect(main.style.animation).toBe(''); } finally { ctl.restore(); } }); it('merges className onto the relative inline-block wrapper', () => { const { container } = render(x); const wrapper = container.firstElementChild as HTMLElement; expect(wrapper.className).toContain('relative'); expect(wrapper.className).toContain('inline-block'); expect(wrapper.className).toContain('custom'); }); });