diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 97d75c2c4ecb0..496ac376bd5a8 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -941,9 +941,11 @@ it.live("reports a pending dialog without evaluating the page and resolves it ex Effect.sync(() => resolvedEvaluation.resolve({ result: { value: true } })), ); yield* Effect.promise(() => started.promise); + // The 4,000-character cap falls between the halves of the emoji. + const message = `Delete row?${" ".repeat(3_988)}😀`; const dialog = { type: () => "confirm", - message: () => "Delete row?", + message: () => message, defaultValue: () => "", accept: vi.fn(async () => { resolvedEvaluation.resolve({ result: { value: true } }); @@ -958,7 +960,7 @@ it.live("reports a pending dialog without evaluating the page and resolves it ex operation: "status", input: {}, }); - expect(status.dialog).toMatchObject({ type: "confirm", message: "Delete row?" }); + expect(status.dialog).toMatchObject({ type: "confirm", message: message.slice(0, 3_999) }); expect(page.title).not.toHaveBeenCalled(); expect(dialog.dismiss).not.toHaveBeenCalled(); const reuse = yield* broker diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 2f681d0c151c9..8d9fded855ed8 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -370,6 +370,9 @@ const DOWNLOAD_LIMIT = 20; const ABORTED_NAVIGATION_DOWNLOAD_WAIT_MS = 5_000; const DOWNLOAD_MAX_BYTES = 1024 * 1024 * 1024; +// Half an emoji serializes to a lone surrogate escape, which provider JSON parsers reject. +const cutDialogText = (text: string) => text.slice(0, 4_000).replace(/[\uD800-\uDBFF]$/, ""); + const tabKey = (threadId: string, tabId: string) => `${threadId}\u0000${tabId}`; const pushBounded = ( @@ -537,8 +540,8 @@ const make = Effect.gen(function* () { tab.dialog ? { type: tab.dialog.type(), - message: tab.dialog.message().slice(0, 4000), - defaultValue: tab.dialog.defaultValue().slice(0, 4000), + message: cutDialogText(tab.dialog.message()), + defaultValue: cutDialogText(tab.dialog.defaultValue()), } : null;