diff --git a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx index 27063f24f1b30f7181eec3ada4a8885a768d5133..9a6bd6a684022676e9bef85497f27728a0d3b7d7 100644 --- a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx +++ b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx @@ -26,6 +26,7 @@ import { AppText } from "../../components/AppText"; import { downloadAndShareAttachment } from "../../lib/attachmentDownload"; import { beginForegroundHandoff } from "../../lib/foreground-handoff"; import { usePreviewStreamAccess } from "../../state/preview"; +import { previewPromptText } from "./previewPromptText"; import { previewStreamDocument, @@ -256,6 +257,7 @@ function PreviewStreamDocumentView({ const [hostSetup, setHostSetup] = useState(null); const [started, setStarted] = useState(false); const [control, setControl] = useState(null); + const dialogRef = useRef(null); const [promptText, setPromptText] = useState(""); const [fileChooser, setFileChooser] = useState(null); const answerFileChooser = (pick: boolean) => { @@ -371,11 +373,14 @@ function PreviewStreamDocumentView({ const message = previewStreamMessage(event.nativeEvent.data); if (message === null) return; switch (message.type) { - case "control": + case "control": { setControl(message); - setPromptText(message.dialog?.defaultValue ?? ""); + const previousDialog = dialogRef.current; + dialogRef.current = message.dialog; + setPromptText((draft) => previewPromptText(previousDialog, message.dialog, draft)); onControl?.(message); return; + } case "unauthorized": if (!onUnauthorized()) { fail("This session can't open the browser stream. Reconnect to try again."); @@ -413,6 +418,8 @@ function PreviewStreamDocumentView({ } setStatus(message.status); if (message.status === "connecting") { + // Dialogs may have closed and reopened while this viewer was disconnected. + dialogRef.current = null; setControl(null); onControl?.(null); } diff --git a/apps/mobile/src/features/browser/previewPromptText.test.ts b/apps/mobile/src/features/browser/previewPromptText.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..51fa7fe91bffb09dd96f19fd9c3d6e5d9aab64cf --- /dev/null +++ b/apps/mobile/src/features/browser/previewPromptText.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { previewPromptText } from "./previewPromptText"; + +describe("browser prompt response", () => { + const prompt = { type: "prompt" as const, message: "Your name?", defaultValue: "Guest" }; + + it("preserves a typed response across repeated control broadcasts", () => { + expect(previewPromptText(prompt, { ...prompt }, "Alice")).toBe("Alice"); + }); + + it("preserves an intentionally empty response", () => { + expect(previewPromptText(prompt, { ...prompt }, "")).toBe(""); + }); + + it("initializes a newly opened prompt", () => { + expect(previewPromptText(null, prompt, "")).toBe("Guest"); + }); + + it("resets a stale draft after reconnecting without dialog history", () => { + expect(previewPromptText(null, prompt, "Alice")).toBe("Guest"); + }); + + it("resets when the message changes", () => { + expect(previewPromptText(prompt, { ...prompt, message: "Next name?" }, "Alice")).toBe("Guest"); + }); + + it("resets when the default value changes", () => { + expect(previewPromptText(prompt, { ...prompt, defaultValue: "Bob" }, "Alice")).toBe("Bob"); + }); + + it("resets when the dialog type changes with matching message and default", () => { + expect(previewPromptText(prompt, { ...prompt, type: "confirm" }, "Alice")).toBe("Guest"); + }); + + it("clears a closed dialog and initializes an identical reopened prompt", () => { + const cleared = previewPromptText(prompt, null, "Alice"); + expect(cleared).toBe(""); + expect(previewPromptText(null, prompt, cleared)).toBe("Guest"); + }); +}); diff --git a/apps/mobile/src/features/browser/previewPromptText.ts b/apps/mobile/src/features/browser/previewPromptText.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac0acf4d7fbe53440cfaf0935a77bffa09152d16 --- /dev/null +++ b/apps/mobile/src/features/browser/previewPromptText.ts @@ -0,0 +1,15 @@ +import type { PreviewStreamControl } from "@t3tools/client-runtime/preview/server-browser-stream"; + +type Dialog = PreviewStreamControl["dialog"]; + +/** Updates the native response draft when a browser control message arrives. */ +export function previewPromptText(previous: Dialog, next: Dialog, draft: string) { + if ( + next?.type !== previous?.type || + next?.message !== previous?.message || + next?.defaultValue !== previous?.defaultValue + ) { + return next?.defaultValue ?? ""; + } + return draft; +}