Ctrl K
ComunidadGuías…

frontend/e2e/table-preferences-fixture.ts

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

Archivo original ↗Preguntar a KiBot
import assert from 'node:assert/strict';
import type { Locator, Page } from 'playwright-core';
import { settle, withPageDiagnostics } from './harness.ts';

/** 标题与整组操作共享一行或分行,同组列设置按钮保持在一起。 */
async function verifyToolbarWrapping(page: Page) {
  const viewport = page.viewportSize()!;
  try {
    for (const width of [...new Set([viewport.width, 650, 390])]) {
      await page.setViewportSize({ width, height: viewport.height });
      await settle(page);
      const toolbars = await page.locator('[data-responsive-toolbar]').evaluateAll(roots => roots.map(root => {
        const box = (element: Element) => {
          const r = element.getBoundingClientRect();
          return { left: r.left, right: r.right, top: r.top, bottom: r.bottom };
        };
        const actions = root.querySelector('[data-toolbar-actions]')!;
        return { root: box(root), heading: box(root.querySelector('[data-toolbar-heading]')!),
          actions: box(actions), buttons: [...actions.querySelectorAll('button')].map(box),
          columns: [...actions.querySelectorAll('[role="group"]')].map(group =>
            [...group.querySelectorAll(':scope > button')].map(box)) };
      }));
      for (const toolbar of toolbars) {
        assert.ok(toolbar.actions.left >= toolbar.root.left - 1 && toolbar.actions.right <= toolbar.root.right + 1);
        if (toolbar.actions.top < toolbar.heading.bottom - 1) {
          assert.ok(toolbar.actions.left >= toolbar.heading.right);
          const centers = toolbar.buttons.map(b => (b.top + b.bottom) / 2);
          assert.ok(Math.max(...centers) - Math.min(...centers) < 1,
            `${width}px 工具栏有部分操作被挤到下一行`);
        }
        for (const buttons of toolbar.columns) {
          assert.ok(Math.max(...buttons.map(b => b.top)) - Math.min(...buttons.map(b => b.top)) < 2,
            `${width}px 列设置按钮被拆行`);
        }
      }
      const footers = await page.locator('[data-table-pagination]').evaluateAll(roots => roots.map(root => {
        const r = root.getBoundingClientRect(), a = root.querySelector('[data-toolbar-actions]')!.getBoundingClientRect();
        return a.left >= r.left - 1 && a.right <= r.right + 1;
      }));
      assert.ok(footers.every(Boolean), `${width}px 分页操作越出容器`);
      const pagination = await page.locator('nav').filter({ has: page.getByRole('button', { name: '上一页', exact: true }) })
        .evaluateAll(roots => roots.map(root => {
          const buttons = [...root.querySelectorAll(':scope > button')];
          return buttons.length === 2 && Math.abs(buttons[0]!.getBoundingClientRect().y - buttons[1]!.getBoundingClientRect().y) < 1;
        }));
      assert.ok(pagination.every(Boolean), `${width}px 上一页和下一页被拆到不同行`);
    }
  } finally {
    await page.setViewportSize(viewport);
    await settle(page);
  }
}

