import { describe, expect, it } from 'vitest'; import { COVER_FRONT_RATIO } from '../src/appearance/layout'; import { javImageKind, normalizeJavImage, normalizeJavPreferences, panelFrame, relayoutJavImages, syncJavImages } from '../src/jav-artwork'; describe('JAV 默认封面', () => { const jav = { is_jav: true, code: 'TEST-001', has_cover: true, has_thumb: true }; it('默认官方封面,两档都按作品身份选择图片', () => { expect(normalizeJavImage(undefined)).toBe('cover'); expect(normalizeJavImage('invalid')).toBe('cover'); expect(javImageKind(jav, 'cover')).toBe('cover'); expect(javImageKind(jav, 'thumbnail')).toBe('thumbnail'); expect(javImageKind({ ...jav, is_jav: false }, 'cover')).toBe('thumbnail'); }); it('缺官方封面或预览图时使用可用图片,两者缺失时留空', () => { expect(javImageKind({ ...jav, has_cover: false }, 'cover')).toBe('thumbnail'); expect(javImageKind({ ...jav, code: '' }, 'cover')).toBe('thumbnail'); expect(javImageKind({ ...jav, has_thumb: false }, 'thumbnail')).toBe('cover'); expect(javImageKind({ ...jav, has_cover: false, has_thumb: false }, 'cover')).toBe(''); }); it('原地更新多个表面的图片并可切回,保留播放器和节点', () => { const root = document.createElement('div'); root.innerHTML = '' + ['big', 'small'].map(layout => ``).join(''); const images = [...root.querySelectorAll('img')]; const video = root.querySelector('video'); syncJavImages(root, 'cover'); expect(images.every(img => img.classList.contains('cover'))).toBe(true); expect(images[0]?.classList.contains('front')).toBe(true); expect(images[1]?.classList.contains('whole')).toBe(true); syncJavImages(root, 'thumbnail'); expect(images.every(img => img.getAttribute('src') === '/poster?id=1&c=4' && !img.classList.contains('cover'))).toBe(true); expect(root.querySelector('video')).toBe(video); expect([...root.querySelectorAll('img')]).toEqual(images); }); it('换图时撤掉上一张算出的取景,模糊背景跟着一起撤', () => { // 取景是按上一张图的折痕和尺寸算的,留着会把正封摆在一块错位的区域上。 // 模糊背景挂在卡片上,`removeAttribute('style')` 够不着它,必须单独清。 const root = document.createElement('div'); root.innerHTML = '
' + '
'; const pic = root.querySelector('.pic') as HTMLElement; const img = root.querySelector('img') as HTMLImageElement; syncJavImages(root, 'thumbnail'); expect(img.classList.contains('panel')).toBe(false); expect(img.getAttribute('style')).toBeNull(); expect(pic.style.getPropertyValue('--cover-blur')).toBe(''); }); it('换版式在同一张图上换取景类名,来源不动,只交回已加载完、要立刻重算的那几张', () => { const root = document.createElement('div'); root.innerHTML = '
' + '
' + '' + ''; const [loaded, pending, thumb] = [...root.querySelectorAll('img')] as [HTMLImageElement, HTMLImageElement, HTMLImageElement]; Object.defineProperty(loaded, 'naturalWidth', { value: 800 }); Object.defineProperty(loaded, 'complete', { value: true }); Object.defineProperty(pending, 'complete', { value: false }); expect(relayoutJavImages(root, 'small')).toEqual([loaded]); expect([loaded.className, pending.className, thumb.className]) .toEqual(['poster cover whole', 'poster cover whole', 'poster']); expect(loaded.getAttribute('style')).toBeNull(); expect((root.firstElementChild as HTMLElement).style.getPropertyValue('--cover-blur')).toBe(''); expect([loaded, pending, thumb].map(img => img.dataset.javImageLayout)).toEqual(['small', 'small', 'small']); expect(loaded.getAttribute('src')).toBe('/cover?code=TEST-001'); // 同一版式再来一次什么都不做:卡片每次重画都会问一遍。 expect(relayoutJavImages(root, 'small')).toEqual([]); }); it.each(['big', 'small'])('%s 保留两种封面来源,保存后恢复同一组合', javLayout => { for (const javImage of ['cover', 'thumbnail']) { const settings = normalizeJavPreferences({ javLayout, javImage }); expect(settings).toEqual({ javLayout, javImage }); expect(normalizeJavPreferences(JSON.parse(JSON.stringify(settings)))).toEqual(settings); expect(javImageKind(jav, settings.javImage)).toBe(javImage); } }); it('沿用已存版式与预览图选择,未知值使用默认值', () => { expect(normalizeJavPreferences({ javLayout: 'preview', javImage: 'cover' })).toEqual({ javLayout: 'small', javImage: 'thumbnail' }); expect(normalizeJavPreferences({ javLayout: 'sleeve' })).toEqual({ javLayout: 'small', javImage: 'cover' }); expect(normalizeJavPreferences({ javLayout: 'cover' })).toEqual({ javLayout: 'big', javImage: 'cover' }); expect(normalizeJavPreferences({ javLayout: 'invalid', javImage: 'invalid' })).toEqual({ javLayout: 'big', javImage: 'cover' }); }); }); /* 大图版式的容器比例,取的就是版式那一层(`src/appearance/layout.ts`)真会用的那个值; 下面算式里写的 0.75 就是它。 */ const BIG_LAYOUT_RATIO = COVER_FRONT_RATIO; /** 算出来的那两档框永远满高贴右缘,纵向两个轴因此恒为 0 与 100。 */ const fullHeight = (left: number) => ({ top: 0, right: 0, bottom: 0, left }); describe('正封取景框换算成卡片里的位置', () => { it('正封装得下就居中,两侧留白相等', () => { // 1600×1000 的封套,折痕右侧那块正封从 887 开始。图片按卡片高度铺满后宽是 // 卡片的 1.6/0.75≈2.1333 倍,正封占其中 (1−0.554375)×2.1333≈0.9507 倍卡片宽, // 装得下:左右各留 (1−0.9507)/2≈0.0247,图片左缘因此推到 −1.158。 const fold = { x0: 887, y0: 0, x1: 1600, y1: 1000, method: 'fold', px: [1600, 1000] }; expect(panelFrame(fold, BIG_LAYOUT_RATIO)) .toEqual({ clip: fullHeight(55.44), left: -115.8, top: 0, height: 100 }); // 容器换一个比例,图片的渲染宽度跟着变:同一个框算出的位置必须跟着动。 expect(panelFrame(fold, 0.5)) .toEqual({ clip: fullHeight(55.44), left: -220, top: 0, height: 100 }); }); it('右半居中的框走同一套算术,方法只是它的来路', () => { // 800×538 是本机封套最常见的尺寸;没找到折痕时正面从右半 421 开始,框宽 379。 const half = { x0: 421, y0: 0, x1: 800, y1: 538, method: 'ratio', px: [800, 538] }; expect(panelFrame(half, BIG_LAYOUT_RATIO)) .toEqual({ clip: fullHeight(52.63), left: -101.3, top: 0, height: 100 }); }); it('正封比卡片宽时贴右缘,从左边切', () => { // 800×500 的正封宽高比 0.800,是本机最宽的一档,比 0.75 的容器还宽 6.7%。 // 居中会同时切掉两边,而标题、女优名和角标都压在右侧,只能让右缘对齐。 expect(panelFrame({ x0: 400, px: [800, 500] }, BIG_LAYOUT_RATIO)) .toEqual({ clip: fullHeight(50), left: -113.33, top: 0, height: 100 }); }); it('人手工框的四条边都作数,横着切一刀就要放大再上移', () => { // 800×538 里框出 (200,50)–(600,450):框比容器宽,贴右缘从左边切;框高只有源图的 // 400/538,图片要放到卡片高的 134.5% 才能让框铺满,多出来的顶上那截靠 top 拉上去。 const manual = { x0: 200, y0: 50, x1: 600, y1: 450, method: 'manual', px: [800, 538] }; expect(panelFrame(manual, BIG_LAYOUT_RATIO)).toEqual({ clip: { top: 9.29, right: 25, bottom: 16.36, left: 25 }, left: -100, top: -12.5, height: 134.5, }); }); it('没有框时返回 null,调用方退回贴右缘的取景', () => { // 本机 1014 张封面里 331 张判定为不裁,接口发的就是 null,这条路不能算出数来。 expect(panelFrame(null, BIG_LAYOUT_RATIO)).toBeNull(); expect(panelFrame(undefined, BIG_LAYOUT_RATIO)).toBeNull(); expect(panelFrame({ x0: 421 }, BIG_LAYOUT_RATIO)).toBeNull(); expect(panelFrame({ x0: Number.NaN, px: [800, 538] }, BIG_LAYOUT_RATIO)).toBeNull(); expect(panelFrame({ x0: 421, px: [0, 0] }, BIG_LAYOUT_RATIO)).toBeNull(); expect(panelFrame({ x0: 421, px: [800, 538] }, 0)).toBeNull(); }); it('折痕夹回图片之内,切掉整幅的框没有正封可摆', () => { // 坏数据按整幅可见处理,比拿负宽度算下去安全。 expect(panelFrame({ x0: -40, px: [1600, 1000] }, BIG_LAYOUT_RATIO)) .toEqual({ clip: fullHeight(0), left: -113.33, top: 0, height: 100 }); // 折痕落在图片右缘上:折痕右边一个像素都不剩,这时没有正封可摆。 expect(panelFrame({ x0: 1600, px: [1600, 1000] }, BIG_LAYOUT_RATIO)).toBeNull(); }); });