import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { render } from "@testing-library/vue"; import { ref } from "vue"; import type { Ref } from "vue"; import { OpenGenerativeUIActivityRenderer } from "../OpenGenerativeUIRenderer"; import type { OpenGenerativeUIContent } from "../OpenGenerativeUIRenderer"; import { SandboxFunctionsKey } from "../../providers/keys"; import type { SandboxFunction } from "../../types"; import { z } from "zod"; const mockRun = vi.fn().mockResolvedValue(undefined); const mockDestroy = vi.fn(); let mockPromiseResolve: () => void; let mockPromise: Promise; function resetMockPromise() { mockPromise = new Promise((resolve) => { mockPromiseResolve = resolve; }); } type SandboxOptions = { frameContent?: string } & Record; type MockCreateFn = ( localApi: Record, options: SandboxOptions, ) => { iframe: HTMLIFrameElement; promise: Promise; run: typeof mockRun; destroy: typeof mockDestroy; }; const mockCreate = vi.fn(() => ({ iframe: document.createElement("iframe"), promise: mockPromise, run: mockRun, destroy: mockDestroy, })); vi.mock("@jetbrains/websandbox", () => ({ default: { create: (localApi: Record, options: SandboxOptions) => mockCreate(localApi, options), }, })); async function flushImport() { await new Promise((resolve) => setTimeout(resolve, 0)); } function renderRenderer( content: OpenGenerativeUIContent, sandboxFunctionsOrRef: | readonly SandboxFunction[] | Ref = [], ) { const sandboxFunctionsRef = typeof (sandboxFunctionsOrRef as Ref).value !== "undefined" ? (sandboxFunctionsOrRef as Ref) : ref(sandboxFunctionsOrRef as readonly SandboxFunction[]); return render(OpenGenerativeUIActivityRenderer, { props: { activityType: "open-generative-ui", content, message: {}, agent: undefined, }, global: { provide: { [SandboxFunctionsKey as symbol]: sandboxFunctionsRef, }, }, }); } describe("OpenGenerativeUIRenderer", () => { beforeEach(() => { vi.clearAllMocks(); resetMockPromise(); }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); it("renders placeholder when no html", async () => { const { getByTestId, queryByTestId } = renderRenderer({ initialHeight: 300, }); await flushImport(); expect(getByTestId("open-generative-ui-placeholder")).not.toBeNull(); expect(queryByTestId("open-generative-ui-final-sandbox")).toBeNull(); expect(mockCreate).not.toHaveBeenCalled(); }); it("creates final sandbox when html is complete", async () => { renderRenderer({ html: ["

Hello

"], htmlComplete: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toContain("Hello"); }); it("creates preview sandbox when streaming", async () => { renderRenderer({ html: ["

Partial

"], htmlComplete: false, cssComplete: true, }); await flushImport(); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toBe(""); }); it("wraps html missing head", async () => { renderRenderer({ html: ["

No head

"], htmlComplete: true, }); await flushImport(); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toContain(""); }); it("joins html chunks when complete", async () => { renderRenderer({ html: ["", "", "

Hello

", ""], htmlComplete: true, }); await flushImport(); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toContain("

Hello

"); }); it("injects jsFunctions via sandbox run", async () => { renderRenderer({ html: ["
App
"], htmlComplete: true, jsFunctions: "function greet() { return 'hi'; }", }); await flushImport(); mockPromiseResolve(); await mockPromise; await flushImport(); expect(mockRun).toHaveBeenCalledWith("function greet() { return 'hi'; }"); }); it("recreates sandbox when html changes", async () => { const first = renderRenderer({ html: ["v1"], htmlComplete: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); mockPromiseResolve(); await mockPromise; resetMockPromise(); first.rerender({ activityType: "open-generative-ui", content: { html: ["v2"], htmlComplete: true, }, message: {}, agent: {}, }); await flushImport(); expect(mockDestroy).toHaveBeenCalledTimes(1); expect(mockCreate).toHaveBeenCalledTimes(2); const [, options] = mockCreate.mock.calls[1]; expect(options.frameContent).toContain("v2"); }); it("destroys sandbox on unmount", async () => { const mounted = renderRenderer({ html: ["bye"], htmlComplete: true, }); await flushImport(); mounted.unmount(); expect(mockDestroy).toHaveBeenCalledTimes(1); }); it("executes jsExpressions sequentially", async () => { renderRenderer({ html: [""], htmlComplete: true, jsExpressions: ["expr1()", "expr2()"], }); await flushImport(); mockPromiseResolve(); await mockPromise; await flushImport(); const exprCalls = mockRun.mock.calls .map((c) => c[0]) .filter((c) => typeof c === "string" && c.startsWith("expr")); expect(exprCalls).toEqual(["expr1()", "expr2()"]); }); it("does not re-execute prior jsExpressions on rerender", async () => { const mounted = renderRenderer({ html: [""], htmlComplete: true, jsExpressions: ["expr1()"], }); await flushImport(); mockPromiseResolve(); await mockPromise; await flushImport(); mounted.rerender({ activityType: "open-generative-ui", content: { html: [""], htmlComplete: true, jsExpressions: ["expr1()", "expr2()"], }, message: {}, agent: {}, }); await flushImport(); const exprCalls = mockRun.mock.calls .map((c) => c[0]) .filter((c) => c === "expr1()" || c === "expr2()"); expect(exprCalls.filter((x) => x === "expr1()")).toHaveLength(1); expect(exprCalls.filter((x) => x === "expr2()")).toHaveLength(1); }); it("queues JS before sandbox readiness and flushes after promise resolves", async () => { renderRenderer({ html: [""], htmlComplete: true, jsFunctions: "function foo() {}", jsExpressions: ["foo()"], }); await flushImport(); expect( mockRun.mock.calls.some( (c) => c[0] === "function foo() {}" || c[0] === "foo()", ), ).toBe(false); mockPromiseResolve(); await mockPromise; await flushImport(); expect(mockRun).toHaveBeenCalledWith("function foo() {}"); expect(mockRun).toHaveBeenCalledWith("foo()"); }); it("passes localApi built from sandbox functions to websandbox", async () => { const handler = vi.fn().mockResolvedValue(42); const sandboxFunctions: SandboxFunction[] = [ { name: "addToCart", description: "Add item to cart", parameters: z.object({ itemId: z.string() }), handler, }, ]; renderRenderer( { html: ["test"], htmlComplete: true, }, sandboxFunctions, ); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); const [localApi] = mockCreate.mock.calls[0]!; expect(localApi).toHaveProperty("addToCart"); expect(localApi.addToCart).toBe(handler); }); it("passes empty localApi with no functions", async () => { renderRenderer( { html: ["test"], htmlComplete: true, }, [], ); await flushImport(); const [localApi] = mockCreate.mock.calls[0]; expect(Object.keys(localApi as object)).toHaveLength(0); }); it("passes multiple sandbox functions via localApi", async () => { const handlerA = vi.fn(); const handlerB = vi.fn(); const sandboxFunctions: SandboxFunction[] = [ { name: "fnA", description: "A", parameters: z.object({}), handler: handlerA, }, { name: "fnB", description: "B", parameters: z.object({}), handler: handlerB, }, ]; renderRenderer( { html: ["multi"], htmlComplete: true, }, sandboxFunctions, ); await flushImport(); const [localApi] = mockCreate.mock.calls[0]!; expect(Object.keys(localApi)).toHaveLength(2); expect(localApi.fnA).toBe(handlerA); expect(localApi.fnB).toBe(handlerB); }); it("recreates sandbox when sandbox functions change", async () => { const handlerA = vi.fn(); const functionsRef = ref([ { name: "fnA", description: "A", parameters: z.object({}), handler: handlerA, }, ]); const mounted = renderRenderer( { html: ["reactive"], htmlComplete: true, }, functionsRef, ); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); mockPromiseResolve(); await mockPromise; resetMockPromise(); const handlerB = vi.fn(); functionsRef.value = [ { name: "fnB", description: "B", parameters: z.object({}), handler: handlerB, }, ]; await mounted.rerender({ activityType: "open-generative-ui", content: { html: ["reactive"], htmlComplete: true, }, message: {}, agent: {}, }); await flushImport(); expect(mockDestroy).toHaveBeenCalledTimes(1); expect(mockCreate).toHaveBeenCalledTimes(2); const [localApi] = mockCreate.mock.calls[1]!; expect(Object.keys(localApi)).toEqual(["fnB"]); expect(localApi.fnB).toBe(handlerB); }); describe("progressive streaming preview", () => { it("creates preview when chunks arrive during streaming", async () => { renderRenderer({ html: ["
Hello
"], htmlComplete: false, cssComplete: true, generating: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toBe(""); }); it("updates preview after throttled rerender", async () => { const mounted = renderRenderer({ html: ["
Hello
"], htmlComplete: false, cssComplete: true, generating: true, }); await flushImport(); mockPromiseResolve(); await mockPromise; await flushImport(); mockRun.mockClear(); mounted.rerender({ activityType: "open-generative-ui", content: { html: ["
Hello
", "

World

"], htmlComplete: false, cssComplete: true, generating: true, }, message: {}, agent: {}, }); await new Promise((resolve) => setTimeout(resolve, 1100)); await flushImport(); const bodyCalls = mockRun.mock.calls.filter( (call) => typeof call[0] === "string" && (call[0] as string).includes("document.body.innerHTML"), ); expect(bodyCalls.length).toBeGreaterThan(0); expect(bodyCalls[bodyCalls.length - 1]?.[0]).toContain("World"); }); it("switches from preview to final sandbox on htmlComplete", async () => { const mounted = renderRenderer({ html: ["
Hello
"], htmlComplete: false, cssComplete: true, generating: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); resetMockPromise(); mounted.rerender({ activityType: "open-generative-ui", content: { html: ["
Hello
"], htmlComplete: true, cssComplete: true, generating: false, }, message: {}, agent: {}, }); await flushImport(); expect(mockDestroy).toHaveBeenCalledTimes(1); expect(mockCreate).toHaveBeenCalledTimes(2); const [, options] = mockCreate.mock.calls[1]; expect(options.frameContent).toContain("Hello"); }); it("gates preview until cssComplete", async () => { const mounted = renderRenderer({ html: ["
Streaming content
"], htmlComplete: false, generating: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(0); mounted.rerender({ activityType: "open-generative-ui", content: { css: "body { margin: 0; color: red; }", cssComplete: true, html: ["
Streaming content
"], htmlComplete: false, generating: true, }, message: {}, agent: {}, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toBe(""); }); it("suppresses preview when body content is not meaningful", async () => { renderRenderer({ html: [""], htmlComplete: false, cssComplete: true, generating: true, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(0); }); it("skips preview for fast completion", async () => { renderRenderer({ html: ["
Done
"], htmlComplete: true, generating: false, }); await flushImport(); expect(mockCreate).toHaveBeenCalledTimes(1); const [, options] = mockCreate.mock.calls[0]; expect(options.frameContent).toContain("Done"); expect(options.frameContent).not.toBe(""); }); }); });