frontend/e2e/home-loading.test.ts
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import {after,before,describe,it} from 'node:test';
import type {Browser} from 'playwright-core';
import {expectBody,launch,requiredEnv,VIEWPORTS} from './harness.ts';
describe('首页加载结构',()=>{
let browser:Browser;
before(async()=>{browser=await launch()});
after(async()=>{await browser?.close()});
for(const viewport of VIEWPORTS)for(const hasFeed of [true,false]){
it(`${viewport.name} 新作区 ${hasFeed?'有内容':'为空'}`,{timeout:60000},async()=>{
const context=await browser.newContext({viewport:{width:viewport.width,height:viewport.height}});
let release:()=>void=()=>{};
try{
await context.addInitScript(({hasFeed})=>{
localStorage.setItem('peach.settings.v1',JSON.stringify({detailAutoplay:false,homeLayout:hasFeed?'big':'small'}));
const seen=new WeakSet<Element>();
(window as any).__catalogSkeletons=0;
(window as any).__catalogRatios=[];
new MutationObserver(()=>{document.querySelectorAll('#grid .catalog-skeleton').forEach(node=>{
if(node.closest('[data-content-skeleton][aria-hidden="true"]')||getComputedStyle(node).visibility==='hidden')return;
if(!seen.has(node)){seen.add(node);(window as any).__catalogSkeletons++;
(window as any).__catalogRatios.push(Number((node.querySelector('[style]') as HTMLElement)?.style.getPropertyValue('--skeleton-card-ratio')))}
});
if(!(window as any).__firstCard){
const card=document.querySelector('#grid [data-media-card]');
if(card){(window as any).__firstCard=card;(window as any).__firstTier=document.querySelector('[data-catalog-tier]')}
}
}).observe(document,{childList:true,subtree:true});
},{hasFeed});
const page=await context.newPage();
const problems:string[]=[];
page.on('pageerror',error=>problems.push(error.message));
page.on('console',message=>{if(message.type()==='error')problems.push(message.text())});
page.setDefaultTimeout(8000);
const scopes:string[]=[];
await page.route('**/api/sources',route=>route.fulfill({json:{ok:true,sources:[{location:'local',online:true},{location:'115',online:false}]}}));
await page.route(/\/api\/facets\?/,async route=>{
scopes.push(new URL(route.request().url()).searchParams.get('loc')||'');
await route.continue();
});
await page.route(/\/api\/tops\?/,async route=>{
const first=!(Number(new URL(route.request().url()).searchParams.get('page'))>0);
await route.fulfill({json:{performers:first?[{id:90001,k:'示例演员',n:1,has_image:false,has_avatar:false}]:[],studios:[]}});
});
await page.route('**/api/entity/shapes',async route=>{
const response=await route.fetch(),data=await response.json();
await route.fulfill({json:{...data,home:{feed:hasFeed}}});
});
await page.route('**/api/settings',async route=>{
if(route.request().method()!=='GET'){await route.continue();return}
await new Promise(resolve=>setTimeout(resolve,650));
await route.fulfill({json:{sidebarOrder:['','jav','follow','performers','tags','studios','flagged','manage']}});
});
const held=new Promise<void>(resolve=>{release=resolve});
await page.route(/\/api\/feeds\/discoveries\?/,async route=>{
await held;
await route.fulfill({json:{ok:true,items:hasFeed?[{id:1,code:'EXM-001',title:'示例新作',has_cover:false}]:[]}});
});
await page.goto(requiredEnv('PEACH_E2E_ORIGIN')+'/',{waitUntil:'load'});
const card=page.locator('#grid [data-media-card]').first();
await expectBody(page,'首页目录',[card],problems);
await page.waitForTimeout(1100);
const before=(await card.boundingBox())!;
const first=await card.elementHandle();
const tiers=await page.locator('[data-catalog-tier]').first().elementHandle();
assert.equal(await page.locator('#feedNew .feednewskeleton').count()>0,hasFeed);
if(hasFeed){
assert.equal(await page.locator('#feedNew .feednewrow').evaluate(node=>getComputedStyle(node).scrollbarWidth),'none','新作骨架不显示原生横向滚动条');
assert.equal(await page.locator('#feedNew > .ovtrack.ov-x:visible').count(),0,'新作骨架不显示外层横向轨道');
const colors=await page.locator('#feedNew .feednewskeleton .pic').first().evaluate(node=>{
const probe=document.createElement('i');probe.style.background='var(--hover)';node.append(probe);
const expected=getComputedStyle(probe).backgroundColor,actual=getComputedStyle(node).backgroundColor;
probe.remove();return {expected,actual};
});
assert.equal(colors.actual,colors.expected,'骨架圆角底色与占位层一致');
}
assert.equal(await page.evaluate(()=>(window as any).__catalogSkeletons),1,'目录骨架只展示一轮');
assert.deepEqual(await page.evaluate(()=>(window as any).__catalogRatios),[hasFeed?3/4:16/9]);
release();
if(hasFeed)await page.locator('#feedNew [data-feed-id]').first().waitFor({state:'visible'});
await page.waitForTimeout(1400);
const after=(await card.boundingBox())!;
assert.ok(Math.abs(after.y-before.y)<=1,`新作加载影响网格位置:${before.y} → ${after.y}`);
assert.equal(await first!.evaluate(node=>node.isConnected),true);
assert.equal(await tiers!.evaluate(node=>node.isConnected),true);
assert.equal(await page.evaluate(()=>(window as any).__firstCard?.isConnected&&(window as any).__firstTier?.isConnected),true);
assert.equal(await page.locator('#feedNew [data-skeleton]').count(),0);
assert.equal(await page.locator('#feedNew').getAttribute('aria-label'),'未入库的新作');
assert.ok(scopes.length>0);
assert.deepEqual([...new Set(scopes)],['local'],'默认筛选始终排除离线来源');
}finally{release();await context.close()}
});
}
});