/** * 25-quicktime — QuickTime 播放器 深层测试 * * 使用 vitest 原汁原味的 vi.mock() + window mock 模式。 * jsdom 不支持 video.play()/pause() API,通过 mock 避免 unhandled errors。 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import { nextTick } from 'vue' import QuickTimeComponent from '../../src/apps/quicktime/QuickTime.vue' import { setupDOM } from '../helpers' // ============================================================ // Mock 依赖模块 // ============================================================ vi.mock('../../src/composables/useBus', () => ({ bus: { on: vi.fn(), off: vi.fn(), emit: vi.fn() }, })) vi.mock('../../src/composables/useWM', () => ({ wm: { setTitle: vi.fn() }, })) // ============================================================ // Mock window.Sys(QuickTime 通过 const Sys = () => (window as any).Sys 访问) // ============================================================ function setupSysMock() { (window as any).Sys = { settings: { volume: 0.6, muted: false }, save: vi.fn(), applyVolume: vi.fn(), registerMedia: vi.fn(), unregisterMedia: vi.fn(), } } // ============================================================ // Mock HTMLVideoElement 方法(jsdom 不支持) // ============================================================ function setupVideoMock() { // 始终覆盖 play/pause/load,无论之前是否已设置 const mockPlay = vi.fn(() => Promise.resolve()) const mockPause = vi.fn() const mockLoad = vi.fn() Object.defineProperty(HTMLMediaElement.prototype, 'play', { value: mockPlay, configurable: true, writable: true, }) Object.defineProperty(HTMLMediaElement.prototype, 'pause', { value: mockPause, configurable: true, writable: true, }) Object.defineProperty(HTMLMediaElement.prototype, 'load', { value: mockLoad, configurable: true, writable: true, }) } // ============================================================ // 辅助函数 // ============================================================ function makeMockWin() { const el = document.createElement('div') el.className = 'window' document.getElementById('window-layer')?.appendChild(el) const body = document.createElement('div') body.className = 'win-body' el.appendChild(body) return { id: 'qt', appId: 'quicktime', el, body, timers: [] as any[], data: {} } } describe('25-quicktime — QuickTime 播放器', () => { let win: ReturnType beforeAll(() => { setupVideoMock() }) beforeEach(() => { vi.clearAllMocks() setupDOM() setupSysMock() win = makeMockWin() }) afterEach(() => { win.el.remove() }) function mountComponent() { return mount(QuickTimeComponent, { props: { win }, attachTo: win.body, }) } function getAppState() { return win.appState } // ==================== 结构 ==================== describe('结构', () => { it('播放器完整结构', () => { mountComponent() expect(win.body.querySelector('.qt-stage')).toBeTruthy() expect(win.body.querySelector('.qt-controls')).toBeTruthy() }) it('3 个控制按钮', () => { mountComponent() expect(win.body.querySelectorAll('.qt-btn')).toHaveLength(3) }) it('进度条和音量存在', () => { mountComponent() expect(win.body.querySelector('.qt-seek')).toBeTruthy() expect(win.body.querySelector('.qt-vol')).toBeTruthy() }) it('时间显示初始值', () => { mountComponent() expect(win.body.querySelectorAll('.music-time')).toHaveLength(2) }) }) // ==================== 导航 ==================== describe('导航', () => { it('nav(1) 切换不抛错', async () => { mountComponent() expect(() => getAppState().nav(1)).not.toThrow() await nextTick() expect(win.body.querySelector('.qt-stage')).toBeTruthy() }) it('nav(-1) 循环不抛错', async () => { mountComponent() expect(() => getAppState().nav(-1)).not.toThrow() await nextTick() expect(win.body.querySelector('.qt-stage')).toBeTruthy() }) it('连续 nav 不抛错', async () => { mountComponent() for (let i = 0; i < 3; i++) { expect(() => getAppState().nav(1)).not.toThrow() await nextTick() } expect(win.body.querySelector('.qt-stage')).toBeTruthy() }) }) // ==================== 控制 ==================== describe('控制', () => { it('toggle 存在且可调用', () => { mountComponent() const appState = getAppState() expect(typeof appState.toggle).toBe('function') // video.play() 已 mock 为返回 Promise.resolve() expect(() => appState.toggle()).not.toThrow() }) it('播放按钮点击不抛错', () => { mountComponent() const btn = win.body.querySelector('.qt-btn') as HTMLElement expect(() => btn.dispatchEvent(new MouseEvent('click', { bubbles: true }))).not.toThrow() }) it('进度条拖拽', () => { mountComponent() const seek = win.body.querySelector('.qt-seek') as HTMLInputElement seek.value = '50' seek.dispatchEvent(new Event('input', { bubbles: true })) expect(seek.value).toBe('50') }) it('音量拖拽不抛错', () => { mountComponent() const vol = win.body.querySelector('.qt-vol') as HTMLInputElement vol.value = '30' // window.Sys 已 mock,onVol 不应报错 expect(() => vol.dispatchEvent(new Event('input', { bubbles: true }))).not.toThrow() expect(vol.value).toBe('30') }) }) // ==================== appState ==================== describe('appState', () => { it('暴露 toggle/nav', () => { mountComponent() const appState = getAppState() expect(typeof appState.toggle).toBe('function') expect(typeof appState.nav).toBe('function') }) }) // ==================== 卸载 ==================== describe('卸载', () => { it('卸载后 DOM 清理干净', () => { const wrapper = mountComponent() wrapper.unmount() expect(win.body.children).toHaveLength(0) }) }) })