/* Copyright 2026 Marimo. All rights reserved. */
import type { components } from "@marimo-team/marimo-api";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
cellId,
requestId,
uiElementId,
widgetModelId,
} from "@/__tests__/branded";
type Base64String = components["schemas"]["Base64String"];
interface TestIslandApp {
id: string;
cells: { code: string; idx: number; output: string }[];
}
interface TestExportContext {
trusted: true;
notebookCode?: string;
}
// Mock browser APIs before any imports
vi.stubGlobal(
"Worker",
vi.fn(() => ({
addEventListener: vi.fn(),
postMessage: vi.fn(),
terminate: vi.fn(),
})),
);
// Create a mock URL class that works as a constructor
class MockURL {
href: string;
constructor(url: string, base?: string | URL) {
this.href = base ? `${base}/${url}` : url;
}
static createObjectURL = vi.fn(() => "blob:mock-url");
static revokeObjectURL = vi.fn();
}
vi.stubGlobal("URL", MockURL);
// Mock the worker RPC before importing the bridge
const {
mockBridge,
mockLoadPackages,
mockStartSessionRequest,
mockParseMarimoIslandApps,
mockCreateMarimoFile,
mockGetMarimoExportContext,
} = vi.hoisted(() => ({
mockBridge: vi.fn(),
mockLoadPackages: vi.fn(),
mockStartSessionRequest: vi.fn(),
mockParseMarimoIslandApps: vi.fn<() => TestIslandApp[]>(() => []),
mockCreateMarimoFile: vi.fn(),
mockGetMarimoExportContext: vi.fn<() => TestExportContext | undefined>(
() => undefined,
),
}));
vi.mock("@/core/wasm/rpc", () => ({
getWorkerRPC: () => ({
proxy: {
request: {
bridge: mockBridge,
loadPackages: mockLoadPackages,
startSession: mockStartSessionRequest,
},
send: {
consumerReady: vi.fn(),
},
},
addMessageListener: vi.fn(),
}),
}));
// Mock the parse module to avoid DOM dependencies
vi.mock("../parse", () => ({
parseMarimoIslandApps: mockParseMarimoIslandApps,
createMarimoFile: mockCreateMarimoFile,
}));
// Mock uuid to have predictable tokens
vi.mock("@/utils/uuid", () => ({
generateUUID: () => "test-uuid-12345",
}));
vi.mock("@/core/static/export-context", () => ({
getMarimoExportContext: mockGetMarimoExportContext,
}));
// Mock getMarimoVersion
vi.mock("@/core/meta/globals", () => ({
getMarimoVersion: () => "0.0.0-test",
}));
// Mock the jotai store
vi.mock("@/core/state/jotai", () => ({
store: {
get: vi.fn(),
set: vi.fn(),
},
}));
// Now import the bridge class
import { IslandsPyodideBridge } from "../bridge";
describe("IslandsPyodideBridge", () => {
let bridge: IslandsPyodideBridge;
beforeEach(() => {
vi.clearAllMocks();
mockParseMarimoIslandApps.mockReturnValue([]);
mockCreateMarimoFile.mockReset();
mockGetMarimoExportContext.mockReturnValue(undefined);
bridge = new IslandsPyodideBridge({ autoStartSessions: false });
});
describe("startSessionsForAllApps", () => {
it("should prefer trusted export notebook code when there is exactly one reactive app", async () => {
mockParseMarimoIslandApps.mockReturnValue([
{
id: "app-1",
cells: [{ code: "x = 1", idx: 0, output: "
1
" }],
},
]);
mockGetMarimoExportContext.mockReturnValue({
trusted: true,
notebookCode:
"import marimo\napp = marimo.App()\n@app.cell\ndef __():\n x = 1\n return",
});
await (
bridge as unknown as { startSessionsForAllApps(): Promise }
).startSessionsForAllApps();
expect(mockCreateMarimoFile).not.toHaveBeenCalled();
expect(mockStartSessionRequest).toHaveBeenCalledWith({
appId: "app-1",
code: "import marimo\napp = marimo.App()\n@app.cell\ndef __():\n x = 1\n return",
});
});
it("should keep synthesized per-app files for multiple reactive apps even when export context exists", async () => {
mockParseMarimoIslandApps.mockReturnValue([
{
id: "app-1",
cells: [{ code: "x = 1", idx: 0, output: "1
" }],
},
{
id: "app-2",
cells: [{ code: "y = 2", idx: 0, output: "2
" }],
},
]);
mockGetMarimoExportContext.mockReturnValue({
trusted: true,
notebookCode: "full notebook should be ignored",
});
mockCreateMarimoFile
.mockReturnValueOnce("generated app 1")
.mockReturnValueOnce("generated app 2");
await (
bridge as unknown as { startSessionsForAllApps(): Promise }
).startSessionsForAllApps();
expect(mockCreateMarimoFile).toHaveBeenCalledTimes(2);
expect(mockStartSessionRequest).toHaveBeenNthCalledWith(1, {
appId: "app-1",
code: "generated app 1",
});
expect(mockStartSessionRequest).toHaveBeenNthCalledWith(2, {
appId: "app-2",
code: "generated app 2",
});
});
it("should synthesize a file for a single app when no trusted export context is present", async () => {
mockParseMarimoIslandApps.mockReturnValue([
{
id: "app-1",
cells: [{ code: "x = 1", idx: 0, output: "1
" }],
},
]);
mockCreateMarimoFile.mockReturnValue("generated app 1");
await (
bridge as unknown as { startSessionsForAllApps(): Promise }
).startSessionsForAllApps();
expect(mockCreateMarimoFile).toHaveBeenCalledTimes(1);
expect(mockStartSessionRequest).toHaveBeenCalledWith({
appId: "app-1",
code: "generated app 1",
});
});
});
describe("sendComponentValues", () => {
it("should include type field and token in control request", async () => {
const request = {
objectIds: [uiElementId("Hbol-0")],
values: [58],
};
await bridge.sendComponentValues(request);
expect(mockBridge).toHaveBeenCalledWith({
functionName: "put_control_request",
payload: {
type: "update-ui-element",
objectIds: ["Hbol-0"],
values: [58],
token: "test-uuid-12345",
},
});
});
it("should preserve all request properties", async () => {
const request = {
objectIds: [uiElementId("slider-1"), uiElementId("slider-2")],
values: [10, 20],
};
await bridge.sendComponentValues(request);
expect(mockBridge).toHaveBeenCalledWith({
functionName: "put_control_request",
payload: expect.objectContaining({
type: "update-ui-element",
objectIds: ["slider-1", "slider-2"],
values: [10, 20],
}),
});
});
});
describe("sendFunctionRequest", () => {
it("should include type field in control request", async () => {
const request = {
functionCallId: requestId("call-123"),
namespace: "test_namespace",
functionName: "my_function",
args: { x: 1, y: 2 },
};
await bridge.sendFunctionRequest(request);
expect(mockBridge).toHaveBeenCalledWith({
functionName: "put_control_request",
payload: {
type: "invoke-function",
functionCallId: "call-123",
namespace: "test_namespace",
functionName: "my_function",
args: { x: 1, y: 2 },
},
});
});
});
describe("sendRun", () => {
it("should include type field in control request", async () => {
const request = {
cellIds: [cellId("cell-1"), cellId("cell-2")],
codes: ["print('hello')", "print('world')"],
};
await bridge.sendRun(request);
expect(mockBridge).toHaveBeenCalledWith({
functionName: "put_control_request",
payload: {
type: "execute-cells",
cellIds: ["cell-1", "cell-2"],
codes: ["print('hello')", "print('world')"],
},
});
});
it("should call loadPackages before putControlRequest", async () => {
const request = {
cellIds: [cellId("cell-1")],
codes: ["import pandas"],
};
await bridge.sendRun(request);
// Verify loadPackages was called with joined codes
expect(mockLoadPackages).toHaveBeenCalledWith("import pandas");
// Verify order: loadPackages should be called before bridge
const loadPackagesCallOrder =
mockLoadPackages.mock.invocationCallOrder[0];
const bridgeCallOrder = mockBridge.mock.invocationCallOrder[0];
expect(loadPackagesCallOrder).toBeLessThan(bridgeCallOrder);
});
});
describe("sendModelValue", () => {
it("should include type field in control request", async () => {
const request = {
modelId: widgetModelId("widget-1"),
message: {
method: "update" as const,
state: { value: 42 },
bufferPaths: [],
},
buffers: [] as Base64String[],
};
await bridge.sendModelValue(request);
expect(mockBridge).toHaveBeenCalledWith({
functionName: "put_control_request",
payload: {
type: "model",
modelId: "widget-1",
message: {
method: "update",
state: { value: 42 },
bufferPaths: [],
},
buffers: [],
},
});
});
});
describe("control request message format", () => {
it("should always include the type field required by msgspec", async () => {
// Test all methods to ensure they include the type field
await bridge.sendComponentValues({ objectIds: [], values: [] });
await bridge.sendFunctionRequest({
functionCallId: requestId(""),
namespace: "",
functionName: "",
args: {},
});
await bridge.sendRun({ cellIds: [], codes: [] });
await bridge.sendModelValue({
modelId: widgetModelId(""),
message: { method: "update", state: {}, bufferPaths: [] },
buffers: [] as Base64String[],
});
// All calls should have the type field
const allCalls = mockBridge.mock.calls;
for (const call of allCalls) {
const payload = call[0].payload;
expect(payload).toHaveProperty("type");
expect(typeof payload.type).toBe("string");
}
});
});
});