Ctrl K
ComunidadGuías…

frontend/e2e/design-detail-continuation.test.ts

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

Archivo original ↗Preguntar a KiBot
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { Browser, Page } from 'playwright-core';
import { DETAIL, openFollowFeed } from './follow-fixture.ts';
import { ITEM, openItemPage } from './item-fixture.ts';
import { launch, VIEWPORTS } from './harness.ts';

async function hoverClearance(page: Page, longHover: boolean) {
  const row = page.locator('#stage [data-related-row]');
  await row.evaluate(node => { node.scrollLeft = 0 });
  const card = row.locator('[data-media-card]').first();
  await card.hover();
  const edges = await card.evaluate(node => {
    const card = node.getBoundingClientRect();
    const row = node.closest('[data-related-row]')!.getBoundingClientRect();
    return [card.left - row.left, card.top - row.top, row.bottom - card.bottom];
  });
  assert.ok(edges.every(edge => edge >= 8 - .5), `8px 悬停面被滚动框裁切:${JSON.stringify(edges)}`);
  if (longHover) {
    const expanded = await card.evaluate(node => {
      node.setAttribute('data-longhover', '');
      node.getBoundingClientRect();
      node.getAnimations().forEach(animation => animation.finish());
      const card = node.getBoundingClientRect(), row = node.closest('[data-related-row]')!.getBoundingClientRect();
      return [card.left - row.left, card.top - row.top, row.bottom - card.bottom];
    });
    assert.ok(expanded.every(edge => edge >= 8.52 - .5), `放大悬停面被裁切:${JSON.stringify(expanded)}`);
    await card.evaluate(node => {
      node.removeAttribute('data-longhover');
      node.getBoundingClientRect();
      node.getAnimations().forEach(animation => animation.finish());
    });
  }
  await row.evaluate(node => { node.scrollLeft = node.scrollWidth - node.clientWidth });
  const right = await row.locator('[data-media-card]').last().evaluate(node =>
    node.closest('[data-related-row]')!.getBoundingClientRect().right - node.getBoundingClientRect().right);
  assert.ok(right >= 8 - .5, `末张卡的悬停面被裁切:${right}px`);
  if (longHover) {
    const last = row.locator('[data-media-card]').last();
    await last.hover();
    const expandedRight = await last.evaluate(node => {
      node.setAttribute('data-longhover', '');
      node.getBoundingClientRect();
      node.getAnimations().forEach(animation => animation.finish());
      return node.closest('[data-related-row]')!.getBoundingClientRect().right - node.getBoundingClientRect().right;
    });
    assert.ok(expandedRight >= 8.52 - .5, `末张放大卡片被裁切:${expandedRight}px`);
    await last.evaluate(node => {
      node.removeAttribute('data-longhover');
      node.getBoundingClientRect();
      node.getAnimations().forEach(animation => animation.finish());
    });
  }
}

