218 lines
5.9 KiB
TypeScript
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'
|
|
);
|
|
});
|
|
});
|