frontend/e2e/jav-discovery-metadata.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 { launch, layout, settle, visit, 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 opened = await visit(browser, '/?jav=1', viewport);
const { page } = opened;
/* 每次请求记一笔:`list` 是推荐列表(只读,带打分),`progress` 是进度轮询,`control:*` 是起停。 */
const requests: string[] = [];
let details = 0, status = '', completed = false, polls = 0;
const job = () => (status ? { job_id: 'fixed-batch', status, completed: completed ? 2 : 0, total: 2,
failed: 0, requests: completed ? 4 : 0, bytes: 1000, message: status === 'stopped' ? '作品资料读取已停止' : '' } : null);
const reply = () => ({ metadata_job: job(), ...(status === 'running' ? { poll_after_ms: 1000 } : {}) });
try {
await settle(page);
await page.route('**/api/taste?*', route => route.fulfill({ json: { summary: { history_visits: 20 }, rankings: {} } }));
await page.route('**/api/jav/detail?*', route => { details++; return route.fulfill({ json: {} }) });
await page.route('**/api/jav/discoveries/metadata', route => {
if (route.request().method() === 'POST') {
const { action } = route.request().postDataJSON() as { action: string };
requests.push(`control:${action}`);
status = action === 'cancel' ? 'stopped' : completed ? 'complete' : 'running';
polls = 0;
} else {
requests.push('progress');
// 起任务后第二次轮询才读完:「停止读取」至少停留一个轮询间隔,负载高时也看得到。
if (status === 'running' && ++polls >= 2) { status = 'complete'; completed = true }
}
return route.fulfill({ json: reply() });
});
await page.route('**/api/jav/discoveries?*', route => {
const params = new URL(route.request().url()).searchParams;
requests.push('list');
assert.ok(!params.has('metadata_refresh') && !params.has('metadata_cancel'), '推荐列表只读,不起停任务');
const items = [777, 778].map((id, index) => ({ id, code: `DEMO-${id}`, title: '示例推荐', performers: '', studio: '',
release_date: `2026-10-${20 + index}`, has_cover: false, cover_url: '', wanted: false,
tags: completed ? ['恋足'] : [], taste_score: index ? 0 : 0.004, ranking_score: index ? -0.02 : 0.003,
source_score: completed ? 4.2 : null, reasons: ['匹配标签'], metadata_state: completed ? 'ready' : 'pending' }));
if (params.get('direction') === 'asc') items.reverse();
return route.fulfill({ json: { items, candidate_total: 2, limited: false,
ranking_method: params.get('sort') === 'taste' ? 'mmr' : 'field-sort', ...reply() } });
});
await page.goto(new URL('/jav?tab=discover', page.url()).href);
await page.getByRole('button', { name: '停止读取', exact: true }).waitFor({ state: 'visible' });
assert.equal(await page.locator('[data-jav-filter]').getByRole('button', { name: '我的收藏', exact: true }).count(), 1);
await page.locator('[data-catalog-tag="恋足"]').waitFor({ state: 'visible' });
assert.equal(details, 0);
// 打开页面补读一次;读的过程中进度单独轮询,推荐列表只在打开时与读完后各取一次。
assert.equal(requests.filter(kind => kind === 'control:start').length, 1);
assert.ok(requests.includes('progress'));
assert.ok(requests.filter(kind => kind === 'list').length <= 2, requests.join(','));
const cards = page.locator('[data-jav-card]');
assert.equal(await cards.first().locator('[data-jav-taste-score]').textContent(), '探索');
const score = cards.first().getByRole('button', { name: '推荐分 0,探索', exact: true });
await score.scrollIntoViewIfNeeded();
await score.focus();
await page.getByRole('tooltip').filter({ hasText: '口味 0.004 · 排序 0.003 · 来源评分 4.2 / 5' }).waitFor({ state: 'visible' });
const otherScore = cards.last().getByRole('button', { name: '推荐分 0,探索', exact: true });
await otherScore.scrollIntoViewIfNeeded();
await otherScore.focus();
await page.getByRole('tooltip').filter({ hasText: '口味 0.000 · 排序 -0.020' }).waitFor({ state: 'visible' });
await page.getByRole('button', { name: '按口味匹配排序', exact: true }).focus();
assert.equal(await cards.first().getByRole('button', { name: '恋足', exact: true }).count(), 1);
for (const [sort, name, direction] of [
['source_score', '按来源评分排序', 'desc'], ['duration', '按时长排序', 'desc'],
['code', '按番号排序', 'asc'], ['release_date', '按发行日期排序', 'desc'], ['taste', '按口味匹配排序', 'desc'],
]) {
await Promise.all([page.getByRole('button', { name, exact: true }).click(),
page.waitForResponse(response => { const params = new URL(response.url()).searchParams;
return response.url().includes('/api/jav/discoveries?') && params.get('sort') === sort && params.get('direction') === direction })]);
assert.equal(await page.getByRole('button', { name, exact: true }).getAttribute('aria-pressed'), 'true');
}
await page.getByRole('button', { name: '按口味匹配排序', exact: true }).click();
await page.waitForFunction(() => document.querySelector('[data-jav-card] h3')?.textContent?.startsWith('DEMO-778'));
completed = false; status = 'running';
await page.getByRole('button', { name: '更新资料', exact: true }).click();
await page.getByRole('button', { name: '停止读取', exact: true }).click();
await page.getByText('作品资料读取已停止', { exact: true }).waitFor({ state: 'visible' });
const count = requests.length;
await page.waitForTimeout(2200);
assert.equal(requests.length, count, '停止后没有自动重启或轮询');
completed = true;
await page.getByRole('button', { name: '立即重试读取作品资料', exact: true }).click();
await page.getByRole('button', { name: '更新资料', exact: true }).waitFor({ state: 'visible' });
await settle(page);
assert.equal(details, 0);
const geometry = await layout(page);
assert.ok(geometry.scrollWidth <= geometry.viewportWidth + 1);
assert.deepEqual(opened.problems.filter(line => !line.includes('/api/links/check')), []);
} finally { await opened.close() }
});
});