Ctrl K
ComunidadGuías…

frontend/e2e/player-skin.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, requiredEnv, visit, VIEWPORTS } from './harness.ts';
import { mockSubtitleMemory } from './subtitle-fixture.ts';

describe('播放器皮肤', () => {
  let browser: Browser;
  before(async () => { browser = await launch() });
  after(async () => { await browser?.close() });

  it('首次加载模块期间保留海报,控制条从完整皮肤开始显示', { timeout: 60_000 }, async () => {
    let release!: () => void;
    let moduleRequested!: Promise<unknown>;
    const gate = new Promise<void>(resolve => { release = resolve });
    const opened = await visit(browser, `/assets/${requiredEnv('PEACH_E2E_ITEM')}`, VIEWPORTS[0]!, async page => {
      const modulePath = '**/vendor/videojs/*/video-player.js';
      moduleRequested = page.waitForRequest(modulePath);
      await page.route(modulePath, async route => { await gate; await route.continue() });
    });
    try {
      await moduleRequested;
      const video = opened.page.locator('#stage video.video-js');
      await video.waitFor();
      assert.equal(await video.evaluate((node: HTMLVideoElement) => node.controls), false, '模块加载期间不显示浏览器控制条');
      release();
      await opened.page.locator('#stage .vjs-peach-right-controls').waitFor();
      assert.equal(await opened.page.locator('#stage .vjs-control-bar').evaluate(bar => getComputedStyle(bar).visibility), 'visible');
      assert.equal(await opened.page.locator('#stage video.vjs-tech').evaluate((node: HTMLVideoElement) => node.controls), false);
      assert.deepEqual(opened.problems, []);
    } finally { release(); await opened.close() }
  });

  for (const viewport of VIEWPORTS) {
    it(`${viewport.name}:刷新后恢复同一视频的旋转、倍速和字幕外观`, { timeout: 60_000 }, async () => {
      const opened = await visit(browser, `/assets/${requiredEnv('PEACH_E2E_ITEM')}`, viewport, async page => {
        await mockSubtitleMemory(page);
        await page.addInitScript(() => localStorage.setItem('peach.settings.v1', JSON.stringify({ detailAutoplay: false })));
      });
      const { page } = opened;
      try {
        await page.locator('#stage .vjs-peach-settings-toggle').click();
        await page.locator('#stage [data-player-rotation]').click();
        await page.locator('#stage [data-player-rotation-option="90"]').click();
        await page.locator('#stage [data-player-speed]').click();
        await page.locator('#stage [data-player-speed-option="1.5"]').click();
        await page.locator('#stage [data-player-menu-back]').click();
        await page.locator('#stage [data-player-subtitles]').click();
        await page.locator('#stage [data-subtitle-options]').click();
        await page.locator('#stage [data-subtitle-style="size"]').click();
        const saved = page.waitForResponse(response => response.url().includes('/api/player/memory')
          && response.request().method() === 'PUT' && response.request().postDataJSON().caption_style?.size === '150');
        await page.locator('#stage [data-subtitle-value="150"]').click();
        assert.equal((await saved).status(), 200);
        await page.reload();
        await page.waitForFunction(() => {
          const video = document.querySelector<HTMLVideoElement>('#stage video.vjs-tech');
          return video?.style.transform.includes('rotate(90deg)') && video.playbackRate === 1.5
            && video.closest<HTMLElement>('.video-js')?.style.getPropertyValue('--peach-caption-scale') === '1.5';
        });
        await page.locator('#stage .vjs-peach-settings-toggle').click();
        assert.equal(await page.locator('#stage [data-player-rotation] b').textContent(), '90°');
        assert.equal(await page.locator('#stage [data-player-speed] b').textContent(), '1.5×');
        assert.deepEqual(opened.problems, []);
      } finally { await opened.close() }
    });

    it(`${viewport.name}:旋转画面适配播放器并恢复原始方向`, { timeout: 60_000 }, async () => {
      const opened = await visit(browser, `/assets/${requiredEnv('PEACH_E2E_ITEM')}`, viewport, async page => {
        await page.addInitScript(() => localStorage.setItem('peach.settings.v1', JSON.stringify({ detailAutoplay: false })));
      });
      const { page } = opened;
      try {
        await page.locator('#stage .vjs-peach-settings-toggle').click();
        const rotationIcon = await page.locator('#stage [data-player-rotation]>svg').first().evaluate(icon => ({
          fill: getComputedStyle(icon).fill, stroke: getComputedStyle(icon).stroke,
        }));
        assert.equal(rotationIcon.fill, 'none');
        assert.notEqual(rotationIcon.stroke, 'none');
        for (const angle of [90, 180, 270, 0]) {
          await page.locator('#stage [data-player-rotation]').click();
          await page.locator(`#stage [data-player-rotation-option="${angle}"]`).click();
          const state = await page.locator('#stage video.vjs-tech').evaluate(video => {
            const bounds = video.getBoundingClientRect();
            const container = video.closest('media-container')!.getBoundingClientRect();
            return { transform: (video as HTMLElement).style.transform,
              fit: getComputedStyle(video).objectFit, width: bounds.width, height: bounds.height,
              containerWidth: container.width, containerHeight: container.height };
          });
          assert.equal(state.transform.includes(`rotate(${angle}deg)`), angle !== 0);
          assert.ok(Math.abs(state.width - state.containerWidth) < 1);
          assert.ok(Math.abs(state.height - state.containerHeight) < 1);
          if (angle) assert.equal(state.fit, 'contain');
        }
        assert.deepEqual(opened.problems, []);
      } finally { await opened.close() }
    });

    it(`${viewport.name}:控制组贴右、进度圆点唯一、全屏保留控件`, { timeout: 60_000 }, async () => {
      const opened = await visit(browser, `/assets/${requiredEnv('PEACH_E2E_ITEM')}`, viewport, async page => {
        await page.addInitScript(theme => localStorage.setItem('peach.settings.v1',
          JSON.stringify({ detailAutoplay: false, theme })), viewport.mobile ? 'dark' : 'light');
      });
      const { page, problems } = opened;
      try {
        await page.locator('#stage .vjs-peach-bezel').waitFor({ state: 'attached' });
        await page.screenshot({ path: join(process.cwd(), `../build/player-skin-${viewport.name}.png`) });
        const geometry = await page.locator('#stage .video-js').evaluate(root => {
          const bar = root.querySelector('.vjs-control-bar')!.getBoundingClientRect();
          const group = root.querySelector('.vjs-peach-right-controls')!.getBoundingClientRect();
          const slider = root.querySelector('.vjs-progress-holder')!;
          const thumb = slider.querySelector('media-slider-thumb')!;
          return { rightGap: bar.right - group.right, thumbOpacity: getComputedStyle(thumb).opacity,
            progressDot: getComputedStyle(slider.querySelector('.vjs-play-progress')!, '::before').content };
        });
        assert.ok(Math.abs(geometry.rightGap) < 1, `右侧控制组离右边缘 ${geometry.rightGap}px`);
        assert.equal(geometry.thumbOpacity, '1', '进度圆点常驻');
        assert.equal(geometry.progressDot, 'none', '进度仅由一枚滑块显示圆点');
        if (!viewport.mobile) {
          await page.locator('#stage .vjs-volume-panel').hover();
          const volume = (await page.locator('#stage .vjs-volume-panel').boundingBox())!;
          assert.equal(volume.width, 111, '音量胶囊展开为 111px');
          const right = (await page.locator('#stage .vjs-peach-right-controls').boundingBox())!;
          const bar = (await page.locator('#stage .vjs-control-bar').boundingBox())!;
          assert.ok(Math.abs(right.x + right.width - bar.x - bar.width) < 1);
        }
        // 演示库的进度预览使用合成图片,不依赖后台抽帧完成时间。
        await page.route('**/poster?**', route => route.fulfill({ contentType: 'image/svg+xml',
          body: '<svg xmlns="http://www.w3.org/2000/svg" width="480" height="270"><rect width="480" height="270" fill="#888"/></svg>' }));
        await page.waitForFunction(() => document.querySelector<HTMLVideoElement>('#stage video.vjs-tech')!.readyState >= 2);
        const progress = (await page.locator('#stage .vjs-progress-control').boundingBox())!;
        await page.mouse.click(progress.x + progress.width / 2, progress.y + 3);
        await page.waitForFunction(() => document.querySelector<HTMLVideoElement>('#stage video.vjs-tech')!.currentTime > .5);
        const dot = await page.locator('#stage .vjs-progress-thumb').evaluate(thumb => {
          const box = thumb.getBoundingClientRect();
          return { width: box.width, height: box.height, color: getComputedStyle(thumb).backgroundColor,
            fill: getComputedStyle(thumb.parentElement!.querySelector('.vjs-play-progress')!).backgroundColor };
        });
        assert.ok(Math.abs(dot.width - dot.height) < .1, '进度圆点保持正圆');
        assert.equal(dot.color, dot.fill, '进度圆点与已播段使用同一颜色');
        if (viewport.mobile) await page.locator('#stage [data-player-expand]').click();
        await page.locator('#stage media-fullscreen-button').click();
        await page.waitForFunction(() => !!document.fullscreenElement);
        const fullscreen = await page.locator('#stage media-container').evaluate(container => {
          const frame = container.getBoundingClientRect();
          const play = container.querySelector('media-play-button')!.getBoundingClientRect();
          const full = container.querySelector('media-fullscreen-button')!.getBoundingClientRect();
          return { frame: [frame.width, frame.height], viewport: [innerWidth, innerHeight],
            fit: getComputedStyle(container.querySelector('video')!).objectFit,
            controlsInside: play.y >= 0 && full.right <= innerWidth && play.bottom <= innerHeight };
        });
        assert.deepEqual(fullscreen.frame, fullscreen.viewport);
        assert.equal(fullscreen.controlsInside, true);
        assert.equal(fullscreen.fit, 'contain', '全屏保留完整视频画面');
        await page.locator('#stage media-fullscreen-button').click();
        await page.waitForFunction(() => !document.fullscreenElement);
        assert.deepEqual(problems, []);
      } finally { await opened.close() }
    });
  }
});