Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions apps/builder/__tests__/webchat-message-input-origin.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// @vitest-environment jsdom

import { act, type ReactNode } from "react"
import { createRoot, type Root } from "react-dom/client"
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"

const mocks = vi.hoisted(() => ({
submittedInputs: [] as Array<Record<string, unknown>>,
actionOptions: null as {
onExecute: (args: { input: Record<string, unknown> }) => void
onSuccess: (args: { data: null }) => void
} | null,
currentFormValues: null as Record<string, unknown> | null,
submit: null as (() => Promise<void>) | null,
clientEmbeddingOrigin: "https://www.example.com" as string | null,
}))

vi.mock("@/features/messages/actions/create-webchat-message.action", () => ({
createWebchatMessageAction: {},
}))

vi.mock("@next-safe-action/adapter-react-hook-form/hooks", () => ({
useHookFormAction: vi.fn((_action, _resolver, options) => {
mocks.actionOptions = options.actionProps
if (!mocks.currentFormValues) {
mocks.currentFormValues = { ...options.formProps.defaultValues }
}

const form = {
control: {},
formState: { isValid: true, isSubmitting: false },
setValue: vi.fn(),
reset: vi.fn((values: Record<string, unknown>) => {
mocks.currentFormValues = { ...values }
}),
}

const submit = async () => {
const input = { ...mocks.currentFormValues, text: "hello" }
mocks.submittedInputs.push(input)
mocks.actionOptions?.onExecute({ input })
mocks.actionOptions?.onSuccess({ data: null })
}
mocks.submit = submit

return { form, handleSubmitWithAction: submit, resetFormAndAction: vi.fn() }
}),
}))

vi.mock("@/features/integration-webchat/lib/authorized-domain", () => ({
getClientEmbeddingOrigin: () => mocks.clientEmbeddingOrigin,
}))
vi.mock("@/features/integration-webchat/providers/store/guest-session-provider", () => ({
useGuestSessionStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({ appendMessage: vi.fn(), guestConversationId: "guest-1", sendMessage: vi.fn() }),
}))
vi.mock("@chatbotx.io/ui/components/ui/form", () => ({ Form: ({ children }: { children: ReactNode }) => children }))
vi.mock("@chatbotx.io/ui/components/ui/textarea", () => ({ Textarea: (props: Record<string, unknown>) => <textarea {...props} /> }))
vi.mock("@chatbotx.io/ui/components/ui/button", () => ({ Button: ({ children, ...props }: Record<string, unknown>) => <button {...props}>{children as ReactNode}</button> }))
vi.mock("react-hook-form", () => ({
Controller: ({ render }: { render: (args: { field: Record<string, unknown> }) => ReactNode }) => render({ field: { value: "", onChange: vi.fn() } }),
useWatch: ({ name }: { name: string }) => (name === "files" ? [] : ""),
}))
vi.mock("../src/features/messages/components/emoji-picker", () => ({ default: () => null }))
vi.mock("../src/features/messages/components/file-upload", () => ({ FileUploadPreview: () => null }))
vi.mock("../src/features/integration-webchat/components/webchat-message-menu", () => ({ default: () => null }))
vi.mock("../src/features/integration-webchat/browser-profile-fields", () => ({ getWebchatProfileFields: () => ({}) }))

import { WebchatMessageInput } from "../src/features/integration-webchat/webchat-message-input"

describe("WebchatMessageInput embedding origin", () => {
let container: HTMLDivElement
let root: Root

beforeEach(() => {
mocks.submittedInputs = []
mocks.actionOptions = null
mocks.currentFormValues = null
mocks.submit = null
mocks.clientEmbeddingOrigin = "https://www.example.com"
container = document.createElement("div")
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => root.unmount())
container.remove()
})

test("keeps normal origin stable across prop changes and reset", async () => {
const props = { workspaceId: "workspace-1", webchatId: "webchat-1", accessToken: "token", parentOrigin: "https://www.example.com" }
await act(async () => root.render(<WebchatMessageInput {...props} />))
await act(async () => root.render(<WebchatMessageInput {...props} parentOrigin="https://chat.example.com/webchat?..." />))
await act(async () => mocks.submit?.())
await act(async () => mocks.submit?.())
expect(mocks.submittedInputs.map((input) => input.parentOrigin)).toEqual(["https://www.example.com", "https://www.example.com"])
})

test("keeps null origin when client resolver returns an origin", async () => {
await act(async () => root.render(<WebchatMessageInput workspaceId="workspace-1" webchatId="webchat-1" parentOrigin={null} />))
await act(async () => mocks.submit?.())
expect(mocks.submittedInputs[0]?.parentOrigin).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import { createId } from "@chatbotx.io/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"
import { PaperclipIcon, SendHorizonalIcon } from "lucide-react"
import { type KeyboardEvent, useEffect, useMemo, useRef } from "react"
import {
type KeyboardEvent,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import { Controller, useWatch } from "react-hook-form"
import { createWebchatMessageAction } from "../messages/actions/create-webchat-message.action"
import EmojiPicker from "../messages/components/emoji-picker"
import { FileUploadPreview } from "../messages/components/file-upload"
import { createWebchatMessageRequest } from "../messages/schema/mutation"
import { getWebchatProfileFields } from "./browser-profile-fields"
import WebchatMessageMenu from "./components/webchat-message-menu"
import { getClientEmbeddingOrigin } from "./lib/authorized-domain"
import { useGuestSessionStore } from "./providers/store/guest-session-provider"

type WebchatMessageInputProps = {
Expand All @@ -33,9 +40,21 @@ export const WebchatMessageInput = (props: WebchatMessageInputProps) => {
parentOrigin,
accessToken,
} = props
const [embeddingOrigin, setEmbeddingOrigin] = useState(parentOrigin)
const { sendMessage, guestConversationId, appendMessage } =
useGuestSessionStore((state) => state)

useEffect(() => {
if (!parentOrigin) {
return
}

const clientEmbeddingOrigin = getClientEmbeddingOrigin()
if (clientEmbeddingOrigin) {
setEmbeddingOrigin(clientEmbeddingOrigin)
Comment on lines +47 to +54
}
}, [])

const textareaRef = useRef<HTMLTextAreaElement>(null)
const defaultValues = useMemo(
() => ({
Expand All @@ -47,14 +66,14 @@ export const WebchatMessageInput = (props: WebchatMessageInputProps) => {
ref: referral,
...getWebchatProfileFields(),
accessToken: accessToken ?? undefined,
parentOrigin: parentOrigin ?? undefined,
parentOrigin: embeddingOrigin ?? undefined,
}),
[
workspaceId,
webchatId,
guestConversationId,
referral,
parentOrigin,
embeddingOrigin,
accessToken,
],
)
Expand Down Expand Up @@ -186,7 +205,7 @@ export const WebchatMessageInput = (props: WebchatMessageInputProps) => {
<div className="flex-1">
<WebchatMessageMenu
accessToken={accessToken}
parentOrigin={parentOrigin}
parentOrigin={embeddingOrigin}
webchatId={webchatId}
workspaceId={workspaceId}
/>
Expand Down