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
@@ -5,6 +5,7 @@ import { useChatDispatch } from "@/stores/chat/chat-context";
import { Logger } from "@/utils/logger"; import { Logger } from "@/utils/logger";
import { useChatKeyboardDiagnostics } from "../hooks/use-chat-keyboard-diagnostics"; 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 { ChatInputTextField } from "./chat-input-text-field";
import { ChatSendButton } from "./chat-send-button"; import { ChatSendButton } from "./chat-send-button";
import styles from "./chat-input-bar.module.css"; import styles from "./chat-input-bar.module.css";
@@ -26,6 +27,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const hasContent = input.trim().length > 0; const hasContent = input.trim().length > 0;
useChatVirtualKeyboardOverlayExperiment();
useChatKeyboardDiagnostics({ containerRef: barRef }); useChatKeyboardDiagnostics({ containerRef: barRef });
const handleInputChange = (value: string) => { const handleInputChange = (value: string) => {
@@ -39,6 +39,7 @@ class MockVisualViewport extends EventTarget {
} }
class MockVirtualKeyboard extends EventTarget { class MockVirtualKeyboard extends EventTarget {
overlaysContent = false;
boundingRect = { boundingRect = {
x: 0, x: 0,
y: 460, y: 460,
@@ -96,6 +97,7 @@ describe("chat keyboard diagnostics", () => {
visualViewportSupported: true, visualViewportSupported: true,
visualHeight: 460, visualHeight: 460,
virtualKeyboardSupported: true, virtualKeyboardSupported: true,
virtualKeyboardOverlaysContent: false,
virtualKeyboardRect: { height: 300 }, virtualKeyboardRect: { height: 300 },
activeElement: "TEXTAREA", activeElement: "TEXTAREA",
activeFocused: true, 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"; const CHAT_KEYBOARD_DEBUG_PARAM = "keyboard_debug";
interface VirtualKeyboardLike extends EventTarget { interface VirtualKeyboardLike extends EventTarget {
readonly overlaysContent?: boolean;
readonly boundingRect?: Pick< readonly boundingRect?: Pick<
DOMRectReadOnly, DOMRectReadOnly,
"height" | "width" | "x" | "y" "height" | "width" | "x" | "y"
@@ -31,6 +32,7 @@ export interface ChatKeyboardDebugSnapshot {
visualOffsetLeft: number | null; visualOffsetLeft: number | null;
visualScale: number | null; visualScale: number | null;
virtualKeyboardSupported: boolean; virtualKeyboardSupported: boolean;
virtualKeyboardOverlaysContent: boolean | null;
virtualKeyboardRect: { virtualKeyboardRect: {
x: number; x: number;
y: number; y: number;
@@ -170,6 +172,8 @@ function createChatKeyboardDebugSnapshot(
visualOffsetLeft: visualViewport?.offsetLeft ?? null, visualOffsetLeft: visualViewport?.offsetLeft ?? null,
visualScale: visualViewport?.scale ?? null, visualScale: visualViewport?.scale ?? null,
virtualKeyboardSupported: getVirtualKeyboard() != null, virtualKeyboardSupported: getVirtualKeyboard() != null,
virtualKeyboardOverlaysContent:
getVirtualKeyboard()?.overlaysContent ?? null,
virtualKeyboardRect: virtualKeyboardRect virtualKeyboardRect: virtualKeyboardRect
? { ? {
x: virtualKeyboardRect.x, 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;
}
-5
View File
@@ -1,12 +1,7 @@
import type { Viewport } from "next";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { ChatRouteProviders } from "@/providers/chat-route-providers"; import { ChatRouteProviders } from "@/providers/chat-route-providers";
export const viewport: Viewport = {
interactiveWidget: "resizes-content",
};
export default function ChatLayout({ children }: { children: ReactNode }) { export default function ChatLayout({ children }: { children: ReactNode }) {
return <ChatRouteProviders>{children}</ChatRouteProviders>; return <ChatRouteProviders>{children}</ChatRouteProviders>;
} }