Files
TrueGrowth/packages/drawnix/src/components/media-library/AssetItem.test.tsx
jiam 5119ac0ef8
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Sync latest TrueGrowth updates
2026-07-08 02:03:18 +08:00

218 lines
5.9 KiB
TypeScript

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) => (
<input
aria-label="选择资产"
type="checkbox"
checked={checked}
onChange={(event) => onChange?.(event.target.checked)}
/>
),
}));
vi.mock('../lazy-image', () => ({
LazyImage: ({ src, alt, className, objectFit }: any) => (
<img
src={src}
alt={alt}
className={className}
data-object-fit={objectFit}
/>
),
}));
vi.mock('../shared/VideoPosterPreview', () => ({
VideoPosterPreview: ({ src, poster, alt, className, fit }: any) => (
<img
src={poster || src}
alt={alt}
className={`video-poster-preview__media ${className || ''}`}
data-fit={fit}
/>
),
}));
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<React.ComponentProps<typeof AssetItem>> = {}
) {
return render(
<AssetItem
asset={asset}
viewMode="grid"
isSelected={false}
onSelect={vi.fn()}
{...props}
/>
);
}
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(
<AssetItem
asset={audioAsset}
viewMode="grid"
isSelected={false}
onSelect={vi.fn()}
disableHoverPlayback
/>
);
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'
);
});
});