Ctrl K
ComunidadGuías…

frontend/e2e/skeleton-paint.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 { join } from 'node:path';
import type { Browser } from 'playwright-core';
import { launch, layout, requiredEnv, VIEWPORTS } from './harness.ts';

const routes = ['/', '/?browse=folders', '/jav', '/jav?tab=discover', '/jav?tab=wants', '/follow', '/performers', '/creators', '/studios', '/agencies', '/series', '/tags',
  '/performers/演示艺人', '/creators/演示创作者', '/studios/演示厂牌', '/series/演示系列', '/agencies/演示事务所', '/tags/演示标签',
  '/stats', '/taste', '/data-cleanup', '/tasks', '/duplicates', '/quality-goals', '/playlists', '/review', '/sources',
  '/settings/library', '/settings/subscriptions', '/settings/downloads', '/settings/network', '/settings/maintenance', '/settings/diagnostics',
  '/unseen', '/watch-later', '/flagged', '/junk-files', '/trash', '/assets/1', '/posts/1', '/works/EXM-001'];

describe('各页骨架的显示门槛与绘制边界', () => {
  let browser: Browser;
  before(async () => { browser = await launch() });
  after(async () => { await browser.close() });
  for (const viewport of VIEWPORTS) for (const theme of ['light', 'dark']) for (const [index, path] of routes.entries()) {
    it(`${viewport.name} ${theme} ${path} 隐藏时不泄露微光,显示时不越出视口`, async () => {
      const context = await browser.newContext({ viewport, reducedMotion: 'reduce' });
      let release = () => {};
      const held = new Promise<void>(resolve => { release = resolve });
      try {
        await context.addInitScript(theme => localStorage.setItem('peach.settings.v1', JSON.stringify({ theme, detailAutoplay: false })), theme);
        const page = await context.newPage();
        await page.route('**/api/**', async route => {
          const url = new URL(route.request().url());
          const overlay = /^\/(assets|posts|works)\//.test(path);
          const pending = overlay ? ['/api/item', '/api/follow', '/api/jav/detail'].includes(url.pathname)
            : !['/api/settings', '/api/entity/shapes'].includes(url.pathname);
          if (route.request().method() === 'GET' && pending) await held;
          await route.continue();
        });
        await page.goto(requiredEnv('PEACH_E2E_ORIGIN') + path, { waitUntil: 'load' });
        await page.locator('[data-skeleton]:visible,[data-skeleton-tier]:visible').first().waitFor();
        const leaks = await page.evaluate(() => {
          const roots = [...document.querySelectorAll('[data-skeleton],[data-skeleton-tier]')];
          const marked = roots.filter(node => !node.classList.contains('skeleton-awaiting'));
          marked.forEach(node => node.classList.add('skeleton-awaiting'));
          const leaks = roots.flatMap(root => [root, ...root.querySelectorAll('*')].flatMap(node =>
            ['', '::before', '::after'].filter(pseudo => {
              const css = getComputedStyle(node, pseudo || null);
              return (!pseudo || !['none', 'normal'].includes(css.content)) && css.visibility !== 'hidden';
            }).map(pseudo => `${node.tagName}.${node.className}${pseudo}`)));
          marked.forEach(node => node.classList.remove('skeleton-awaiting'));
          return leaks;
        });
        assert.deepEqual(leaks, []);
        const missing = await page.locator('[data-skeleton] svg use,[data-skeleton-tier] svg use,[data-skeleton-control] svg use').evaluateAll(nodes =>
          nodes.map(node => node.getAttribute('href') || '').filter(href => href.startsWith('#') && !document.getElementById(href.slice(1))));
        assert.deepEqual(missing, [], '骨架使用了未提供的图标');
        const scrollbars = await page.locator('[data-skeleton],[data-skeleton-tier]').evaluateAll(roots =>
          roots.flatMap(root => [root,...root.querySelectorAll('*')]).filter(node => {
            const style=getComputedStyle(node);
            return /^(auto|scroll)$/.test(style.overflowX) || /^(auto|scroll)$/.test(style.overflowY);
          }).filter(node => getComputedStyle(node).scrollbarWidth !== 'none')
            .map(node => `${node.tagName}.${node.className}`));
        assert.deepEqual(scrollbars, [], '骨架显示了原生滚动条');
        assert.deepEqual((await layout(page)).offenders, []);
        const evidence = process.env.PEACH_SKELETON_EVIDENCE;
        if (evidence) await page.screenshot({ path: join(evidence, `${viewport.name}-${theme}-${index}.png`) });
      } finally { release(); await context.close() }
    });
  }
});