frontend/e2e/table-preferences.test.ts
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { mkdirSync } from 'node:fs';
import { after, before, describe, it } from 'node:test';
import type { Browser, Locator, Page } from 'playwright-core';
import { followSource, openFollowManage } from './design-fixture.ts';
import { launch, layout, VIEWPORTS } from './harness.ts';
import { verifyTablePreferences } from './table-preferences-fixture.ts';
async function visibleSelectAll(table: Locator) {
const label = table.locator('thead label');
const box = (await label.boundingBox())!;
assert.ok(box.width >= 16 && box.height >= 16, JSON.stringify(box));
await label.click();
await table.locator('tbody input:checked:not([role="switch"])').first().waitFor({ state: 'attached' });
assert.ok(await table.locator('tbody input:checked:not([role="switch"])').count() > 0);
await label.click();
await table.locator('tbody input:checked:not([role="switch"])').first().waitFor({ state: 'detached', timeout: 5000 });
assert.equal(await table.locator('tbody input:checked:not([role="switch"])').count(), 0);
}
async function noPageOverflow(page: Page) {
const measured = await layout(page);
assert.ok(measured.scrollWidth <= measured.viewportWidth + 1, JSON.stringify(measured));
}
describe('所有数据表共用列设置', () => {
let browser: Browser;
before(async () => { browser = await launch(); mkdirSync('../build/agent-verification/all-tables', { recursive: true }) });
after(async () => { await browser.close() });
for (const viewport of VIEWPORTS) {
it(`${viewport.name}:关注来源可全选、调整列和拖动,名称右侧留给行选择`, { timeout: 60000 }, async () => {
const opened = await openFollowManage(browser, viewport);
const { page } = opened;
try {
await page.getByRole('button', { name: '表格视图', exact: true }).click();
const table = page.getByRole('grid', { name: '关注来源', exact: true });
await visibleSelectAll(table);
await verifyTablePreferences(page, table, '关注来源', 'source', { id: 'provider', label: '站点' },
() => page.getByRole('button', { name: '表格视图', exact: true }).click());
const row = table.locator('tbody tr').first(), cell = row.locator('td').nth(2), link = cell.locator('a');
await link.scrollIntoViewIfNeeded();
const hit = await cell.evaluate(node => {
const cell = node.getBoundingClientRect(), anchor = node.querySelector('a')!.getBoundingClientRect();
return { cell: cell.width, link: anchor.width, gap: cell.right - anchor.right };
});
assert.ok(hit.gap > 40 && hit.link < hit.cell - 40, JSON.stringify(hit));
await cell.click({ position: { x: Math.min(hit.cell - 8, hit.link + 28), y: 6 } });
assert.equal(await row.getByRole('checkbox').isChecked(), true);
await noPageOverflow(page);
await page.screenshot({ path: `../build/agent-verification/all-tables/follow-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:JAV 订阅共用列设置、可见全选和站点图标`, { timeout: 60000 }, async () => {
const opened = await openFollowManage(browser, viewport);
const { page } = opened;
try {
await page.route('**/api/feeds', route => route.fulfill({ json: { unread: 0, sources: [{
id: 1, kind: 'javdb-actor', kind_label: 'JAV 订阅', name: '示例订阅', url: 'https://javdb.com/actors/example',
entity_id: null, enabled: true, interval_minutes: 360, last_fetched_at: null, last_new_count: 0,
}] } }));
await page.reload({ waitUntil: 'load' });
await page.getByRole('tab', { name: '关注列表', exact: true }).waitFor();
const table = page.getByRole('grid', { name: 'JAV 订阅源', exact: true });
await visibleSelectAll(table);
assert.equal(await table.locator('tbody img[src="/site-mark?source=javdb"]').count(), 1);
await verifyTablePreferences(page, table, 'JAV 订阅源', 'name', { id: 'interval', label: '频率' });
await noPageOverflow(page);
await page.screenshot({ path: `../build/agent-verification/all-tables/feeds-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:两张别名表列设置独立,空白选择和改名按钮不冲突`, { timeout: 60000 }, async () => {
const opened = await openFollowManage(browser, viewport);
const { page } = opened;
try {
const name = 'Suzutaru 3D (鈴太郎3D)'; let renamed = false;
await page.route('**/api/follow?summary=1', route => route.fulfill({ json: {
sources: [followSource(1, name, 'Kemono', name)], counts: {}, suggestions: [],
author_aliases: [{ canonical_key: renamed ? '鈴太郎3d' : 'suzutaru3d(鈴太郎3d)', canonical_name: renamed ? '鈴太郎3D' : name,
aliases: [{ key: 'platform', name: renamed ? name : '鈴太郎3D' }] }],
alias_suggestions: [{ canonical: '作者 B', alias: '平台别名 B', evidence: '来源确认' }],
} }));
await page.goto(new URL('/subscriptions?tab=add', page.url()).href, { waitUntil: 'load' });
const pending = page.getByRole('grid', { name: '待合并创作者别名', exact: true });
const saved = page.getByRole('grid', { name: '已保存创作者别名', exact: true });
await verifyTablePreferences(page, pending, '待合并创作者别名', 'canonical', { id: 'evidence', label: '依据' });
await verifyTablePreferences(page, saved, '已保存创作者别名', 'canonical', { id: 'aliases', label: '平台别名' });
const identity = saved.locator('tbody td').nth(1);
const inset = await identity.evaluate(node => node.querySelector('span')!.getBoundingClientRect().left - node.getBoundingClientRect().left);
assert.ok(inset <= 16, `头像左侧间隔 ${inset}`);
await identity.click({ position: { x: 260, y: 4 } });
assert.equal(await saved.locator('tbody input').isChecked(), true);
await identity.click({ position: { x: 260, y: 4 } });
await saved.getByRole('button', { name: `修改 ${name} 的统一名称`, exact: true }).click();
const dialog = page.getByRole('dialog', { name: '修改统一名称', exact: true });
await dialog.getByRole('button', { name: '鈴太郎3D', exact: true }).click();
await page.route('**/api/follow/author-alias', route => {
assert.equal(route.request().postDataJSON().action, 'rename'); renamed = true;
return route.fulfill({ json: { ok: true } });
});
await dialog.getByRole('button', { name: '修改统一名称', exact: true }).click();
await saved.getByText('鈴太郎3D', { exact: true }).waitFor();
await noPageOverflow(page);
await page.screenshot({ path: `../build/agent-verification/all-tables/aliases-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
}
});