import assert from 'node:assert/strict'; import {after,before,describe,it} from 'node:test'; import type {Browser} from 'playwright-core'; import {launch,layout,requiredEnv,VIEWPORTS} from './harness.ts'; describe('新作封面显示时机',()=>{ let browser:Browser; before(async()=>{browser=await launch()}); after(async()=>{await browser?.close()}); for(const viewport of VIEWPORTS){ it(`${viewport.name} 新作正封完整居中并补底,整幅大封不使用模糊`,{timeout:60000},async()=>{ const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height},reducedMotion:'reduce'}); try{ await context.addInitScript(()=>localStorage.setItem('peach.settings.v1',JSON.stringify({detailAutoplay:false,feedAutoScroll:false}))); const page=await context.newPage(); const cases=[ {width:632,height:900,front:632}, {width:1500,height:1000,front:704}, {width:1500,height:1000,front:667,poster_box:{x0:833,y0:0,x1:1500,y1:1000,px:[1500,1000],method:'manual'}}, {width:1800,height:1000,front:850,wide:true}, {width:2184,height:1638,front:2184,layout:'still'}, ]; await page.route(/\/cover-image\?code=EXM-00\d+$/,route=>{ const one=cases[Number(new URL(route.request().url()).searchParams.get('code')!.slice(-1))-1]!; return route.fulfill({contentType:'image/svg+xml',body:``}); }); await page.route(/\/api\/entity\/shapes/,route=>route.fulfill({json:{home:{feed:true},entities:[]}})); await page.route(/\/api\/feeds\/discoveries\?/,route=>route.fulfill({json:{ok:true,items:cases.map((one,index)=>({ id:index+1,code:`EXM-00${index+1}`,title:one.wide?'Blu-ray 示例新作':'示例新作',cover_url:`https://covers.example.test/fit/${index}`,poster_box:one.poster_box,cover_frame:{layout:one.layout}, }))}})); await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/'); for(let index=0;index{ const pic=el.querySelector('.pic')!,img=pic.querySelector('img')!; const box=pic.getBoundingClientRect(),image=img.getBoundingClientRect(); const clip=getComputedStyle(img).clipPath.match(/[\d.]+/g)?.map(Number)||[0,0,0,0]; const left=image.left+image.width*clip[3]!/100,right=image.right-image.width*clip[1]!/100; return {width:box.width,height:box.height,left:left-box.left,right:box.right-right,background:getComputedStyle(pic,'::before').backgroundImage}; }); assert.ok(Math.abs(shape.width/shape.height-.75)<.001,'新作卡片保持固定比例'); assert.ok(Math.abs(shape.height-224)<1,'同排封面等高'); if(cases[index]!.layout==='still')assert.equal(shape.background,'none','整幅大封不使用模糊'); else{ assert.notEqual(shape.background,'none','普通正封的留白由封面模糊补底'); assert.ok(shape.left>=-1&&shape.right>=-1&&Math.abs(shape.left-shape.right)<1,'正封完整居中'); } } assert.ok((await layout(page)).scrollWidth<=viewport.width+1,'横滚限制在新作行'); await page.screenshot({path:`../build/feed-cover-fit-${viewport.name}.png`}); }finally{await context.close()} }); it(`${viewport.name} 新作头像显示人物图与首字并提供独立资料入口`,{timeout:60000},async()=>{ const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height},reducedMotion:'reduce'}); try{ await context.addInitScript(()=>localStorage.setItem('peach.settings.v1',JSON.stringify({detailAutoplay:false,feedAutoScroll:false}))); const page=await context.newPage(); const name='有很长名字的规范人物'; await page.route(/\/entity-image\?kind=performer&id=42/,route=>route.fulfill({contentType:'image/svg+xml',body:''})); await page.route(/\/api\/entity\/shapes/,route=>route.fulfill({json:{home:{feed:true},entities:[]}})); await page.route(/\/api\/feeds\/discoveries\?/,route=>route.fulfill({json:{ok:true,items:[ {id:1,code:'EXM-001',title:'示例新作',link:'https://javdb.com/v/example',performers:name+'、乙、丙、丁',release_date:'2026-10-30', performer_refs:[{id:42,name,has_image:true,image_version:'7'},...['乙','丙','丁'].map((name,index)=>({id:43+index,name,has_image:false}))]}, {id:2,code:'EXM-002',title:'未建档新作',performers:'未建档人物'}]}})); await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/stats'); await page.evaluate(()=>{history.pushState({},'','/');window.dispatchEvent(new PopStateEvent('popstate'))}); const card=page.locator('#feedNew [data-feed-id="1"]'); await card.waitFor({state:'visible'}); const avatar=card.getByRole('link',{name:'打开女优页:'+name,exact:true}); await avatar.locator('img').waitFor({state:'visible'}); assert.equal(await card.locator('.mav').count(),3); assert.equal(await card.locator('.mav img').count(),1); assert.equal(await avatar.getAttribute('href'),'/performers/42'); assert.equal(await avatar.evaluate(el=>getComputedStyle(el).pointerEvents),'auto'); const names=await card.locator('.feednewperformers').boundingBox(); const date=await card.locator('.feednewdate').boundingBox(); assert.ok(names&&date&&names.width>=30&&date.y>=names.y+names.height-1,'姓名和完整日期各有可见的一行'); assert.equal(await page.locator('#feedNew [data-feed-id="2"] .mav .ini').textContent(),'未'); const geometry=await layout(page); assert.ok(geometry.scrollWidth<=geometry.viewportWidth+1); await page.screenshot({path:`../build/feed-avatars-${viewport.name}.png`}); await avatar.click(); await page.waitForURL('**/performers/42'); }finally{await context.close()} }); } for(const viewport of VIEWPORTS)for(const delay of [0,1800]){ it(`${viewport.name} ${delay?'慢图':'预加载'}完成取景才显示`,{timeout:60000},async()=>{ const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height}}); try{ await context.addInitScript(()=>{ localStorage.setItem('peach.settings.v1',JSON.stringify({detailAutoplay:false})); (window as any).__unframed=[]; const check=(img:HTMLImageElement)=>{ if(!img.matches('.feednew img.cover')||img.dataset.frame)return; if(getComputedStyle(img).visibility==='visible'&&img.naturalWidth) (window as any).__unframed.push({src:img.src,complete:img.complete}); }; // 捕获监听先于应用执行,记录加载图尚未取景的那一刻。 document.addEventListener('load',event=>{ if(event.target instanceof HTMLImageElement)check(event.target); },true); const sample=()=>{document.querySelectorAll('.feednew img.cover').forEach(check);requestAnimationFrame(sample)}; requestAnimationFrame(sample); }); const page=await context.newPage(); const remote='https://covers.example.test/framing.svg',proxied=requiredEnv('PEACH_E2E_ORIGIN')+'/cover-image?code=EXM-001'; const audits=await context.newCDPSession(page), lazyIssues:string[]=[]; audits.on('Audits.issueAdded',({issue})=>{ if(issue.code==='LazyLoadImageIssue'&&issue.details.lazyLoadImageIssueDetails?.url===proxied) lazyIssues.push(issue.details.lazyLoadImageIssueDetails.url); }); await audits.send('Audits.enable'); await page.route(/\/cover-image\?code=EXM-001$/,async route=>{ if(delay)await new Promise(resolve=>setTimeout(resolve,delay)); await route.fulfill({contentType:'image/svg+xml',body:''}); }); await page.route(/\/api\/feeds\/discoveries\?/,route=>route.fulfill({json:{ok:true,items:[{id:1,code:'EXM-001',title:'示例新作',has_cover:false,cover_url:remote}]}})); await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/stats'); for(let round=0;round<2;round++){ await page.evaluate(()=>{history.pushState({},'','/');window.dispatchEvent(new PopStateEvent('popstate'))}); const image=page.locator('#feedNew img.cover.panel'); await image.waitFor({state:'visible'}); assert.equal(await image.evaluate(img=>img.style.getPropertyValue('--panel-clip')),'0% 0% 0% 53.07%'); const positions=await image.evaluate(async img=>{ const samples:string[]=[]; for(let n=0;n<12;n++){ await new Promise(requestAnimationFrame); const css=getComputedStyle(img); samples.push([css.left,css.top,css.width,css.height,css.clipPath].join('|')); } return samples; }); assert.equal(new Set(positions).size,1,'可见封面的取景保持稳定'); assert.deepEqual(lazyIssues,[],'正封裁切图在 DevTools 中有明确尺寸'); const ratio=await image.evaluate((img:HTMLImageElement)=>{ const box=img.getBoundingClientRect();return box.width/box.height; }); assert.ok(Math.abs(ratio-1.5)<.01,'裁切使用源图宽高比'); assert.deepEqual(await page.evaluate(()=>(window as any).__unframed),[],'已加载封面在取景完成前保持隐藏'); await page.evaluate(()=>{history.pushState({},'','/stats');window.dispatchEvent(new PopStateEvent('popstate'))}); await page.locator('#feedNew').waitFor({state:'hidden'}); } }finally{await context.close()} }); } for(const viewport of VIEWPORTS){ it(`${viewport.name} 设置里的视频大小同步首页和 JAV`,{timeout:60000},async()=>{ const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height},reducedMotion:'reduce'}); try{ await context.addInitScript(()=>localStorage.setItem('peach.settings.v1',JSON.stringify({detailAutoplay:false,homeLayout:'big',javLayout:'big'}))); const page=await context.newPage(); await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/'); await page.locator('#grid [data-media-card]').first().waitFor({state:'visible'}); if(viewport.name==='mobile')await page.locator('#filterBtn').click(); await page.locator('#settingsBtn').click(); await page.locator('#settingsPanel').getByRole('tab',{name:'浏览与播放',exact:true}).click(); await page.locator('#javSizeSetting label:has(input[value="small"])').click(); const saved=await page.evaluate(()=>JSON.parse(localStorage.getItem('peach.settings.v1')||'{}')); assert.equal(saved.homeLayout,'small');assert.equal(saved.javLayout,'small'); await page.locator('#settingsClose').click(); const ratio=await page.locator('#grid [data-media-pic]').first().evaluate(node=>{ const rect=node.getBoundingClientRect();return rect.width/rect.height; }); assert.ok(Math.abs(ratio-16/9)<.01); }finally{await context.close()} }); it(`${viewport.name} 竖版正封完整放入卡片`,{timeout:60000},async()=>{ const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height}}); try{ const page=await context.newPage(),remote='https://covers.example.test/front.svg'; await page.route(/\/cover-image\?code=EXM-001$/,route=>route.fulfill({contentType:'image/svg+xml',body:''})); await page.route(/\/api\/feeds\/discoveries\?/,route=>route.fulfill({json:{ok:true,items:[{id:1,code:'EXM-001',title:'示例新作',has_cover:false,cover_url:remote}]}})); await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/'); const image=page.locator('#feedNew img.cover[data-frame="front"]');await image.waitFor({state:'visible'}); assert.equal(await image.evaluate(img=>getComputedStyle(img).objectFit),'contain'); }finally{await context.close()} }); } });