import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import type { Browser } from 'playwright-core'; import { 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) { it(`${viewport.name} 新作封面抓取显示进度并在完成后换成图片`, { timeout: 60_000 }, async () => { const context = await browser.newContext({ viewport }); try { const page = await context.newPage(); let completed = false; await page.route('**/api/entity/shapes', route => route.fulfill({ json: { ok: true, entities: [], home: { feed: true } } })); await page.route(/\/api\/feeds\/discoveries\?/, route => route.fulfill({ json: { ok: true, items: [ { id: 1, code: 'EXM-001', title: '示例新作', has_cover: completed, cover_state: completed ? null : 'fetching' }, ] } })); await page.route(/\/cover\?code=EXM-001/, route => route.fulfill({ contentType: 'image/svg+xml', body: '' })); await page.goto(requiredEnv('PEACH_E2E_ORIGIN') + '/', { waitUntil: 'load' }); const pending = page.locator('#feedNew [data-feed-cover-state="fetching"]'); await pending.waitFor(); assert.equal(await pending.innerText(), '抓取中'); const color = await pending.evaluate(node => getComputedStyle(node.closest('.pic')!).backgroundColor); assert.notEqual(color, 'rgb(0, 0, 0)', '抓取中封面仍是黑底'); completed = true; await page.locator('#feedNew img.cover').waitFor({ timeout: 10_000 }); assert.equal(await pending.count(), 0); } finally { await context.close() } }); it(`${viewport.name} 首页筛选控件立即显示,浮层保留最终材质`, { timeout: 60_000 }, async () => { const context = await browser.newContext({ viewport }); let release = () => {}; const held = new Promise(resolve => { release = resolve }); try { await context.addInitScript(() => localStorage.setItem('peach.settings.v1', JSON.stringify({ detailAutoplay: false }))); const page = await context.newPage(); await page.route(/\/api\/(?:items|facets)\?/, async route => { await held; await route.continue() }); await page.goto(requiredEnv('PEACH_E2E_ORIGIN') + '/', { waitUntil: 'load' }); const frame = page.locator('[data-catalog-frame]'); await frame.waitFor(); assert.ok(await frame.locator('[data-entity-layout]').count(), '版式键没有立即显示'); assert.ok(await frame.locator('[data-entity-sort]').count(), '排序键没有立即显示'); const styles = await frame.evaluate(node => ({ filter: getComputedStyle(node).backdropFilter, background: getComputedStyle(node).backgroundColor })); assert.notEqual(styles.filter, 'none', '筛选框等待时失去了玻璃材质'); assert.notEqual(styles.background, 'rgba(0, 0, 0, 0)'); } finally { release(); await context.close() } }); for (const jav of [false, true]) it(`${viewport.name} ${jav ? 'JAV' : '普通'}艺人等待态遵循作品语境`, { timeout: 60_000 }, async () => { const context = await browser.newContext({ viewport }); let release = () => {}; const held = new Promise(resolve => { release = resolve }); try { const page = await context.newPage(); await page.route('**/performers/90001', async route => { const response = await route.fetch({ url: requiredEnv('PEACH_E2E_ORIGIN') + '/' }); await route.fulfill({ response }); }); await page.route('**/api/entity/shapes', route => route.fulfill({ json: { ok: true, entities: [ { id: 90001, kind: 'performer', names: ['演示艺人'], parts: ['costars', 'facts', ...(jav ? ['jav'] : [])] }, ] } })); await page.route(/\/api\/entity\?/, async route => { await held; await route.fulfill({ json: { error: '未取得' } }) }); await page.goto(requiredEnv('PEACH_E2E_ORIGIN') + '/performers/90001', { waitUntil: 'load' }); const root = page.locator('[data-skeleton="entity/performer"]'); await root.waitFor(); assert.ok(await root.locator('[data-entity-foot]').count(), '数字地址漏了相关艺人'); assert.ok(await root.locator('[data-entity-facts]').count(), '数字地址漏了资料表'); assert.ok(await root.locator('[data-entity-state]').count(), '观看状态被画成了占位'); assert.ok(await root.locator('[data-entity-sort]').count(), '排序控件被画成了占位'); assert.equal(await root.locator('[aria-label="JAV 卡片版式"]').count() > 0, jav, '卡片版式开关误用了艺人类型'); const ratio = await root.locator('[style*="--skeleton-card-ratio"]').first().evaluate(node => Number((node as HTMLElement).style.getPropertyValue('--skeleton-card-ratio'))); assert.equal(ratio, jav ? .75 : 16 / 9, '等待态封面比例与作品语境不一致'); } finally { release(); await context.close() } }); } /* 形状名单在后台任务频繁写账本时要算好几秒:只有目录页在本机没记过新作结论时才等它, 其余页面和记过结论的目录页都不该被它挡住开页。 */ it('形状名单迟迟不回时各页照常开页取数', { timeout: 90_000 }, async () => { const pages: [string, string, string | null][] = [ ['/tasks', '/api/tasks', null], ['/performers', '/api/index', null], ['/', '/api/items', '0'], ]; for (const [path, content, recalled] of pages) { const context = await browser.newContext({ viewport: VIEWPORTS[0] }); let release = () => {}; const held = new Promise(resolve => { release = resolve }); try { if (recalled !== null) await context.addInitScript(value => localStorage.setItem('peach.home-feed.v1', value), recalled); const page = await context.newPage(); await page.route('**/api/entity/shapes', async route => { await held; await route.continue() }); const requested = page.waitForRequest(request => new URL(request.url()).pathname === content, { timeout: 8_000 }); await page.goto(requiredEnv('PEACH_E2E_ORIGIN') + path, { waitUntil: 'load' }); await requested.catch(() => assert.fail(`${path} 在形状名单返回前没有开始读取 ${content}`)); } finally { release(); await context.close() } } }); });