describe('详情底部的队列与推荐', () => {
  let browser: Browser;
  before(async () => { browser = await launch() });
  after(async () => { await browser.close() });

  it('分卷队列使用详情实体头像', async () => {
    const opened = await openItemPage(browser, `/assets/${ITEM.part}?parts=${ITEM.part}`, VIEWPORTS[0]!);
    try {
      const images = opened.page.locator('#stage [data-queue-item="21"] .mav img');
      assert.equal(await images.count(), 2);
      assert.match(await images.first().getAttribute('src') || '', /entity-image\?kind=performer&id=1/);
      await opened.page.waitForFunction(() => [...document.querySelectorAll<HTMLImageElement>(
        '#stage [data-queue-item="21"] .mav img')].every(image => image.complete && image.naturalWidth > 0));
    } finally { await opened.close() }
  });

  for (const viewport of VIEWPORTS) for (const kind of ['local', 'follow'] as const) {
    it(`${viewport.name} ${kind}:接着看空结果显示完整说明`, { timeout: 60_000 }, async () => {
      const opened = kind === 'local'
        ? await openItemPage(browser, `/assets/${ITEM.plain}`, viewport)
        : await openFollowFeed(browser, `/posts/${DETAIL.collection}`, viewport,
          { ready: '#stage [data-follow-queue]', settings: { relatedLimit: 12 } });
      try {
        const page = opened.page;
        await page.route(kind === 'local' ? '**/api/related?*' : '**/api/follow?*', route => {
          if (kind === 'follow' && new URL(route.request().url()).searchParams.has('item')) return route.fallback();
          return route.fulfill({ json: kind === 'local' ? { items: [] }
            : { groups: [], sources: [], author_aliases: [], has_more: false } });
        });
        await page.reload({ waitUntil: 'load' });
        const empty = page.locator('#stage [data-item-related] [data-empty-state]');
        await empty.waitFor({ state: 'visible' });
        assert.equal(await empty.locator('h3').textContent(), '暂无接着看的内容');
        assert.equal(await empty.locator('p').textContent(), '可以回到列表挑选其他作品。');
        const box = await empty.boundingBox();
        assert.ok(box && box.width > 0 && box.x >= 0 && box.x + box.width <= viewport.width + 1);
        assert.equal(await page.locator('#stage [data-item-related] [aria-busy="true"]').count(), 0);
        await empty.scrollIntoViewIfNeeded();
        await page.locator('#stage').screenshot({ path: `../build/detail-hover-avatar/${kind}-${viewport.name}-empty.png` });
        assert.deepEqual(opened.problems.filter(problem => !problem.includes('VIDEOJS')), []);
      } finally { await opened.close() }
    });

    it(`${viewport.name} ${kind}:分集与推荐共用布局,悬停面完整`, { timeout: 60_000 }, async () => {
      const opened = kind === 'local'
        ? await openItemPage(browser, `/assets/${ITEM.part}?parts=${ITEM.part}`, viewport)
        : await openFollowFeed(browser, `/posts/${DETAIL.collection}`, viewport,
          { ready: '#stage [data-follow-queue]' });
      try {
        const page = opened.page;
        await page.locator('#stage [data-related-row] [data-media-card]').first().waitFor({ timeout: 15_000 });
        const boxes = await page.evaluate(() => {
          const queue = document.querySelector('#stage [data-mix-queue]')!;
          const related = document.querySelector('#stage [data-item-related]')!;
          const q = queue.getBoundingClientRect(), r = related.getBoundingClientRect();
          const queuePic = queue.querySelector('[data-mix-item-pic]')!.getBoundingClientRect();
          const relatedPic = related.querySelector('[data-media-pic]')!.getBoundingClientRect();
          return { q: { top: q.top, bottom: q.bottom, right: q.right }, r: { top: r.top, left: r.left },
            covers: { queue: { top: queuePic.top, height: queuePic.height }, related: { top: relatedPic.top, height: relatedPic.height } },
            sameSurface: getComputedStyle(queue).backgroundColor === getComputedStyle(related).backgroundColor };
        });
        if (viewport.mobile) assert.ok(boxes.r.top >= boxes.q.bottom - .5, JSON.stringify(boxes));
        else {
          assert.ok(Math.abs(boxes.q.top - boxes.r.top) <= .5 && boxes.q.right <= boxes.r.left + .5, JSON.stringify(boxes));
          assert.ok(boxes.sameSurface, '队列与推荐应使用同一底色');
          assert.ok(Math.abs(boxes.covers.queue.top - boxes.covers.related.top) <= .5,
            `封面上沿需对齐:${JSON.stringify(boxes.covers)}`);
          assert.ok(Math.abs(boxes.covers.queue.height - boxes.covers.related.height) <= .5,
            `封面高度需相同:${JSON.stringify(boxes.covers)}`);
          const bigCover = await page.locator('#stage [data-related-row] [data-media-pic]').first().evaluate(node => {
            const picture = node as HTMLElement;
            const ratio = picture.style.getPropertyValue('--card-ratio');
            picture.style.setProperty('--card-ratio', '.75');
            const height = picture.getBoundingClientRect().height;
            picture.style.setProperty('--card-ratio', ratio);
            return height;
          });
          assert.ok(Math.abs(bigCover - boxes.covers.queue.height) <= .5, `大图比例的推荐封面高度需相同:${bigCover}`);
        }
        await hoverClearance(page, kind === 'local' && !viewport.mobile);
        await page.locator('#stage [data-related-row]').evaluate(node => { node.scrollLeft = 0 });
        await page.locator('#stage [data-related-row] [data-media-card]').first().hover();
        await page.locator('#stage').screenshot({ path: `../build/detail-hover-avatar/${kind}-${viewport.name}.png` });
        assert.deepEqual(opened.problems.filter(problem => !problem.includes('VIDEOJS')), []);
      } finally { await opened.close() }
    });
  }
});