frontend/e2e/orphan-table.test.ts
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
import { after, before, describe, it } from 'node:test';
import type { Browser } from 'playwright-core';
import { launch, layout, settle, VIEWPORTS, visit } from './harness.ts';
import { verifyTablePreferences, verifyColumnContentWidth } from './table-preferences-fixture.ts';
describe('孤儿记录数据表', () => {
let browser: Browser;
before(async () => { browser = await launch(); await mkdir('../build/table-evidence', { recursive: true }); });
after(async () => { await browser.close(); });
for (const viewport of VIEWPORTS) it(`${viewport.name}:面板内标题、共用分页与选中后的批量操作`, async () => {
const opened = await visit(browser, '/data-cleanup/links', viewport);
const { page } = opened;
try {
const surface = (node: Element) => {
const style = getComputedStyle(node);
return { shadow: style.boxShadow, border: style.border, radius: style.borderRadius };
};
const fieldsetFace = await page.locator('#link-manager section').evaluate(surface);
const items = Array.from({ length: 23 }, (_, index) => ({
id: index + 1, name: `作品 ${23 - index}・很长的作品名称用于复核截断和操作区域`,
code: '', location: 'pikpak', duration: 600, size: 10, vanished_at: 100,
has_thumb: false, records: { watch_queue: 1, last_played: 1 }, candidates: [],
}));
await page.route('**/api/orphan-records', (route) => route.fulfill({
status: 200, contentType: 'application/json', body: JSON.stringify({ total: items.length, items }),
}));
const writes: unknown[] = [];
await page.route('**/api/batch', async (route) => {
writes.push(route.request().postDataJSON());
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, purged: 2, blocked: [] }) });
});
await page.goto(new URL('/data-cleanup/orphan-records', page.url()).href, { waitUntil: 'load' });
const section = page.locator('#orphan-records');
const table = page.getByRole('grid', { name: '文件已消失的作品', exact: true });
await table.waitFor();
await settle(page);
await verifyTablePreferences(page, table, '文件已消失的作品', 'item', { id: 'records', label: '个人记录' });
await verifyColumnContentWidth(table, 'item', 'span[title]');
const widths = await table.locator('th').evaluateAll(nodes => Object.fromEntries(nodes.map(node =>
[node.getAttribute('data-key'), node.getBoundingClientRect().width])));
assert.ok(widths.item! > widths.target! * 2, JSON.stringify(widths));
assert.ok(widths.target! <= 144, JSON.stringify(widths));
const tableFace = await page.locator('#orphan-records [data-data-table-panel]').evaluate(surface);
assert.deepEqual(tableFace, fieldsetFace, '数据表面板与管理卡片使用统一边线和阴影');
const scroller = table.locator('..');
assert.equal(await scroller.getAttribute('data-overlay-scrollbar'), 'true');
if (viewport.mobile) {
const track = section.locator('[data-board-data-table] .ov-x');
await track.waitFor({ state: 'visible' });
const before = await track.locator('.ovthumb').evaluate(node => getComputedStyle(node).transform);
await scroller.evaluate(node => { node.scrollLeft = node.scrollWidth });
await page.waitForFunction(previous => {
const node = document.querySelector('#orphan-records [data-overlay-scrollbar]');
const thumb = document.querySelector('#orphan-records [data-board-data-table] .ov-x .ovthumb');
return !!node && !!thumb && node.scrollLeft > 0 && getComputedStyle(thumb).transform !== previous;
}, before);
const after = await track.locator('.ovthumb').evaluate(node => getComputedStyle(node).transform);
assert.notEqual(after, before, '横向滑块没有跟随表格滚动');
await scroller.evaluate(node => { node.scrollLeft = 0 });
}
assert.equal(await section.locator('[data-data-table-panel] #orphan-records-title').count(), 1);
assert.equal(await table.locator('tbody tr').count(), 20);
assert.equal(await section.getByRole('button', { name: '删除所选记录(0)', exact: true }).count(), 0);
const pagination = section.locator('[data-table-pagination]');
assert.ok((await pagination.textContent())?.includes('1–20 / 23 条作品'));
await pagination.getByRole('button', { name: /每页显示数量/ }).waitFor();
await section.getByRole('button', { name: /文件已消失的作品排序/ }).waitFor();
await page.setViewportSize({ width: viewport.width, height: 2400 });
await section.evaluate((element) => window.scrollTo(0, element.getBoundingClientRect().top + window.scrollY - 100));
await section.screenshot({ path: `../build/table-evidence/orphan-${viewport.name}.png` });
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await table.locator('tbody tr').first().locator('label:has(input[type="checkbox"])').click();
await section.locator('[data-selection-dock]').waitFor();
await section.getByRole('button', { name: '下一页', exact: true }).click();
await table.locator('tbody tr').first().locator('label:has(input[type="checkbox"])').click();
await table.getByRole('columnheader', { name: /^作品/ }).click({ position: { x: 40, y: 18 } });
assert.ok((await table.locator('tbody tr').first().textContent())?.includes('作品 1・'));
await pagination.scrollIntoViewIfNeeded();
await page.screenshot({ path: `../build/table-evidence/orphan-selected-${viewport.name}.png` });
await section.getByRole('button', { name: '删除所选记录(2)', exact: true }).click();
await page.getByRole('alertdialog').getByRole('button', { name: '取消', exact: true }).click();
await table.locator('thead label:has(input[type="checkbox"])').click();
await section.getByRole('button', { name: '删除所选记录(23)', exact: true }).click();
const dialog = page.getByRole('alertdialog');
const list = dialog.getByRole('list', { name: '所选记录', exact: true });
await list.waitFor();
assert.equal(await list.locator(':scope > li').count(), items.length);
assert.equal(await list.getAttribute('data-overlay-scrollbar'), 'true');
const overflow = await list.evaluate(node => node.scrollHeight > node.clientHeight);
assert.equal(overflow, true, '长清单在弹层内滚动');
const edges = list.locator('..').locator(':scope > .ui-ov-edges');
await page.waitForFunction(() => !!document.querySelector('dialog .ui-can-scroll-bottom'));
assert.equal(await edges.locator('.ui-ov-edge-bottom').evaluate(node => getComputedStyle(node).visibility), 'visible');
await list.evaluate(node => { node.scrollTop = node.scrollHeight / 2 });
await page.waitForFunction(() => !!document.querySelector('dialog .ui-can-scroll-top.ui-can-scroll-bottom'));
await list.evaluate(node => { node.scrollTop = node.scrollHeight });
await page.waitForFunction(() => !!document.querySelector('dialog .ui-can-scroll-top:not(.ui-can-scroll-bottom)'));
const box = await dialog.boundingBox();
assert.ok(box && box.y >= 0 && box.y + box.height <= viewport.height);
await dialog.screenshot({ path: `../build/table-evidence/orphan-confirm-${viewport.name}.png` });
await dialog.getByRole('button', { name: '取消', exact: true }).click();
assert.deepEqual(writes, []);
const geometry = await layout(page);
assert.ok(geometry.scrollWidth <= geometry.viewportWidth + 1, JSON.stringify(geometry));
assert.deepEqual(geometry.offenders, []);
assert.deepEqual(opened.problems, []);
} finally { await opened.close(); }
});
});