Ctrl K
ComunidadGuías…

frontend/e2e/ui-feedback.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 } from 'playwright-core';
import { launch, layout, settle, VIEWPORTS, visit } from './harness.ts';

/* 每个分区取一块不随本机配置出没的卡:骨架按终态结构画,随配置变的卡(下载渠道的「云下载」
   跟着媒体库里有没有网盘)照本机上次记下的结构画,新开的浏览器里没有记录,按最小结构画。 */
const sections = [
  ['library', '媒体库与入库', '媒体库与目录'], ['subscriptions', '来源与订阅', '作品资料站点'],
  ['downloads', '下载渠道', '浏览器添加 JAV'], ['network', '网络与访问', 'Peach 代理'],
  ['maintenance', '应用与维护', '开机自启'], ['diagnostics', '系统诊断', '库健康'],
] as const;

describe('等待反馈与文件夹模式', () => {
  let browser: Browser;
  before(async () => { browser = await launch() });
  after(async () => { await browser?.close() });
  for (const viewport of VIEWPORTS) {
    for (const [key, name, title] of sections) it(`${viewport.name} ${name} 的首屏骨架保持分区与边界`, async () => {
      let release = () => {};
      const held = new Promise<void>(resolve => { release = resolve });
      const opened = await visit(browser, `/settings/${key}`, viewport, async page => {
        await page.route('**/api/configuration', async route => { await held; await route.continue() });
      });
      try {
        const { page } = opened;
        const root = page.locator(`[data-skeleton="board/settings/${key}"]`);
        await root.waitFor();
        assert.equal((await root.locator('.ui-board-local-nav [aria-selected="true"]').innerText()).trim(), name);
        assert.ok(await root.locator(`[aria-label="${title}"]`).count());
        if (key !== 'library') assert.equal(await root.locator('[aria-label="媒体库与目录"]').count(), 0);
        if (key === 'library') assert.equal(await root.locator('[data-folder-row]').count(), 1);
        const geometry = await layout(page);
        assert.ok(geometry.scrollWidth <= geometry.viewportWidth, JSON.stringify(geometry));
        assert.deepEqual(geometry.offenders, []);
        release();
        await root.waitFor({ state: 'detached' });
        await settle(page);
        assert.deepEqual(opened.problems, []);
      } finally { release(); await opened.close() }
    });

    it(`${viewport.name} 文件夹等待态只在各自占位内显示微光`, async () => {
      let release = () => {};
      const held = new Promise<void>(resolve => { release = resolve });
      const opened = await visit(browser, '/?browse=folders', viewport, async page => {
        await page.route('**/api/folders?*', async route => { await held; await route.continue() });
      });
      try {
        const { page } = opened;
        await page.locator('#catalogFolderHeader [data-skeleton]').waitFor();
        if (!viewport.mobile) {
          const tree = page.locator('#catalogFoldersTree [aria-label="正在读取目录树"]');
          await tree.waitFor();
          assert.equal(await tree.locator(':scope > div').count(), 8);
          const bounds = await tree.boundingBox();
          assert.ok(bounds && bounds.width <= 224 && bounds.height < 350);
        }
        const spans = await page.locator('#catalogFolderHeader .skeleton-sheen').evaluateAll(nodes => nodes.map(node => {
          const css = getComputedStyle(node), pseudo = getComputedStyle(node, '::after');
          return { position: css.position, overflow: css.overflow, pseudo: pseudo.position, width: node.getBoundingClientRect().width };
        }));
        assert.equal(spans.length, 6);
        assert.ok(spans.every(row => row.position === 'relative' && row.overflow === 'hidden' && row.pseudo === 'absolute' && row.width > 0));
        release();
        await settle(page);
        assert.deepEqual((await layout(page)).offenders, []);
        assert.deepEqual(opened.problems, []);
      } finally { release(); await opened.close() }
    });

    it(`${viewport.name} 多个子文件夹可以展开和收起`, async () => {
      const opened = await visit(browser, '/?browse=folders', viewport, async page => {
        await page.route('**/api/folders?*', route => route.fulfill({ json: {
          roots: [], selected: null, ancestors: [], matches: [], conflicts: [], rule: null, rules: [], rules_revision: 1,
          children: Array.from({ length: 40 }, (_, i) => ({ id: `folder-${i}`, name: `目录 ${i}`, parent: null,
            path: `R:\\demo\\${i}`, location: 'local', root: 'root', has_children: false, files: 1, videos: 1 })),
        } }));
      });
      try {
        const { page } = opened;
        const group = page.locator('#catalogFolderHeader [aria-label="子文件夹"]');
        await group.waitFor();
        assert.equal(await group.getByRole('button').count(), 12);
        await page.getByRole('button', { name: '展开全部 40 个文件夹' }).click();
        assert.equal(await group.getByRole('button').count(), 40);
        const collapse = page.getByRole('button', { name: '收起文件夹' });
        assert.equal(await collapse.getAttribute('aria-expanded'), 'true');
        await collapse.click();
        assert.equal(await group.getByRole('button').count(), 12);
        await settle(page);
        assert.deepEqual((await layout(page)).offenders, []);
        assert.deepEqual(opened.problems, []);
      } finally { await opened.close() }
    });

    it(`${viewport.name} 文件夹滚动条内缩且各滚动位置均在面板内`, async () => {
      const opened = await visit(browser, '/?browse=folders', viewport, async page => {
        const roots = Array.from({ length: 50 }, (_, i) => ({ id: `root-${i}`, name: `媒体目录 ${i}`,
          parent: null, path: `R:\\demo\\${i}`, location: 'local', root: `root-${i}`,
          has_children: false, files: 1, videos: 1 }));
        await page.route('**/api/folders?*', route => route.fulfill({ json: {
          roots, children: [], selected: null, ancestors: [], matches: [], conflicts: [], rule: null, rules: [], rules_revision: 1,
        } }));
      });
      try {
        const { page } = opened;
        if (viewport.mobile) await page.getByRole('button', { name: '打开文件夹目录' }).click();
        const scroller = page.locator('[aria-label="文件夹导航"]:visible .folder-tree-scroller');
        await scroller.waitFor();
        await scroller.locator('..').locator('.ovtrack.ov-y').waitFor();
        for (const height of [160, 32]) {
          await scroller.evaluate((node, value) => { node.style.maxHeight = `${value}px` }, height);
          for (const fraction of [0, .5, 1]) {
            await scroller.evaluate((node, value) => { node.scrollTop = (node.scrollHeight - node.clientHeight) * value }, fraction);
            await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
            const bounds = await scroller.evaluate(node => {
              const box = node.getBoundingClientRect();
              const thumb = node.parentElement!.querySelector('.ovtrack.ov-y .ovthumb')!.getBoundingClientRect();
              return { right: box.right - thumb.right, top: thumb.top - box.top, bottom: box.bottom - thumb.bottom };
            });
            assert.ok(bounds.right >= 7, `滚动条贴近右框:${JSON.stringify(bounds)}`);
            assert.ok(bounds.top >= 7 && bounds.bottom >= 7, `滚动条越出上下内边距:${JSON.stringify(bounds)}`);
          }
        }
        assert.deepEqual(opened.problems, []);
      } finally { await opened.close() }
    });

    it(`${viewport.name} 资料页文件夹筛选保留人物范围`, async () => {
      let filtered = false;
      const opened = await visit(browser, '/performers/演示艺人?browse=folders', viewport, async page => {
        await page.route('**/api/entity?*', route => route.fulfill({ json: {
          id: 90001, kind: 'performer', canonical_name: '演示艺人', asset_count: 1, tags: [], links: [],
          related_performers: [], aliases: [], metadata: {}, entry_links: [], has_image: false,
        } }));
        await page.route('**/api/folders?*', route => route.fulfill({ json: {
          roots: [], selected: null, ancestors: [], matches: [], conflicts: [], rule: null, rules: [], rules_revision: 1,
          children: [{ id: 'folder-1', name: '示例文件夹', parent: null, path: 'R:\\demo', location: 'local', root: 'root', has_children: false, files: 1, videos: 1 }],
        } }));
        await page.route(/\/api\/items\?/, async route => {
          const query = new URL(route.request().url()).searchParams;
          if (query.has('folder')) filtered = query.get('folder') === 'folder-1' && query.get('performer') === '演示艺人';
          await route.fulfill({ json: { total: 1, items: [{ id: 1, name: '示例作品', medium: 'video', location: 'local', has_thumb: false }], has_more: false } });
        });
      });
      try {
        const { page } = opened;
        const toggle = page.getByRole('button', { name: '文件夹模式', exact: true });
        await toggle.waitFor();
        assert.equal(await toggle.getAttribute('aria-pressed'), 'true');
        await page.locator('[data-folder-header-host]').getByRole('button', { name: '示例文件夹' }).click();
        await page.waitForFunction(() => new URLSearchParams(location.search).get('folder') === 'folder-1');
        await settle(page);
        assert.equal(filtered, true);
        await toggle.click();
        await settle(page);
        assert.equal(await page.locator('[data-folder-workspace]').getAttribute('data-folder-browse'), null);
        assert.deepEqual((await layout(page)).offenders, []);
        assert.deepEqual(opened.problems, []);
      } finally { await opened.close() }
    });
  }
});