Ctrl K
ComunidadGuías…

frontend/e2e/catalog-list-widths.test.ts

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

Archivo original ↗Preguntar a KiBot
import assert from 'node:assert/strict';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { after, before, describe, it } from 'node:test';
import type { Browser } from 'playwright-core';
import { launch, layout, VIEWPORTS, visit } 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: 90_000 }, async () => {
    const opened = await visit(browser, '/', viewport);
    const { page } = opened;
    try {
      const name = `${'很长的影片名称'.repeat(30)}.mp4`;
      await page.route('**/api/items?*', route => route.fulfill({ json: { items: [{
        id: 99001, name, creator: '示例作者', medium: 'video', duration: 60,
        size: 150994944, width: 1920, height: 1080, rating: 80, play_count: 3,
        o_count: 2, first_seen: '2026-01-03', last_played: 1767312000,
        code: 'TEST-001', release_date: '2026-01-02',
      }], total: 1 } }));
      await page.getByRole('radiogroup', { name: '馆藏显示方式' }).locator('label')
        .filter({ has: page.getByRole('radio', { name: '列表', exact: true }) }).click();
      const row = page.locator('[data-catalog-row][data-id="99001"]');
      await row.waitFor();
      const geometry = await row.evaluate(node => {
        const picture = node.querySelector('[data-list-picture]')!;
        const placeholder = picture.firstElementChild!;
        const a = picture.getBoundingClientRect(), b = placeholder.getBoundingClientRect();
        const title = node.querySelector<HTMLElement>('[data-list-name]')!;
        return { picture: getComputedStyle(picture).backgroundColor,
          expected: getComputedStyle(picture).getPropertyValue('--sunk').trim(),
          centerDelta: Math.abs(a.top + a.height / 2 - b.top - b.height / 2),
          titleWidth: title.clientWidth, titleHeight: title.clientHeight, fullHeight: title.scrollHeight,
          lineHeight: parseFloat(getComputedStyle(title).lineHeight), clamp: getComputedStyle(title).webkitLineClamp };
      });
      assert.ok(geometry.centerDelta <= 1, JSON.stringify(geometry));
      assert.notEqual(geometry.picture, 'rgb(255, 255, 255)');
      // 长标题在「作品」列里最多两行,多出的截掉。
      assert.ok(geometry.titleWidth < 320 && geometry.clamp === '2' && geometry.fullHeight > geometry.titleHeight
        && geometry.titleHeight <= geometry.lineHeight * 2 + 1, JSON.stringify(geometry));
      assert.equal(await row.locator('[data-list-name]').getAttribute('title'), name);
      const defaultLabels = ['作品', '艺人', '媒体库', '标签', '时长', '评分', '观看次数', '入库时间'];
      const visibleLabels = () => page.locator('[data-catalog-table] th:not([data-table-space]) > span').allTextContents();
      assert.deepEqual(await visibleLabels(), defaultLabels);
      await page.getByRole('button', { name: '显示列', exact: true }).click();
      await page.getByRole('button', { name: '显示全部列', exact: true }).click();
      await page.keyboard.press('Escape');
      assert.deepEqual(await visibleLabels(), ['作品', '艺人', '媒体库', '标签', '时长', '评分', '观看次数', '入库时间', '发行时间', '观看时间', '高潮计数', '分辨率', '体积', '番号']);
      for (const label of ['分辨率', '评分', '高潮计数', '观看次数', '时长', '体积', '入库时间', '观看时间', '发行时间', '番号']) {
        await page.getByRole('columnheader', { name: label, exact: false }).waitFor();
      }
      assert.ok((await row.textContent())?.includes('1920 × 1080'));
      assert.equal(await row.getByRole('group', { name: '评分 4 / 5', exact: true }).count(), 1);
      assert.equal(await row.locator('[data-rating-stars] [data-rate]').count(), 5);
      assert.equal(await row.locator('[data-rating-stars] [data-rate][data-on="true"]').count(), 4);
      assert.ok((await row.textContent())?.includes('2026-01-03'));
      const header = page.getByRole('columnheader', { name: /^作品/ });
      const initialWidth = (await header.boundingBox())!.width;
      if (!viewport.mobile) {
        const sort = await header.getAttribute('aria-sort');
        const handle = await header.locator('.react-aria-ColumnResizer').boundingBox();
        assert.ok(handle);
        await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
        await page.mouse.down();
        await page.mouse.move(handle.x + handle.width / 2 + 48, handle.y + handle.height / 2, { steps: 8 });
        await page.mouse.up();
        await page.waitForFunction(width => document.querySelector('[data-catalog-table] th[data-key="name"]')!.getBoundingClientRect().width > width + 20, initialWidth);
        assert.equal(await header.getAttribute('aria-sort'), sort);
      }
      const beforeKeyboard = (await header.boundingBox())!.width;
      const resizer = header.getByRole('slider');
      await resizer.focus();
      await resizer.press('Enter');
      await resizer.press('ArrowRight');
      await resizer.press('ArrowRight');
      await resizer.press('Tab');
      await page.waitForFunction(width => document.querySelector('[data-catalog-table] th[data-key="name"]')!.getBoundingClientRect().width > width, beforeKeyboard);
      const resizedWidth = (await header.boundingBox())!.width;
      await page.getByRole('button', { name: '显示列', exact: true }).click();
      const columnMenu = page.getByRole('dialog', { name: '表格显示列' });
      await columnMenu.locator('label').filter({ has: page.getByRole('checkbox', { name: '评分', exact: true }) }).click();
      await page.keyboard.press('Escape');
      assert.equal(await page.getByRole('columnheader', { name: /^评分/ }).count(), 0);
      await page.reload({ waitUntil: 'load' });
      await page.locator('[data-catalog-row][data-id="99001"]').waitFor();
      assert.ok(Math.abs((await header.boundingBox())!.width - resizedWidth) <= 1);
      assert.equal(await page.getByRole('columnheader', { name: /^评分/ }).count(), 0);
      await page.getByRole('button', { name: /作品列表排序/ }).click();
      const sortResponse = page.waitForResponse(response => {
        const url = new URL(response.url());
        return url.pathname === '/api/items' && url.searchParams.get('sort') === 'rating';
      });
      await page.getByRole('option', { name: '评分', exact: true }).click();
      await sortResponse;
      assert.equal(await page.getByRole('columnheader', { name: /^评分/ }).count(), 0);
      await page.getByRole('button', { name: '显示列', exact: true }).click();
      await page.getByRole('button', { name: '显示全部列', exact: true }).click();
      await page.keyboard.press('Escape');
      await page.getByRole('columnheader', { name: /^评分/ }).waitFor();
      const scroll = page.locator('[data-catalog-table] .overflow-x-auto');
      const scrolled = await scroll.evaluate(node => {
        node.scrollLeft = node.scrollWidth;
        return { left: node.scrollLeft, extra: node.scrollWidth - node.clientWidth };
      });
      assert.ok(scrolled.extra > 0 && scrolled.left > 0, JSON.stringify(scrolled));
      await page.getByRole('button', { name: '重置列宽', exact: true }).click();
      await page.waitForFunction(width => Math.abs(document.querySelector('[data-catalog-table] th[data-key="name"]')!.getBoundingClientRect().width - width) <= 1, initialWidth);
      await scroll.evaluate(node => { node.scrollLeft = 0 });
      await page.getByRole('button', { name: '显示列', exact: true }).click();
      await page.getByRole('button', { name: '恢复默认显示', exact: true }).click();
      await page.keyboard.press('Escape');
      assert.deepEqual(await visibleLabels(), defaultLabels);
      const measured = await layout(page);
      assert.ok(measured.scrollWidth <= measured.viewportWidth + 1, JSON.stringify(measured));
      const screenshots = join('..', 'build', 'agent-verification', 'library-table');
      mkdirSync(screenshots, { recursive: true });
      await page.screenshot({ path: join(screenshots, `${viewport.name}.png`) });
      await page.evaluate(() => {
        document.documentElement.dataset.theme = 'dark';
        document.documentElement.classList.add('dark');
      });
      await page.screenshot({ path: join(screenshots, `${viewport.name}-dark.png`) });
      assert.deepEqual(opened.problems.filter(line => !line.includes('VIDEOJS')), []);
    } finally { await opened.close() }
  });
});