From 9d04176e902bb499bb06cad24b5a463510dc8a73 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 16 Jul 2026 15:52:00 +0800 Subject: [PATCH] test(chat): enable virtual keyboard overlay experiment --- src/app/chat/components/chat-input-bar.tsx | 2 + .../use-chat-keyboard-diagnostics.test.tsx | 2 + ...rtual-keyboard-overlay-experiment.test.tsx | 43 +++++++++++ .../hooks/use-chat-keyboard-diagnostics.ts | 4 + ...hat-virtual-keyboard-overlay-experiment.ts | 73 +++++++++++++++++++ src/app/chat/layout.tsx | 5 -- 6 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 src/app/chat/hooks/__tests__/use-chat-virtual-keyboard-overlay-experiment.test.tsx create mode 100644 src/app/chat/hooks/use-chat-virtual-keyboard-overlay-experiment.ts diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index 909e2b60..bdd4ac7a 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -5,6 +5,7 @@ import { useChatDispatch } from "@/stores/chat/chat-context"; import { Logger } from "@/utils/logger"; import { useChatKeyboardDiagnostics } from "../hooks/use-chat-keyboard-diagnostics"; +import { useChatVirtualKeyboardOverlayExperiment } from "../hooks/use-chat-virtual-keyboard-overlay-experiment"; import { ChatInputTextField } from "./chat-input-text-field"; import { ChatSendButton } from "./chat-send-button"; import styles from "./chat-input-bar.module.css"; @@ -26,6 +27,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const hasContent = input.trim().length > 0; + useChatVirtualKeyboardOverlayExperiment(); useChatKeyboardDiagnostics({ containerRef: barRef }); const handleInputChange = (value: string) => { diff --git a/src/app/chat/hooks/__tests__/use-chat-keyboard-diagnostics.test.tsx b/src/app/chat/hooks/__tests__/use-chat-keyboard-diagnostics.test.tsx index c6d5375a..8083de47 100644 --- a/src/app/chat/hooks/__tests__/use-chat-keyboard-diagnostics.test.tsx +++ b/src/app/chat/hooks/__tests__/use-chat-keyboard-diagnostics.test.tsx @@ -39,6 +39,7 @@ class MockVisualViewport extends EventTarget { } class MockVirtualKeyboard extends EventTarget { + overlaysContent = false; boundingRect = { x: 0, y: 460, @@ -96,6 +97,7 @@ describe("chat keyboard diagnostics", () => { visualViewportSupported: true, visualHeight: 460, virtualKeyboardSupported: true, + virtualKeyboardOverlaysContent: false, virtualKeyboardRect: { height: 300 }, activeElement: "TEXTAREA", activeFocused: true, diff --git a/src/app/chat/hooks/__tests__/use-chat-virtual-keyboard-overlay-experiment.test.tsx b/src/app/chat/hooks/__tests__/use-chat-virtual-keyboard-overlay-experiment.test.tsx new file mode 100644 index 00000000..2caaa8b3 --- /dev/null +++ b/src/app/chat/hooks/__tests__/use-chat-virtual-keyboard-overlay-experiment.test.tsx @@ -0,0 +1,43 @@ +/* @vitest-environment jsdom */ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + enableChatVirtualKeyboardOverlayExperiment, + isChatVirtualKeyboardOverlayExperimentEnabled, +} from "../use-chat-virtual-keyboard-overlay-experiment"; + +describe("Virtual Keyboard Overlay experiment", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("enables only for the explicit experiment parameter", () => { + expect( + isChatVirtualKeyboardOverlayExperimentEnabled( + "?keyboard_debug=1&virtual_keyboard_overlay=1", + ), + ).toBe(true); + expect( + isChatVirtualKeyboardOverlayExperimentEnabled( + "?keyboard_debug=1&virtual_keyboard_overlay=0", + ), + ).toBe(false); + expect(isChatVirtualKeyboardOverlayExperimentEnabled("")).toBe(false); + }); + + it("enables overlay content and restores the previous value", () => { + const virtualKeyboard = { overlaysContent: false }; + Object.defineProperty(navigator, "virtualKeyboard", { + configurable: true, + value: virtualKeyboard, + }); + vi.spyOn(window.console, "info").mockImplementation(() => {}); + + const cleanup = enableChatVirtualKeyboardOverlayExperiment(); + + expect(virtualKeyboard.overlaysContent).toBe(true); + cleanup(); + expect(virtualKeyboard.overlaysContent).toBe(false); + }); +}); diff --git a/src/app/chat/hooks/use-chat-keyboard-diagnostics.ts b/src/app/chat/hooks/use-chat-keyboard-diagnostics.ts index 778f7962..b7bc6c0e 100644 --- a/src/app/chat/hooks/use-chat-keyboard-diagnostics.ts +++ b/src/app/chat/hooks/use-chat-keyboard-diagnostics.ts @@ -8,6 +8,7 @@ import { PlatformDetector } from "@/utils/platform-detect"; const CHAT_KEYBOARD_DEBUG_PARAM = "keyboard_debug"; interface VirtualKeyboardLike extends EventTarget { + readonly overlaysContent?: boolean; readonly boundingRect?: Pick< DOMRectReadOnly, "height" | "width" | "x" | "y" @@ -31,6 +32,7 @@ export interface ChatKeyboardDebugSnapshot { visualOffsetLeft: number | null; visualScale: number | null; virtualKeyboardSupported: boolean; + virtualKeyboardOverlaysContent: boolean | null; virtualKeyboardRect: { x: number; y: number; @@ -170,6 +172,8 @@ function createChatKeyboardDebugSnapshot( visualOffsetLeft: visualViewport?.offsetLeft ?? null, visualScale: visualViewport?.scale ?? null, virtualKeyboardSupported: getVirtualKeyboard() != null, + virtualKeyboardOverlaysContent: + getVirtualKeyboard()?.overlaysContent ?? null, virtualKeyboardRect: virtualKeyboardRect ? { x: virtualKeyboardRect.x, diff --git a/src/app/chat/hooks/use-chat-virtual-keyboard-overlay-experiment.ts b/src/app/chat/hooks/use-chat-virtual-keyboard-overlay-experiment.ts new file mode 100644 index 00000000..eb478735 --- /dev/null +++ b/src/app/chat/hooks/use-chat-virtual-keyboard-overlay-experiment.ts @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect } from "react"; + +const CHAT_VIRTUAL_KEYBOARD_OVERLAY_PARAM = "virtual_keyboard_overlay"; + +interface VirtualKeyboardOverlayLike { + overlaysContent: boolean; +} + +interface NavigatorWithVirtualKeyboard extends Navigator { + readonly virtualKeyboard?: VirtualKeyboardOverlayLike; +} + +export function useChatVirtualKeyboardOverlayExperiment(): void { + useEffect(() => { + if (!isChatVirtualKeyboardOverlayExperimentEnabled(window.location.search)) { + return; + } + return enableChatVirtualKeyboardOverlayExperiment(); + }, []); +} + +export function isChatVirtualKeyboardOverlayExperimentEnabled( + search: string, +): boolean { + return ( + new URLSearchParams(search).get(CHAT_VIRTUAL_KEYBOARD_OVERLAY_PARAM) === + "1" + ); +} + +export function enableChatVirtualKeyboardOverlayExperiment(): () => void { + const virtualKeyboard = getVirtualKeyboard(); + if (!virtualKeyboard) { + window.console.info( + "[keyboard-debug] Virtual Keyboard Overlay API is unavailable", + ); + return () => {}; + } + + const previousValue = virtualKeyboard.overlaysContent; + try { + virtualKeyboard.overlaysContent = true; + window.console.info("[keyboard-debug] Virtual Keyboard Overlay enabled", { + previousValue, + currentValue: virtualKeyboard.overlaysContent, + }); + } catch (error) { + window.console.info( + "[keyboard-debug] Virtual Keyboard Overlay failed", + error, + ); + return () => {}; + } + + return () => { + try { + virtualKeyboard.overlaysContent = previousValue; + } catch { + // The WebView may invalidate the API object while the route is closing. + } + }; +} + +function getVirtualKeyboard(): VirtualKeyboardOverlayLike | null { + const virtualKeyboard = ( + navigator as NavigatorWithVirtualKeyboard + ).virtualKeyboard; + return virtualKeyboard && typeof virtualKeyboard.overlaysContent === "boolean" + ? virtualKeyboard + : null; +} diff --git a/src/app/chat/layout.tsx b/src/app/chat/layout.tsx index 048d0789..275a186c 100644 --- a/src/app/chat/layout.tsx +++ b/src/app/chat/layout.tsx @@ -1,12 +1,7 @@ -import type { Viewport } from "next"; import type { ReactNode } from "react"; import { ChatRouteProviders } from "@/providers/chat-route-providers"; -export const viewport: Viewport = { - interactiveWidget: "resizes-content", -}; - export default function ChatLayout({ children }: { children: ReactNode }) { return {children}; }