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() }
}
});
});