/* 卡片悬停预览:起不起、什么时候进入长悬停、怎么收掉。 */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { configureHoverPreview, releaseHover, releaseHoverPreviews, wireHover } from '../../src/card-art/hover'; const settings = { hoverDelaySeconds: 0, selecting: false, censored: false }; function card(): HTMLElement { const el = document.createElement('div'); el.dataset.mediaCard = ''; el.innerHTML = '
'; document.body.append(el); return el; } const enter = (el: HTMLElement) => el.dispatchEvent(new MouseEvent('mouseenter')); const leave = (el: HTMLElement) => el.dispatchEvent(new MouseEvent('mouseleave')); beforeEach(() => { Object.assign(settings, { hoverDelaySeconds: 0, selecting: false, censored: false }); configureHoverPreview({ selecting: () => settings.selecting, censored: () => settings.censored, delaySeconds: () => settings.hoverDelaySeconds, }); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); document.body.replaceChildren(); }); describe('悬停放大设置', () => { it('关闭时不启动计时,开启后按延迟放大', () => { const el = card(); wireHover(el, { id: 7, location: 'pikpak', has_thumb: true }); enter(el); vi.advanceTimersByTime(10000); expect(el.hasAttribute('data-previewing')).toBe(false); leave(el); settings.hoverDelaySeconds = 3; enter(el); expect(el.hasAttribute('data-previewing')).toBe(false); el.querySelector('img.ui-hvframes')!.dispatchEvent(new Event('load')); expect(el.hasAttribute('data-previewing')).toBe(true); vi.advanceTimersByTime(2999); expect(el.hasAttribute('data-longhover')).toBe(false); vi.advanceTimersByTime(1); expect(el.hasAttribute('data-longhover')).toBe(true); leave(el); expect(el.hasAttribute('data-previewing')).toBe(false); expect(el.hasAttribute('data-longhover')).toBe(false); }); it('计时中关闭后不进入放大状态:延时到点时再读一次设置', () => { settings.hoverDelaySeconds = 3; const el = card(); wireHover(el, { id: 7, location: 'pikpak', has_thumb: true }); enter(el); el.querySelector('img.ui-hvframes')!.dispatchEvent(new Event('load')); settings.hoverDelaySeconds = 0; vi.advanceTimersByTime(3000); expect(el.hasAttribute('data-longhover')).toBe(false); }); }); describe('远端源只扫接触印相', () => { it('叠一层扫视图,不碰封面那张图;移开就撤掉', () => { const el = card(); el.querySelector('[data-media-pic]')!.innerHTML = ''; wireHover(el, { id: 7, location: 'pikpak', has_thumb: true }); expect(el.dataset.hoverMode).toBe('frames'); enter(el); const layer = el.querySelector('img.ui-hvframes'); expect(layer?.getAttribute('src')).toBe('/poster?id=7&c=4'); expect(layer?.hidden).toBe(true); layer?.dispatchEvent(new Event('load')); expect(layer?.hidden).toBe(false); expect(el.querySelector('img.cover')?.getAttribute('src')).toBe('/cover?code=A'); leave(el); expect(el.querySelector('img.ui-hvframes')).toBeNull(); }); it('下一格先拉到手再换上去;没到手就停在当前这格,取图失败也放开闸', () => { const preloads: HTMLImageElement[] = []; const RealImage = globalThis.Image; vi.stubGlobal('Image', class { constructor() { const img = new RealImage(); preloads.push(img); return img; } }); const el = card(); wireHover(el, { id: 7, location: 'pikpak', has_thumb: true }); enter(el); const layer = el.querySelector('img.ui-hvframes')!; vi.advanceTimersByTime(430); expect(preloads).toHaveLength(0); layer.dispatchEvent(new Event('load')); vi.advanceTimersByTime(430); expect(preloads.map(img => img.getAttribute('src'))).toEqual(['/poster?id=7&c=5']); vi.advanceTimersByTime(430); expect(preloads).toHaveLength(1); expect(layer.getAttribute('src')).toBe('/poster?id=7&c=4'); preloads[0]!.onload!(new Event('load')); expect(new URL(layer.src).search).toBe('?id=7&c=5'); vi.advanceTimersByTime(430); expect(preloads[1]!.getAttribute('src')).toBe('/poster?id=7&c=6'); preloads[1]!.onerror!(new Event('error')); vi.advanceTimersByTime(430); expect(preloads[2]!.getAttribute('src')).toBe('/poster?id=7&c=6'); vi.unstubAllGlobals(); }); it('没有接触印相就不挂预览', () => { const el = card(); wireHover(el, { id: 7, location: 'pikpak', has_thumb: false }); enter(el); expect(el.querySelector('img.ui-hvframes')).toBeNull(); }); it('首图失败撤掉图层和倒计时,保留封面;重新进入可以再取图', () => { settings.hoverDelaySeconds = 3; const el = card(); el.querySelector('[data-media-pic]')!.innerHTML = ''; wireHover(el, { id: 7, location: '115', has_thumb: true }); enter(el); el.querySelector('img.ui-hvframes')!.dispatchEvent(new Event('error')); vi.advanceTimersByTime(5000); expect(el.querySelector('img.ui-hvframes')).toBeNull(); expect(el.hasAttribute('data-previewing')).toBe(false); expect(el.hasAttribute('data-longhover')).toBe(false); expect(el.querySelector('img.cover')?.getAttribute('src')).toBe('/cover?id=7'); leave(el); enter(el); expect(el.querySelector('img.ui-hvframes')).not.toBeNull(); }); it('卡片换资源后撤掉旧图层、计时与监听,只请求当前资源', () => { settings.hoverDelaySeconds = 3; const el = card(); wireHover(el, { id: 7, location: '115', has_thumb: true }); enter(el); const old = el.querySelector('img.ui-hvframes')!; wireHover(el, { id: 8, location: '115', has_thumb: false }); old.dispatchEvent(new Event('load')); enter(el); vi.advanceTimersByTime(5000); expect(el.querySelector('img.ui-hvframes')).toBeNull(); expect(el.hasAttribute('data-longhover')).toBe(false); wireHover(el, { id: 9, location: '115', has_thumb: true }); enter(el); expect(el.querySelectorAll('img.ui-hvframes')).toHaveLength(1); expect(el.querySelector('img.ui-hvframes')?.getAttribute('src')).toBe('/poster?id=9&c=4'); }); it('多选或遮挡时不起', () => { const el = card(); wireHover(el, { id: 7, location: 'pikpak', has_thumb: true }); settings.selecting = true; enter(el); expect(el.querySelector('img.ui-hvframes')).toBeNull(); settings.selecting = false; settings.censored = true; enter(el); expect(el.querySelector('img.ui-hvframes')).toBeNull(); }); }); describe('本地文件拉真视频', () => { it('停 340 ms 才起一段静音视频,同一时间只留一个', () => { const first = card(); const second = card(); wireHover(first, { id: 1, location: 'local' }); wireHover(second, { id: 2, location: 'local' }); expect(first.dataset.hoverMode).toBe('video'); enter(first); vi.advanceTimersByTime(339); expect(first.querySelector('video.hv')).toBeNull(); vi.advanceTimersByTime(1); const video = first.querySelector('video.hv'); expect(video?.getAttribute('src')).toBe('/stream?id=1'); expect(video?.muted).toBe(true); enter(second); vi.advanceTimersByTime(340); expect(first.querySelector('video.hv')).toBeNull(); expect(second.querySelector('video.hv')).not.toBeNull(); }); it('预览视频取得首帧后才覆盖封面', () => { const el = card(); wireHover(el, { id: 1, location: 'local' }); enter(el); vi.advanceTimersByTime(340); const video = el.querySelector('video.hv')!; let frame: (() => void) | undefined; Object.defineProperty(video, 'requestVideoFrameCallback', { value: (done: () => void) => { frame = done; return 1 } }); video.dispatchEvent(new Event('loadedmetadata')); expect(video.hasAttribute('data-playing')).toBe(false); video.dispatchEvent(new Event('loadeddata')); expect(video.hasAttribute('data-playing')).toBe(false); frame?.(); expect(video.hasAttribute('data-playing')).toBe(true); leave(el); expect(el.querySelector('video')).toBeNull(); }); it('滚动中划过的卡不起;到点时正在滚动也不放', () => { const scrolling = window as unknown as { __scrolling?: boolean }; const el = card(); wireHover(el, { id: 1, location: 'local' }); scrolling.__scrolling = true; enter(el); vi.advanceTimersByTime(340); expect(el.querySelector('video.hv')).toBeNull(); scrolling.__scrolling = false; enter(el); scrolling.__scrolling = true; vi.advanceTimersByTime(340); expect(el.querySelector('video.hv')).toBeNull(); delete scrolling.__scrolling; }); it('遮挡在防抖期间打开,到点也不放', () => { const el = card(); wireHover(el, { id: 1, location: 'local' }); enter(el); settings.censored = true; vi.advanceTimersByTime(340); expect(el.querySelector('video.hv')).toBeNull(); }); }); describe('收掉预览', () => { it('releaseHover 收掉这张卡自己的预览;releaseHoverPreviews 留下 except 那张', () => { const kept = card(); const dropped = card(); wireHover(kept, { id: 1, location: 'pikpak', has_thumb: true }); wireHover(dropped, { id: 2, location: 'pikpak', has_thumb: true }); enter(kept); enter(dropped); releaseHoverPreviews(document, kept); expect(kept.querySelector('img.ui-hvframes')).not.toBeNull(); expect(dropped.querySelector('img.ui-hvframes')).toBeNull(); releaseHover(kept); expect(kept.querySelector('img.ui-hvframes')).toBeNull(); }); it('卡片重画过、回调跟着旧元素走了,留在画面上的扫视图照样收掉', () => { const el = card(); el.querySelector('[data-media-pic]')!.innerHTML = ''; releaseHoverPreviews(); expect(el.querySelector('img.ui-hvframes')).toBeNull(); }); });