/* @vitest-environment jsdom */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { isEditableKeyboardTarget, resolveMobileKeyboardExposedFill, resolveMobileKeyboardState, stabilizeMobileViewportKeyboardFocus, } from './mobileViewportKeyboardFocus'; const originalMatchMedia = window.matchMedia; const originalRequestAnimationFrame = window.requestAnimationFrame; const originalCancelAnimationFrame = window.cancelAnimationFrame; const originalInnerHeight = window.innerHeight; const originalMaxTouchPoints = navigator.maxTouchPoints; const originalVisualViewport = window.visualViewport; function defineWindowValue(key: Key, value: Window[Key]) { Object.defineProperty(window, key, { configurable: true, value, }); } function defineNavigatorValue( key: Key, value: Navigator[Key], ) { Object.defineProperty(navigator, key, { configurable: true, value, }); } beforeEach(() => { document.body.innerHTML = ''; document.documentElement.removeAttribute('data-mobile-keyboard-open'); document.documentElement.removeAttribute('data-mobile-viewport-keyboard-focus'); document.documentElement.removeAttribute('style'); }); afterEach(() => { vi.useRealTimers(); document.body.innerHTML = ''; document.documentElement.removeAttribute('data-mobile-keyboard-open'); document.documentElement.removeAttribute('data-mobile-viewport-keyboard-focus'); document.documentElement.removeAttribute('style'); defineWindowValue('matchMedia', originalMatchMedia); defineWindowValue('requestAnimationFrame', originalRequestAnimationFrame); defineWindowValue('cancelAnimationFrame', originalCancelAnimationFrame); defineWindowValue('innerHeight', originalInnerHeight); defineNavigatorValue('maxTouchPoints', originalMaxTouchPoints); defineWindowValue('visualViewport', originalVisualViewport); }); describe('isEditableKeyboardTarget', () => { it('matches controls that open the mobile keyboard', () => { const input = document.createElement('input'); input.type = 'text'; const textarea = document.createElement('textarea'); const buttonInput = document.createElement('input'); buttonInput.type = 'file'; expect(isEditableKeyboardTarget(input)).toBe(true); expect(isEditableKeyboardTarget(textarea)).toBe(true); expect(isEditableKeyboardTarget(buttonInput)).toBe(false); }); it('ignores disabled and readonly controls', () => { const disabledInput = document.createElement('input'); disabledInput.disabled = true; const readonlyInput = document.createElement('input'); readonlyInput.readOnly = true; expect(isEditableKeyboardTarget(disabledInput)).toBe(false); expect(isEditableKeyboardTarget(readonlyInput)).toBe(false); }); }); describe('resolveMobileKeyboardExposedFill', () => { it('uses the active platform shell fill for exposed mini-program keyboard space', () => { document.body.innerHTML = `
`; expect(resolveMobileKeyboardExposedFill()).toContain('rgb(255, 253, 249)'); }); it('falls back to the light platform fill before the shell mounts', () => { document.body.innerHTML = ''; expect(resolveMobileKeyboardExposedFill()).toContain('#fffdf9'); }); }); describe('resolveMobileKeyboardState', () => { it('detects the keyboard inset without asking the app shell to shift', () => { expect( resolveMobileKeyboardState({ layoutHeight: 800, visualTop: 0, visualHeight: 500, hasEditableTarget: true, }), ).toEqual({ isOpen: true, insetBottom: 300, }); }); it('stays closed when no editable target is focused', () => { expect( resolveMobileKeyboardState({ layoutHeight: 800, visualTop: 0, visualHeight: 500, hasEditableTarget: false, }), ).toEqual({ isOpen: false, insetBottom: 0, }); }); it('accounts for browser visual viewport panning without returning a shell offset', () => { expect( resolveMobileKeyboardState({ layoutHeight: 800, visualTop: 120, visualHeight: 500, hasEditableTarget: true, }), ).toEqual({ isOpen: true, insetBottom: 180, }); }); }); describe('stabilizeMobileViewportKeyboardFocus', () => { it('marks H5 keyboard state without applying a global shell transform offset', () => { vi.useFakeTimers(); document.body.innerHTML = `
`; defineNavigatorValue('maxTouchPoints', 1); defineWindowValue( 'matchMedia', vi.fn().mockReturnValue({ matches: true }) as unknown as Window['matchMedia'], ); defineWindowValue( 'requestAnimationFrame', ((callback: FrameRequestCallback) => { callback(0); return 1; }) as Window['requestAnimationFrame'], ); defineWindowValue( 'cancelAnimationFrame', vi.fn() as unknown as Window['cancelAnimationFrame'], ); defineWindowValue('innerHeight', 800); defineWindowValue('visualViewport', { height: 500, offsetTop: 120, addEventListener: vi.fn(), removeEventListener: vi.fn(), } as unknown as VisualViewport); stabilizeMobileViewportKeyboardFocus(); document.getElementById('theme-input')?.focus(); document.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); vi.runAllTimers(); expect(document.documentElement.dataset.mobileKeyboardOpen).toBe('true'); expect( document.documentElement.style.getPropertyValue( '--platform-keyboard-focus-offset', ), ).toBe('0px'); expect( document.documentElement.style.getPropertyValue( '--platform-keyboard-inset-bottom', ), ).toBe('180px'); expect( document.documentElement.style.getPropertyValue( '--platform-keyboard-exposed-fill', ), ).toContain('rgb(255, 253, 249)'); }); });