import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import type React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssetItem } from './AssetItem'; import { AssetSource, AssetType, type Asset } from '../../types/asset.types'; vi.mock('tdesign-react', () => ({ Checkbox: ({ checked, onChange }: any) => ( onChange?.(event.target.checked)} /> ), })); vi.mock('../lazy-image', () => ({ LazyImage: ({ src, alt, className, objectFit }: any) => ( {alt} ), })); vi.mock('../shared/VideoPosterPreview', () => ({ VideoPosterPreview: ({ src, poster, alt, className, fit }: any) => ( {alt} ), })); vi.mock('../shared/hover', () => ({ HoverTip: ({ children }: { children: React.ReactNode }) => <>{children}, })); vi.mock('../../hooks/useAssetSize', () => ({ useAssetSize: (_id: string, _url: string, size?: number) => size, })); vi.mock('../../hooks/useThumbnailUrl', () => ({ useThumbnailUrl: (url: string) => url, })); vi.mock('../../hooks/useUnifiedCache', () => ({ useUnifiedCache: () => ({ isCached: true, cacheWarning: undefined, }), })); const baseAsset = { source: AssetSource.LOCAL, createdAt: 1, size: 4096, }; const videoAsset: Asset = { ...baseAsset, id: 'video-asset', type: AssetType.VIDEO, url: '/media/demo.mp4', thumbnail: '/media/demo-poster.jpg', name: '视频素材', mimeType: 'video/mp4', }; const imageAsset: Asset = { ...baseAsset, id: 'image-asset', type: AssetType.IMAGE, url: '/media/demo.jpg', name: '图片素材', mimeType: 'image/jpeg', }; const audioAsset: Asset = { ...baseAsset, id: 'audio-asset', type: AssetType.AUDIO, url: '/media/demo.mp3', thumbnail: '/media/demo-cover.jpg', name: '音频素材', mimeType: 'audio/mpeg', }; function renderAssetItem( asset: Asset, props: Partial> = {} ) { return render( ); } describe('AssetItem hover preview', () => { beforeEach(() => { vi.spyOn(window.HTMLMediaElement.prototype, 'play').mockResolvedValue(); vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation( () => undefined ); }); afterEach(() => { vi.restoreAllMocks(); }); it('plays video on hover and restores the poster on mouse leave', async () => { const { container } = renderAssetItem(videoAsset); const item = container.querySelector('.asset-item') as HTMLElement; expect(container.querySelector('.asset-item__hover-video')).toBeNull(); fireEvent.mouseEnter(item); const hoverVideo = await waitFor(() => { const node = container.querySelector('.asset-item__hover-video'); expect(node).toBeInstanceOf(HTMLVideoElement); return node as HTMLVideoElement; }); expect(hoverVideo.getAttribute('src')).toBe(videoAsset.url); expect(hoverVideo.muted).toBe(true); expect(hoverVideo.loop).toBe(true); await waitFor(() => { expect(window.HTMLMediaElement.prototype.play).toHaveBeenCalled(); }); fireEvent.mouseLeave(item); await waitFor(() => { expect(container.querySelector('.asset-item__hover-video')).toBeNull(); }); expect(screen.getByAltText(videoAsset.name)).not.toBeNull(); }); it('plays audio on hover and restores the cover on mouse leave', async () => { const { container } = renderAssetItem(audioAsset); const item = container.querySelector('.asset-item') as HTMLElement; expect(container.querySelector('.asset-item__hover-audio')).toBeNull(); expect(screen.getByAltText(audioAsset.name)).not.toBeNull(); fireEvent.mouseEnter(item); const hoverAudio = await waitFor(() => { const node = container.querySelector('.asset-item__hover-audio'); expect(node).toBeInstanceOf(HTMLAudioElement); return node as HTMLAudioElement; }); expect(hoverAudio.getAttribute('src')).toBe(audioAsset.url); await waitFor(() => { expect(window.HTMLMediaElement.prototype.play).toHaveBeenCalled(); }); fireEvent.mouseLeave(item); await waitFor(() => { expect(container.querySelector('.asset-item__hover-audio')).toBeNull(); }); expect(screen.getByAltText(audioAsset.name)).not.toBeNull(); }); it('does not start video or audio hover playback when disabled', async () => { const { container, rerender } = renderAssetItem(videoAsset, { disableHoverPlayback: true, }); const videoItem = container.querySelector('.asset-item') as HTMLElement; fireEvent.mouseEnter(videoItem); expect(container.querySelector('.asset-item__hover-video')).toBeNull(); expect(window.HTMLMediaElement.prototype.play).not.toHaveBeenCalled(); rerender( ); const audioItem = container.querySelector('.asset-item') as HTMLElement; fireEvent.mouseEnter(audioItem); expect(container.querySelector('.asset-item__hover-audio')).toBeNull(); expect(window.HTMLMediaElement.prototype.play).not.toHaveBeenCalled(); }); it('passes cover fit to image and video thumbnails when requested', () => { renderAssetItem(imageAsset, { thumbnailFit: 'cover' }); expect( screen.getByAltText(imageAsset.name).getAttribute('data-object-fit') ).toBe('cover'); renderAssetItem(videoAsset, { thumbnailFit: 'cover' }); expect(screen.getByAltText(videoAsset.name).getAttribute('data-fit')).toBe( 'cover' ); }); });