Ctrl K
ComunidadGuías…

frontend/e2e/orphan-table.test.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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(); }
  });
});