203 lines
6.1 KiB
TypeScript
203 lines
6.1 KiB
TypeScript
/**
|
||
* 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<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 已 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)
|
||
})
|
||
})
|
||
})
|