frontend/test/player-v10.test.ts
Rama master · réplica de lectura del repositorio original.
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { createV10Player } from '../src/player/v10-player';
const engine = () => ({ bandwidthEstimate: 8000000, currentLevel: -1,
levels: [{ width: 1280, height: 720 }, { width: 1920, height: 1080 }], on: vi.fn() });
class TestMedia extends HTMLElement {
source: unknown = null;
engine = engine();
adapter = { destroy: vi.fn() };
}
beforeAll(() => {
if (!customElements.get('hlsjs-video')) customElements.define('hlsjs-video', TestMedia);
if (!customElements.get('dash-video')) customElements.define('dash-video', class extends TestMedia {});
});
afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); vi.unstubAllGlobals() });
function mounted() {
const video = document.createElement('video'); document.body.append(video);
const player = createV10Player(video, { controls: false });
const media = player.el().querySelector('hlsjs-video') as TestMedia;
return { video, player, media };
}
describe('Video.js 10 播放会话', () => {
it('普通 HTTP 的随机源为无 ID 的视频创建独立播放器', () => {
vi.stubGlobal('crypto', { getRandomValues: crypto.getRandomValues.bind(crypto) });
const first = mounted().player;
const second = mounted().player;
try {
expect(first.el().id).toMatch(/^peach-player-/);
expect(first.el().id).not.toBe(second.el().id);
} finally { first.dispose(); second.dispose() }
});
it('片源交给异步 adapter 后立即播放,原生 play 使用已挂载的片源', async () => {
const { video, player, media } = mounted();
Object.defineProperty(media, 'source', { set: (value: { src: string }) => {
queueMicrotask(() => { video.src = value.src });
} });
const play = vi.spyOn(video, 'play').mockImplementation(() => video.getAttribute('src')
? Promise.resolve() : Promise.reject(new DOMException('没有可播放的片源', 'NotSupportedError')));
player.src({ src: '/example.mp4', type: 'video/mp4' });
await expect(player.play()).resolves.toBeUndefined();
expect(play).toHaveBeenCalledOnce();
expect(video.src).toContain('/example.mp4');
player.dispose();
});
it.each(['pause', 'source', 'dispose'] as const)('排队播放期间 %s 会取消该请求', async action => {
const { video, player } = mounted();
const play = vi.spyOn(video, 'play').mockResolvedValue(undefined);
const pending = player.play();
if (action === 'pause') player.pause();
else if (action === 'source') player.src({ src: '/next.mp4', type: 'video/mp4' });
else player.dispose();
await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
expect(play).not.toHaveBeenCalled();
player.dispose();
});
it('媒体错误可补充来源状态文案,展示错误不会再次触发错误处理', () => {
const { player, media } = mounted();
const callback = vi.fn(() => player.error({ code: 2, message: '媒体目录无法访问' }));
player.on('error', callback);
media.dispatchEvent(new Event('error'));
expect(callback).toHaveBeenCalledOnce();
expect(player.error()).toEqual({ code: 2, message: '媒体目录无法访问' });
player.dispose();
});
it('固定清晰度和自动码率使用同一条 HLS 引擎,带宽读取实际估计', async () => {
const { player, media } = mounted();
player.src({ src: '/example.m3u8', type: 'application/vnd.apple.mpegurl' });
const levels = player.qualityLevels!();
expect(levels.length).toBe(2);
videoMetadata(player.el()); videoMetadata(player.el());
expect(media.engine.on).toHaveBeenCalledTimes(2);
levels[0]!.enabled = false; levels[1]!.enabled = true;
await Promise.resolve(); expect(media.engine.currentLevel).toBe(1);
levels[0]!.enabled = true; levels[1]!.enabled = true;
await Promise.resolve(); expect(media.engine.currentLevel).toBe(-1);
expect(player.bandwidth!()).toBe(8000000);
player.dispose();
});
it('清晰度选择排队后切换片源或销毁,不再写入旧引擎', async () => {
const { player, media } = mounted();
player.src({ src: '/first.m3u8', type: 'application/vnd.apple.mpegurl' });
const levels = player.qualityLevels!();
levels[0]!.enabled = false; levels[1]!.enabled = true;
player.src({ src: '/second.m3u8', type: 'application/vnd.apple.mpegurl' });
await Promise.resolve(); expect(media.engine.currentLevel).toBe(-1);
levels[0]!.enabled = false; levels[1]!.enabled = true;
player.dispose();
await Promise.resolve(); expect(media.engine.currentLevel).toBe(-1);
});
it('HLS 与 DASH 切源保留原生视频和字幕,销毁停播并解除事件', () => {
const { video, player, media } = mounted();
const pause = vi.spyOn(video, 'pause').mockImplementation(() => {});
const load = vi.spyOn(video, 'load').mockImplementation(() => {});
const played = vi.fn(); player.on('play', played);
video.dispatchEvent(new Event('play')); expect(played).toHaveBeenCalledOnce();
const track = document.createElement('track'); track.label = '中文'; video.append(track);
player.src({ src: '/example.mpd', type: 'application/dash+xml' });
expect(media.adapter.destroy).toHaveBeenCalledOnce();
expect(player.el().querySelector('dash-video > video')).toBe(video);
expect(video.querySelector('track')).toBe(track);
player.dispose(); player.dispose();
expect(pause).toHaveBeenCalledOnce(); expect(load).toHaveBeenCalledOnce();
video.dispatchEvent(new Event('play')); expect(played).toHaveBeenCalledOnce();
expect(document.querySelector('.video-js')).toBeNull();
});
});
function videoMetadata(root: HTMLElement) { root.querySelector('video')!.dispatchEvent(new Event('loadedmetadata')) }