/* Copyright 2026 Marimo. All rights reserved. */ import { render, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { TestUtils } from "@/__tests__/test-helpers"; import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener"; import { visibleForTesting } from "../AnyWidgetPlugin"; import { MODEL_MANAGER, Model } from "../model"; import type { WidgetModelId } from "../types"; import { BINDING_MANAGER } from "../widget-binding"; const { LoadedSlot } = visibleForTesting; // Helper to create typed model IDs for tests const asModelId = (id: string): WidgetModelId => id as WidgetModelId; // Mock a minimal AnyWidget implementation const mockWidget = { initialize: vi.fn(), render: vi.fn(), }; describe("LoadedSlot", () => { const modelId = asModelId("test-model-id"); let mockModel: Model<{ count: number }>; const mockProps = { widget: mockWidget, data: { jsUrl: "http://example.com/widget.js", jsHash: "abc123", modelId: modelId, }, host: document.createElement( "div", ) as unknown as HTMLElementNotDerivedFromRef, modelId: modelId, }; beforeEach(() => { vi.clearAllMocks(); // Create and register a mock model before each test mockModel = new Model( { count: 0 }, { sendUpdate: vi.fn().mockResolvedValue(undefined), sendCustomMessage: vi.fn().mockResolvedValue(undefined), }, ); MODEL_MANAGER.set(modelId, mockModel); }); afterEach(() => { BINDING_MANAGER.destroy(modelId); }); it("should render a div with ref", () => { const { container } = render(); expect(container.querySelector("div")).not.toBeNull(); }); it("should call runAnyWidgetModule on initialization", async () => { render(); // Wait a render await waitFor(() => { expect(mockWidget.render).toHaveBeenCalled(); }); }); it("should not remount when value update drops model_id", async () => { // Regression: when the frontend sends a state update (e.g. {zoom_level: 0}), // it overwrites the UIElement value that originally held {model_id: "..."}. // The key must stay stable because modelId comes from data, not value. const { container, rerender } = render(); await waitFor(() => { expect(mockWidget.render).toHaveBeenCalledTimes(1); }); const divBefore = container.querySelector("div"); // Simulate a value update that does NOT include model_id // (this is what happens when the widget sends trait state) rerender(); await waitFor(() => { // The div should be the same DOM node (no remount) expect(container.querySelector("div")).toBe(divBefore); // render should not be called again (no remount) expect(mockWidget.render).toHaveBeenCalledTimes(1); }); }); it("should re-run widget when widget prop changes", async () => { const { rerender } = render(); // Wait for initial render await waitFor(() => { expect(mockWidget.render).toHaveBeenCalled(); }); // Create a new widget mock const newMockWidget = { initialize: vi.fn(), render: vi.fn(), }; // Change the widget rerender(); await TestUtils.nextTick(); // Wait for re-render with new widget await waitFor(() => { expect(newMockWidget.render).toHaveBeenCalled(); }); }); });