Ctrl K
ComunidadGuías…

frontend/e2e/sidebar-motion.test.ts

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

Archivo original ↗Preguntar a KiBot
/* 侧栏开合的运行期约束:过渡中不重画玻璃贴图,停下后按终态尺寸补画,主区落在让出的位置上。
 *
 * 冒烟默认按减少动态效果打开页面,那时过渡是 0s,这些约束无从观察;这里在页面里切回有动效。 */
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';

import type { Browser, Page } from 'playwright-core';

import { expectBody, launch, settle, visit, VIEWPORTS } from './harness.ts';

const DESKTOP = VIEWPORTS.find((viewport) => !viewport.mobile)!;

interface Toggle {
  /** 第一段过渡结束那一刻,已经画过几张位移贴图。 */
  mapsDuring: number;
  /** 两段过渡都结束、补画之后,抽屉那张位移贴图的宽度(同尺寸的贴图会复用,不一定重画)。 */
  mapWidth: number;
  drawerClientWidth: number;
  opticDuring: string;
  opticAfter: string;
  open: boolean;
  drawerWidth: number;
  drawerRight: number;
  mainLeft: number;
  mainRight: number;
  railW: number;
  viewport: number;
  /** 点击后 300ms(过渡的时长)里出了几帧,首帧算点击那一刻。 */
  frames: number;
}

/** 点一下左上角的切换键,等 `body` 的 `padding-left` 与抽屉的 `width` 两段过渡都停下。 */
async function toggle(page: Page): Promise<Toggle> {
  return page.evaluate(() => new Promise<Toggle>((resolve, reject) => {
    const counter = window as unknown as { __maps: number };
    const drawer = document.querySelector<HTMLElement>('#drawer')!;
    const start = counter.__maps;
    const pending = new Set(['padding-left', 'width']);
    let mapsDuring = -1;
    let opticDuring = '';
    const timer = setTimeout(() => reject(new Error(`过渡没有结束,还差 ${[...pending].join('、')}`)), 5_000);
    /* 帧数从点击那一刻数到 300ms:主线程被一次整页重排占住多久,这段里就少出多少帧。 */
    let clicked = 0;
    let frames = 1;
    let counting = true;
    const tick = (now: number) => {
      if (!counting) return;
      if (now - clicked <= 300) frames++;
      requestAnimationFrame(tick);
    };
    const window300 = new Promise<void>((done) => setTimeout(done, 320));
    const onEnd = (event: TransitionEvent) => {
      const key = event.target === document.body && event.propertyName === 'padding-left' ? 'padding-left'
        : event.target === drawer && event.propertyName === 'width' ? 'width' : '';
      if (!key || !pending.delete(key)) return;
      if (mapsDuring < 0) {
        mapsDuring = counter.__maps - start;
        opticDuring = drawer.style.getPropertyValue('--glass-optic');
      }
      if (pending.size) return;
      clearTimeout(timer);
      window.removeEventListener('transitionend', onEnd, true);
      void window300.then(() => requestAnimationFrame(() => requestAnimationFrame(() => {
        counting = false;
        const drawerBox = drawer.getBoundingClientRect();
        const mainBox = document.querySelector('#main')!.getBoundingClientRect();
        resolve({
          mapsDuring,
          mapWidth: Number(document.getElementById(/#(peach-optic-\d+)/.exec(drawer.style.getPropertyValue('--glass-optic'))?.[1] || '')
            ?.querySelector('feImage')?.getAttribute('width')),
          drawerClientWidth: Math.round(drawer.clientWidth),
          opticDuring,
          opticAfter: drawer.style.getPropertyValue('--glass-optic'),
          open: drawer.classList.contains('open'),
          drawerWidth: Math.round(drawerBox.width),
          drawerRight: drawerBox.right,
          mainLeft: mainBox.left,
          mainRight: mainBox.right,
          railW: parseFloat(getComputedStyle(document.body).getPropertyValue('--railW')),
          viewport: document.documentElement.clientWidth,
          frames,
        });
      })));
    };
    // 挂在 window 的捕获阶段:比页面挂在 document 上的补画先拿到这一次结束。
    window.addEventListener('transitionend', onEnd, true);
    clicked = performance.now();
    requestAnimationFrame(tick);
    document.querySelector<HTMLElement>('#filterBtn')!.click();
  }));
}

describe('侧栏开合', () => {
  let browser: Browser;

  before(async () => {
    browser = await launch();
  });

  after(async () => {
    await browser.close();
  });

  it('过渡中玻璃退成纯模糊不重画贴图,停下后按终态补画,主区左缘落在让出的位置', { timeout: 60_000 }, async () => {
    const opened = await visit(browser, '/', DESKTOP);
    try {
      const { page } = opened;
      await expectBody(page, '/', [page.locator('#drawer.open')]);
      await settle(page);
      await page.waitForFunction(() => document.querySelector<HTMLElement>('#drawer')?.dataset.opticGlass === 'true');
      await page.emulateMedia({ reducedMotion: 'no-preference' });
      await page.evaluate(() => {
        const counter = window as unknown as { __maps: number };
        counter.__maps = 0;
        const native = HTMLCanvasElement.prototype.toDataURL;
        HTMLCanvasElement.prototype.toDataURL = function (this: HTMLCanvasElement, ...args: Parameters<HTMLCanvasElement['toDataURL']>) {
          counter.__maps++;
          return native.apply(this, args);
        };
      });

      const collapsed = await toggle(page);
      const expanded = await toggle(page);
      for (const [name, state] of [['收起', collapsed], ['展开', expanded]] as const) {
        assert.equal(state.mapsDuring, 0, `${name}过渡中重画了 ${state.mapsDuring} 张玻璃贴图`);
        assert.equal(state.opticDuring, 'blur(14px)', `${name}过渡中抽屉仍挂着旧尺寸的位移贴图`);
        assert.equal(state.mapWidth, state.drawerClientWidth, `${name}停下后贴图宽 ${state.mapWidth},没有按终态尺寸 ${state.drawerClientWidth} 补上`);
        assert.match(state.opticAfter, /url\("#peach-optic-\d+"\)/, `${name}停下后抽屉没有恢复位移滤镜`);
        assert.ok(Math.abs(state.mainLeft - state.railW) <= .5, `${name}后主区左缘 ${state.mainLeft},应落在 --railW ${state.railW}`);
        assert.ok(Math.abs(state.mainRight - state.viewport) <= 1, `${name}后主区右缘 ${state.mainRight} 没有贴到视口右缘 ${state.viewport}`);
        assert.ok(state.drawerRight <= state.mainLeft, `${name}后抽屉右缘 ${state.drawerRight} 压进了主区`);
        /* 下限只拦「整段过渡被一次长任务吞掉、直接跳到终态」:除了点击那一帧,300ms 里至少
           还要出一帧中间帧。本机无头 Chrome 收起 10~15 帧、展开 30~38 帧;CPU 降速 6 倍后
           收起 2 帧、展开 4 帧,CI 的慢机落在这两档之间。 */
        assert.ok(state.frames >= 2, `${name}过渡的 300ms 里只出了 ${state.frames} 帧,主线程整段被占住`);
      }
      assert.equal(collapsed.open, false);
      assert.equal(expanded.open, true);
      assert.ok(collapsed.drawerWidth < expanded.drawerWidth, `收起 ${collapsed.drawerWidth}px 不比展开 ${expanded.drawerWidth}px 窄`);
      assert.ok(collapsed.railW < expanded.railW, '收起后主区没有往左让回来');
      assert.deepEqual(opened.problems, []);
    } finally {
      await opened.close();
    }
  });
});