/** 在真实表格上核验共享列设置、键盘调整、拖动和刷新恢复。 */
export async function verifyTablePreferences(page: Page, table: Locator, label: string, identity: string, optional: { id: string; label: string }, restoreView?: () => Promise<unknown>) {
  await table.waitFor();
  await verifyToolbarWrapping(page);
  const controls = page.getByRole('group', { name: `${label}列设置`, exact: true });
  const header = table.locator(`th[data-key="${identity}"]`);
  const width = async () => header.evaluate(node => node.getBoundingClientRect().width);
  const initial = await width();
  const handle = header.getByRole('slider');
  await withPageDiagnostics(page, `${label}进入键盘列宽调整`, async () => {
    // React Aria 首次进入集合会先聚焦行导航;确认该状态后再移入列宽手柄。
    await table.locator('tbody tr').first().focus();
    await table.locator('tbody tr[data-focused]').first().waitFor();
    await handle.focus();
    assert.equal(await handle.evaluate(node => document.activeElement === node), true,
      `${label}键盘调整前焦点必须位于列宽手柄`);
    await handle.press('Enter');
    await header.locator('.react-aria-ColumnResizer[data-resizing]').waitFor();
  });
  await handle.press('ArrowRight'); await handle.press('ArrowRight');
  await page.waitForFunction(({ node, initial }) => node && node.getBoundingClientRect().width > initial,
    { node: await header.elementHandle(), initial }, { timeout: 5000 });
  await handle.press('Tab');
  const resized = await width(); assert.ok(resized > initial, `${label}: ${initial} → ${resized}`);
  const scroll = table.locator('..');
  const range = await scroll.evaluate(node => node.scrollWidth - node.clientWidth);
  if (range > 1) {
    await scroll.evaluate(node => { node.scrollLeft = 0 });
    await table.locator('tbody tr').first().scrollIntoViewIfNeeded();
    const box = (await scroll.boundingBox())!, row = (await table.locator('tbody tr').first().boundingBox())!;
    const x = box.x + box.width - 20, y = row.y + row.height - 4;
    await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.move(x - 80, y, { steps: 8 }); await page.mouse.up();
    assert.ok(await scroll.evaluate(node => node.scrollLeft) >= Math.min(80, range) - 2, label);
    assert.equal(await table.locator('tbody input[type="checkbox"]:checked:not([role="switch"])').count(), 0, label);
  }
  await controls.getByRole('button', { name: '显示列', exact: true }).click();
  const menu = page.getByRole('dialog', { name: `${label}显示列`, exact: true });
  const actionLayout = await menu.evaluate(element => {
    const actions = element.querySelector<HTMLElement>('[data-selection-actions]')!;
    const buttons = [...actions.querySelectorAll('button')].map(button => {
      const box = button.getBoundingClientRect();
      const style = getComputedStyle(button);
      const naturalWidth = button.lastElementChild!.getBoundingClientRect().width
        + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)
        + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);
      return { width: box.width, height: box.height, top: box.top, naturalWidth,
        scrollWidth: button.scrollWidth, clientWidth: button.clientWidth };
    });
    const optionBox = element.querySelector('[data-selection-options]')!.getBoundingClientRect();
    const optionWidths = [...element.querySelectorAll('label')].map(label =>
      label.lastElementChild!.getBoundingClientRect().right - label.getBoundingClientRect().left);
    return { width: actions.getBoundingClientRect().width, top: actions.getBoundingClientRect().top,
      bottom: actions.getBoundingClientRect().bottom, placement: element.getAttribute('data-selection-placement'),
      contentWidth: element.getBoundingClientRect().width, naturalWidth: Math.max(...optionWidths, ...buttons.map(button => button.naturalWidth)),
      optionTop: optionBox.top, optionBottom: optionBox.bottom, buttons };
  });
  assert.ok(actionLayout.placement === 'top' ? actionLayout.bottom <= actionLayout.optionTop
    : actionLayout.top >= actionLayout.optionBottom, `${label} 选项应靠近触发器`);
  assert.equal(actionLayout.buttons.length, 2);
  assert.ok(Math.abs(actionLayout.contentWidth - actionLayout.naturalWidth) <= 1, `${label} 菜单宽度跟随内容`);
  for (const button of actionLayout.buttons) {
    assert.ok(Math.abs(button.width - actionLayout.width) <= 1, `${label} 长按钮铺满操作区`);
    assert.ok(button.scrollWidth <= button.clientWidth + 1, `${label} 按钮文案应完整显示`);
  }
  assert.ok(actionLayout.buttons[1]!.top >= actionLayout.buttons[0]!.top + actionLayout.buttons[0]!.height);
  await menu.locator('label').filter({ has: page.getByRole('checkbox', { name: optional.label, exact: true }) }).click();
  await page.keyboard.press('Escape');
  assert.equal(await table.locator(`th[data-key="${optional.id}"]`).count(), 0);
  await page.reload({ waitUntil: 'load' });
  await restoreView?.();
  await table.waitFor();
  assert.ok(Math.abs(await width() - resized) <= 1, label);
  assert.equal(await table.locator(`th[data-key="${optional.id}"]`).count(), 0);
  await controls.getByRole('button', { name: '显示列', exact: true }).click();
  await menu.getByRole('button', { name: '恢复默认显示', exact: true }).click();
  await page.keyboard.press('Escape');
  await table.locator(`th[data-key="${optional.id}"]`).waitFor();
  await controls.getByRole('button', { name: '重置列宽', exact: true }).click();
  await page.waitForFunction(({ node, initial }) => node && Math.abs(node.getBoundingClientRect().width - initial) <= 1,
    { node: await header.elementHandle(), initial }, { timeout: 5000 });
  assert.ok(Math.abs(await width() - initial) <= 1, label);
  assert.ok(await table.locator('th[data-table-space]').evaluate(node => node.getBoundingClientRect().width) <= 1,
    `${label} 默认剩余空间由名称列承接`);
  await scroll.evaluate(node => { node.scrollLeft = 0 });
}

/** 内容的可用宽度跟随列变化,文字完整保留在 DOM 中。 */
export async function verifyColumnContentWidth(table: Locator, id: string, selector: string) {
  const header = table.locator(`th[data-key="${id}"]`);
  const index = await header.evaluate(node => [...node.parentElement!.children].indexOf(node));
  const cell = table.locator('tbody tr').first().locator('td').nth(index);
  const content = cell.locator(selector).first();
  const measure = () => content.evaluate(node => ({ width: node.getBoundingClientRect().width,
    cell: node.closest('td')!.getBoundingClientRect().width,
    text: node.getAttribute('title') || node.getAttribute('aria-label') || node.textContent }));
  const initial = await measure();
  // 用户按住列边拖动;键盘调整时表格的方向键导航会抢走焦点,测不出内容宽度。
  const resizer = header.locator('.react-aria-ColumnResizer');
  // 滚到视口中间,避开右下角弹出的提示。
  await resizer.evaluate(node => node.scrollIntoView({ block: 'center', inline: 'nearest' }));
  const box = (await resizer.boundingBox())!;
  const page = content.page(), x = box.x + box.width / 2, y = box.y + box.height / 2;
  await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.move(x + 100, y, { steps: 10 }); await page.mouse.up();
  await page.waitForFunction(({ node, initial }) => node && node.closest('td')!.getBoundingClientRect().width > initial + 70,
    { node: await content.elementHandle(), initial: initial.cell }, { timeout: 5000 }).catch(() => undefined);
  const resized = await measure();
  assert.ok(resized.cell > initial.cell + 70, `${id} 列没有拉宽:${JSON.stringify({ initial, resized })}`);
  assert.ok(Math.abs((resized.width - initial.width) - (resized.cell - initial.cell)) <= 2,
    `${id} 内容没有随列拉宽:${JSON.stringify({ initial, resized })}`);
  assert.equal(resized.text, initial.text);
  const geometry = await content.evaluate(node => ({ content: node.getBoundingClientRect().right,
    cell: node.closest('td')!.getBoundingClientRect().right }));
  assert.ok(geometry.content <= geometry.cell, JSON.stringify(geometry));
}