test(app): add unit tests for useVisualViewport composable

Tests keyboard detection, viewport height tracking, debounce behavior.
Includes withSetup test helper for composables with lifecycle hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 22:22:02 +00:00
co-authored by Claude Opus 4.6
parent 1a9b769e45
commit c6e384a288
2 changed files with 111 additions and 0 deletions
@@ -0,0 +1,18 @@
import { createApp, defineComponent } from 'vue'
/**
* Helper to test composables that use lifecycle hooks (onMounted, etc.)
* Creates a temporary Vue app, mounts it, runs the composable, and returns the result.
*/
export function withSetup<T>(composable: () => T): [T, ReturnType<typeof createApp>] {
let result!: T
const app = createApp(defineComponent({
setup() {
result = composable()
return () => null
},
}))
const div = document.createElement('div')
app.mount(div)
return [result, app]
}
@@ -0,0 +1,93 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { withSetup } from './testUtils'
import { useVisualViewport } from '../useVisualViewport'
// Helper to create a mock VisualViewport
function createMockViewport(height = 800) {
const listeners: Record<string, Set<EventListener>> = {}
return {
height,
width: 390,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn((event: string, handler: EventListener) => {
if (!listeners[event]) listeners[event] = new Set()
listeners[event].add(handler)
}),
removeEventListener: vi.fn((event: string, handler: EventListener) => {
listeners[event]?.delete(handler)
}),
dispatchEvent: vi.fn(),
onresize: null,
onscroll: null,
_listeners: listeners,
_setHeight(h: number) {
this.height = h
// Fire resize listeners
listeners['resize']?.forEach(fn => fn(new Event('resize')))
},
}
}
describe('useVisualViewport', () => {
let mockVV: ReturnType<typeof createMockViewport>
beforeEach(() => {
vi.useFakeTimers()
mockVV = createMockViewport(800)
Object.defineProperty(window, 'visualViewport', { value: mockVV, configurable: true, writable: true })
Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true, writable: true })
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
it('returns expected API shape', () => {
const [result] = withSetup(() => useVisualViewport())
expect(result.keyboardHeight).toBeDefined()
expect(result.isKeyboardOpen).toBeDefined()
expect(result.viewportHeight).toBeDefined()
})
it('initializes viewportHeight from visualViewport', () => {
const [result] = withSetup(() => useVisualViewport())
expect(result.viewportHeight.value).toBe(800)
})
it('detects keyboard open when height shrinks significantly', () => {
const [result] = withSetup(() => useVisualViewport())
// Simulate keyboard opening (shrinks to 500px from 800px = 300px keyboard)
mockVV._setHeight(500)
vi.advanceTimersByTime(60) // past debounce
expect(result.isKeyboardOpen.value).toBe(true)
expect(result.keyboardHeight.value).toBe(300)
expect(result.viewportHeight.value).toBe(500)
})
it('does not trigger keyboard for small viewport changes', () => {
const [result] = withSetup(() => useVisualViewport())
// Small change (50px) — not keyboard
mockVV._setHeight(750)
vi.advanceTimersByTime(60)
expect(result.isKeyboardOpen.value).toBe(false)
expect(result.keyboardHeight.value).toBe(50)
})
it('debounces rapid viewport changes', () => {
const [result] = withSetup(() => useVisualViewport())
// Rapid changes — only last should apply
mockVV._setHeight(600)
vi.advanceTimersByTime(10)
mockVV._setHeight(550)
vi.advanceTimersByTime(10)
mockVV._setHeight(500)
vi.advanceTimersByTime(60)
expect(result.viewportHeight.value).toBe(500)
expect(result.keyboardHeight.value).toBe(300)
})
})