macos-web/tests/cases/25-quicktime.test.ts
2026-07-23 15:16:57 +08:00

203 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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.SysQuickTime 通过 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<typeof makeMockWin>
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 已 mockonVol 不应报错
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)
})
})
})