test(chat): enable virtual keyboard overlay experiment

This commit is contained in:
2026-07-16 15:52:00 +08:00
parent 31eda84ea9
commit 9d04176e90
6 changed files with 124 additions and 5 deletions
@@ -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,
@@ -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);
});
});
@@ -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,
@@ -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;
}