@marimo-team/islands 0.23.15-dev63 → 0.23.15-dev65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import { dt as kioskModeAtom, ht as outputIsStale } from "./html-to-image-DDENewl2.js";
10
10
  import "./chunk-5FQGJX7Z-B9WoXK1R.js";
11
11
  import { u as createLucideIcon } from "./dist-B96tMAst.js";
12
- import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-yIEEWI5w.js";
12
+ import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-CSV-51WI.js";
13
13
  import { X as useDebouncedCallback } from "./input-DStQIuqD.js";
14
14
  import "./toDate-B3IRfHhw.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.15-dev63",
3
+ "version": "0.23.15-dev65",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,61 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createStore } from "jotai";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { RuntimeState } from "@/core/kernel/RuntimeState";
6
+ import { initializeIslands } from "../bootstrap";
7
+ import type { IslandsPyodideBridge } from "../bridge";
8
+ import { islandsHydratedAtom, islandsPendingInitialRunsAtom } from "../state";
9
+ import type { IslandsKernelMessage } from "../worker/worker";
10
+
11
+ function completedRun(sessionGeneration: number): IslandsKernelMessage {
12
+ return {
13
+ sessionGeneration,
14
+ message: JSON.stringify({
15
+ op: "completed-run",
16
+ data: { op: "completed-run", run_id: null },
17
+ }) as IslandsKernelMessage["message"],
18
+ };
19
+ }
20
+
21
+ describe("initializeIslands", () => {
22
+ afterEach(vi.restoreAllMocks);
23
+
24
+ it("waits for each initial session to complete", async () => {
25
+ const root = document.createElement("div");
26
+ root.innerHTML =
27
+ '<marimo-island data-app-id="app-1" data-reactive="true"></marimo-island>';
28
+
29
+ let consumeMessage: ((message: IslandsKernelMessage) => void) | undefined;
30
+ const bridge = {
31
+ consumeMessages: (consumer: (message: IslandsKernelMessage) => void) => {
32
+ consumeMessage = consumer;
33
+ },
34
+ sendComponentValues: vi.fn(),
35
+ } as unknown as IslandsPyodideBridge;
36
+ const store = createStore();
37
+ vi.spyOn(RuntimeState.INSTANCE, "start").mockImplementation(
38
+ () => undefined,
39
+ );
40
+
41
+ await initializeIslands({
42
+ bridge,
43
+ store,
44
+ root,
45
+ autoInitializePlugins: false,
46
+ });
47
+
48
+ expect(store.get(islandsHydratedAtom)).toBe(false);
49
+ store.set(islandsPendingInitialRunsAtom, new Set([1, 2]));
50
+
51
+ consumeMessage?.(completedRun(1));
52
+ expect(store.get(islandsHydratedAtom)).toBe(false);
53
+
54
+ consumeMessage?.(completedRun(1));
55
+ consumeMessage?.(completedRun(0));
56
+ expect(store.get(islandsHydratedAtom)).toBe(false);
57
+
58
+ consumeMessage?.(completedRun(2));
59
+ expect(store.get(islandsHydratedAtom)).toBe(true);
60
+ });
61
+ });
@@ -8,6 +8,8 @@ import {
8
8
  uiElementId,
9
9
  widgetModelId,
10
10
  } from "@/__tests__/branded";
11
+ import { notebookAtom } from "@/core/cells/cells";
12
+ import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
11
13
 
12
14
  type Base64String = components["schemas"]["Base64String"];
13
15
  interface TestIslandApp {
@@ -61,7 +63,12 @@ const {
61
63
  mockStartSessionRequest: vi.fn(),
62
64
  mockMessageListeners: new Map<string, (payload: never) => void>(),
63
65
  mockStoreSet: vi.fn(),
64
- mockParseMarimoIslandApps: vi.fn<() => TestIslandApp[]>(() => []),
66
+ mockParseMarimoIslandApps: vi.fn<
67
+ (
68
+ root?: Document | Element,
69
+ options?: { materialize?: boolean },
70
+ ) => TestIslandApp[]
71
+ >(() => []),
65
72
  mockCreateMarimoFile: vi.fn(),
66
73
  mockGetMarimoExportContext: vi.fn<() => TestExportContext | undefined>(
67
74
  () => undefined,
@@ -235,6 +242,10 @@ describe("IslandsPyodideBridge", () => {
235
242
  code: "generated app 2",
236
243
  sessionGeneration: 2,
237
244
  });
245
+ expect(mockStoreSet).toHaveBeenCalledWith(
246
+ islandsPendingInitialRunsAtom,
247
+ new Set([1, 2]),
248
+ );
238
249
 
239
250
  await sendSlider();
240
251
  await bridge.stopSession();
@@ -309,7 +320,17 @@ describe("IslandsPyodideBridge", () => {
309
320
  sessionGeneration: 1,
310
321
  }),
311
322
  );
323
+ const events: string[] = [];
312
324
  mockCreateMarimoFile.mockReturnValue("generated app 2");
325
+ mockParseMarimoIslandApps.mockImplementation((_root, options) => {
326
+ events.push(options?.materialize === false ? "inspect" : "materialize");
327
+ return [app()];
328
+ });
329
+ mockStoreSet.mockImplementation((target) => {
330
+ if (target === islandsPendingInitialRunsAtom) {
331
+ events.push("reset");
332
+ }
333
+ });
313
334
  await bridge.initializeApps();
314
335
 
315
336
  expect(mockReplaceSessionRequest).toHaveBeenCalledOnce();
@@ -318,12 +339,17 @@ describe("IslandsPyodideBridge", () => {
318
339
  code: "generated app 2",
319
340
  sessionGeneration: 2,
320
341
  });
321
- expect(mockStoreSet).toHaveBeenCalledOnce();
342
+ expect(mockStoreSet).toHaveBeenCalledWith(
343
+ islandsPendingInitialRunsAtom,
344
+ new Set([2]),
345
+ );
346
+ expect(events).toEqual(["inspect", "reset", "materialize"]);
322
347
  });
323
348
 
324
349
  it("stops the matching active app", async () => {
325
350
  mockSingleApp();
326
351
  await bridge.initializeApps();
352
+ mockStoreSet.mockClear();
327
353
 
328
354
  await bridge.stopSession("other-app");
329
355
  await bridge.stopSession("app-1");
@@ -333,6 +359,11 @@ describe("IslandsPyodideBridge", () => {
333
359
  appId: "app-1",
334
360
  sessionGeneration: 1,
335
361
  });
362
+ expect(mockStoreSet.mock.calls[0]).toEqual([
363
+ islandsPendingInitialRunsAtom,
364
+ null,
365
+ ]);
366
+ expect(mockStoreSet.mock.calls[1]?.[0]).toBe(notebookAtom);
336
367
 
337
368
  mockBridge.mockClear();
338
369
  const control = sendSlider();
@@ -42,6 +42,7 @@ import { store as defaultStore } from "../state/jotai";
42
42
  import type { IslandsPyodideBridge } from "./bridge";
43
43
  import { MarimoIslandElement } from "./components/web-components";
44
44
  import {
45
+ islandsPendingInitialRunsAtom,
45
46
  shouldShowIslandsWarningIndicatorAtom,
46
47
  userTriedToInteractWithIslandsAtom,
47
48
  } from "./state";
@@ -74,6 +75,7 @@ export async function initializeIslands(
74
75
  // Setup networking
75
76
  store.set(requestClientAtom, bridge);
76
77
  store.set(initialModeAtom, "read");
78
+ store.set(islandsPendingInitialRunsAtom, null);
77
79
 
78
80
  // Initialize plugins for rendering static HTML
79
81
  if (config.autoInitializePlugins !== false) {
@@ -129,10 +131,13 @@ export async function initializeIslands(
129
131
  // the envelope and the payload carry the op. The bridge types the message
130
132
  // as NotificationPayload (just {data}), but the actual wire format
131
133
  // includes the outer op too.
132
- bridge.consumeMessages((message) => {
134
+ // Worker metadata identifies the session that emitted the message.
135
+ bridge.consumeMessages(({ message, sessionGeneration }) => {
133
136
  handleMessage(
134
137
  message as unknown as JsonString<IslandsNotificationMessage>,
135
138
  actions,
139
+ store,
140
+ sessionGeneration,
136
141
  );
137
142
  });
138
143
 
@@ -161,9 +166,12 @@ type IslandsNotificationMessage = {
161
166
  *
162
167
  * Wire format from Python: {"op": "<name>", "data": {"op": "<name>", ...}}
163
168
  */
169
+ // oxlint-disable-next-line marimo/prefer-object-params
164
170
  function handleMessage(
165
171
  message: JsonString<IslandsNotificationMessage>,
166
172
  actions: NotebookActions,
173
+ store: Store,
174
+ sessionGeneration: number,
167
175
  ): void {
168
176
  try {
169
177
  const msg = jsonParseWithSpecialChar(message);
@@ -192,7 +200,6 @@ function handleMessage(
192
200
  case "storage-download-ready":
193
201
  case "secret-keys-result":
194
202
  case "startup-logs":
195
- case "completed-run":
196
203
  case "interrupted":
197
204
  case "reconnected":
198
205
  case "cache-cleared":
@@ -201,6 +208,17 @@ function handleMessage(
201
208
  case "notebook-document-transaction":
202
209
  return;
203
210
 
211
+ case "completed-run":
212
+ store.set(islandsPendingInitialRunsAtom, (pending) => {
213
+ if (!pending?.has(sessionGeneration)) {
214
+ return pending;
215
+ }
216
+ const next = new Set(pending);
217
+ next.delete(sessionGeneration);
218
+ return next;
219
+ });
220
+ return;
221
+
204
222
  case "kernel-ready":
205
223
  handleKernelReady(msg.data, {
206
224
  autoInstantiate: true,
@@ -4,17 +4,16 @@
4
4
  import { getWorkerRPC } from "@/core/wasm/rpc";
5
5
  import { Deferred } from "@/utils/Deferred";
6
6
  import { throwNotImplemented } from "@/utils/functions";
7
- import type { JsonString } from "@/utils/json/base64";
8
7
  import { Logger } from "@/utils/Logger";
9
8
  import { generateUUID } from "@/utils/uuid";
10
9
  import { initialNotebookState, notebookAtom } from "../cells/cells";
11
- import type { CommandMessage, NotificationPayload } from "../kernel/messages";
10
+ import type { CommandMessage } from "../kernel/messages";
12
11
  import type { EditRequests, RunRequests } from "../network/types";
13
12
  import { store as defaultStore } from "../state/jotai";
14
13
  import { getMarimoExportContext } from "../static/export-context";
15
14
  import { createMarimoFile, parseMarimoIslandApps } from "./parse";
16
- import { islandsInitializedAtom } from "./state";
17
- import type { WorkerSchema } from "./worker/worker";
15
+ import { islandsInitializedAtom, islandsPendingInitialRunsAtom } from "./state";
16
+ import type { IslandsKernelMessage, WorkerSchema } from "./worker/worker";
18
17
  import type { WorkerFactory } from "./worker-factory";
19
18
  import { DefaultWorkerFactory } from "./worker-factory";
20
19
 
@@ -60,7 +59,7 @@ interface AppSession {
60
59
  export class IslandsPyodideBridge implements RunRequests, EditRequests {
61
60
  private rpc: ReturnType<typeof getWorkerRPC<WorkerSchema>>;
62
61
  private messageConsumer:
63
- | ((message: JsonString<NotificationPayload>) => void)
62
+ | ((message: IslandsKernelMessage) => void)
64
63
  | undefined;
65
64
  private readonly store: typeof defaultStore;
66
65
  private readonly root: Document | Element;
@@ -110,12 +109,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
110
109
  },
111
110
  );
112
111
 
113
- this.rpc.addMessageListener(
114
- "kernelMessage",
115
- ({ message }: { message: JsonString<NotificationPayload> }) => {
116
- this.messageConsumer?.(message);
117
- },
118
- );
112
+ this.rpc.addMessageListener("kernelMessage", (message) => {
113
+ this.messageConsumer?.(message);
114
+ });
119
115
  }
120
116
 
121
117
  async initializeApps(): Promise<void> {
@@ -126,7 +122,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
126
122
  }
127
123
 
128
124
  private async startApps(): Promise<void> {
129
- const apps = parseMarimoIslandApps(this.root);
125
+ const apps = parseMarimoIslandApps(this.root, { materialize: false });
130
126
  const managesSingleApp = apps.length === 1;
131
127
  Logger.debug(
132
128
  `Starting sessions for ${apps.length} app(s):`,
@@ -140,15 +136,27 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
140
136
  ? getMarimoExportContext()
141
137
  : undefined;
142
138
  const notebookCode = exportContext?.notebookCode;
139
+ const singleApp = managesSingleApp ? apps[0] : undefined;
140
+ const singleAppFile = singleApp
141
+ ? notebookCode || createMarimoFile(singleApp)
142
+ : undefined;
143
+ if (
144
+ singleApp &&
145
+ this.session?.appId === singleApp.id &&
146
+ this.session.code === singleAppFile
147
+ ) {
148
+ parseMarimoIslandApps(this.root);
149
+ return;
150
+ }
151
+ if (apps.length > 0) {
152
+ this.store.set(
153
+ islandsPendingInitialRunsAtom,
154
+ new Set(apps.map((_, index) => this.nextSessionGeneration + index + 1)),
155
+ );
156
+ }
157
+ parseMarimoIslandApps(this.root);
143
158
  for (const app of apps) {
144
- const file = notebookCode || createMarimoFile(app);
145
- if (
146
- managesSingleApp &&
147
- this.session?.appId === app.id &&
148
- this.session.code === file
149
- ) {
150
- return;
151
- }
159
+ const file = singleAppFile || createMarimoFile(app);
152
160
  Logger.debug(`App ${app.id} marimo file:\n`, file);
153
161
  const request = {
154
162
  code: file,
@@ -199,6 +207,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
199
207
  appId: session.appId,
200
208
  sessionGeneration: session.sessionGeneration,
201
209
  });
210
+ this.store.set(islandsPendingInitialRunsAtom, null);
202
211
  this.session = undefined;
203
212
  this.sessionReady = new Deferred<AppSession>();
204
213
  this.store.set(notebookAtom, initialNotebookState());
@@ -225,9 +234,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
225
234
  /**
226
235
  * Sets up a consumer for kernel messages
227
236
  */
228
- consumeMessages(
229
- consumer: (message: JsonString<NotificationPayload>) => void,
230
- ): void {
237
+ consumeMessages(consumer: (message: IslandsKernelMessage) => void): void {
231
238
  this.messageConsumer = consumer;
232
239
  this.rpc.proxy.send.consumerReady({});
233
240
  }
@@ -19,6 +19,7 @@ import {
19
19
  ISLAND_TAG_NAMES,
20
20
  ISLANDS_JSON_SCRIPT_TYPE,
21
21
  } from "@/core/islands/constants";
22
+ import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
22
23
  import { requestClientAtom } from "@/core/network/requests";
23
24
  import { store } from "@/core/state/jotai";
24
25
  import { parseMarimoIslandApps } from "../../parse";
@@ -37,6 +38,7 @@ afterAll(() => {
37
38
 
38
39
  beforeEach(() => {
39
40
  store.set(notebookAtom, MockNotebook.notebookState({ cellData: {} }));
41
+ store.set(islandsPendingInitialRunsAtom, new Set());
40
42
  store.set(requestClientAtom, MockRequestClient.create());
41
43
  });
42
44
 
@@ -46,6 +48,38 @@ afterEach(async () => {
46
48
  });
47
49
 
48
50
  describe("MarimoIslandElement lifecycle", () => {
51
+ it("keeps static output until the initial run completes", async () => {
52
+ store.set(islandsPendingInitialRunsAtom, new Set([1]));
53
+ setNotebook("runtime-cell", "runtime output");
54
+ const container = document.createElement("div");
55
+ container.innerHTML = `
56
+ <marimo-island
57
+ data-app-id="app-1"
58
+ data-cell-id="runtime-cell"
59
+ data-cell-idx="0"
60
+ data-reactive="true"
61
+ >
62
+ <marimo-cell-output><div>static output</div></marimo-cell-output>
63
+ <marimo-cell-code hidden>${encodeURIComponent("value = 1")}</marimo-cell-code>
64
+ </marimo-island>
65
+ `;
66
+
67
+ await actAndFlush(() => document.body.append(container));
68
+
69
+ const island = container.querySelector<HTMLElement>(
70
+ ISLAND_TAG_NAMES.ISLAND,
71
+ );
72
+ expect(island?.textContent).toContain("static output");
73
+ expect(island?.textContent).not.toContain("runtime output");
74
+
75
+ await actAndFlush(() => {
76
+ store.set(islandsPendingInitialRunsAtom, new Set());
77
+ });
78
+
79
+ expect(island?.textContent).toContain("runtime output");
80
+ expect(island?.textContent).not.toContain("static output");
81
+ });
82
+
49
83
  it("binds a reactive island after its runtime cell index is materialized", async () => {
50
84
  setNotebook("outgoing-cell", "outgoing runtime output");
51
85
  const container = document.createElement("div");
@@ -15,6 +15,7 @@ import type { CellId } from "@/core/cells/ids";
15
15
  import { isOutputEmpty } from "@/core/cells/outputs";
16
16
  import type { CellRuntimeState } from "@/core/cells/types";
17
17
  import { ISLAND_TAG_NAMES } from "../constants";
18
+ import { islandsHydratedAtom } from "../state";
18
19
  import { IslandControls } from "./IslandControls";
19
20
  import { useIslandControls } from "./useIslandControls";
20
21
 
@@ -64,6 +65,7 @@ export const MarimoOutputWrapper: React.FC<MarimoOutputWrapperProps> = ({
64
65
  [cellId],
65
66
  );
66
67
  const runtime = useAtomValue(selectAtom(notebookAtom, selector));
68
+ const isHydrated = useAtomValue(islandsHydratedAtom);
67
69
 
68
70
  // Sync cell status to the host <marimo-island> element as a data attribute
69
71
  // so downstream consumers can style based on status (e.g. [data-status="running"])
@@ -71,7 +73,8 @@ export const MarimoOutputWrapper: React.FC<MarimoOutputWrapperProps> = ({
71
73
  useSyncStatusToIsland(wrapperRef, status);
72
74
 
73
75
  // If no runtime yet, show static content
74
- if (!runtime?.output) {
76
+ // Keep static content mounted while the initial run is pending.
77
+ if (!isHydrated || !runtime?.output) {
75
78
  return (
76
79
  <div ref={wrapperRef} className="relative min-h-6 empty:hidden">
77
80
  {children}
@@ -11,6 +11,15 @@ import { atom } from "jotai";
11
11
  */
12
12
  export const islandsInitializedAtom = atom<boolean | string>(false);
13
13
 
14
+ /** Pending initial session generations. Null waits for a session to start. */
15
+ export const islandsPendingInitialRunsAtom = atom<ReadonlySet<number> | null>(
16
+ new Set<number>(),
17
+ );
18
+
19
+ export const islandsHydratedAtom = atom(
20
+ (get) => get(islandsPendingInitialRunsAtom)?.size === 0,
21
+ );
22
+
14
23
  /**
15
24
  * Whether the user has tried to interact with the islands.
16
25
  */
@@ -45,11 +45,14 @@ async function loadPyodideAndPackages() {
45
45
  }
46
46
 
47
47
  const pyodideReadyPromise = loadPyodideAndPackages();
48
- const messageBuffer = new MessageBuffer(
49
- (message: JsonString<NotificationPayload>) => {
50
- rpc.send.kernelMessage({ message });
51
- },
52
- );
48
+ export interface IslandsKernelMessage {
49
+ message: JsonString<NotificationPayload>;
50
+ sessionGeneration: number;
51
+ }
52
+
53
+ const messageBuffer = new MessageBuffer<IslandsKernelMessage>((payload) => {
54
+ rpc.send.kernelMessage(payload);
55
+ });
53
56
  interface SessionRequest {
54
57
  code: string;
55
58
  appId: string;
@@ -78,7 +81,12 @@ async function startSession(
78
81
  });
79
82
  const nextBridge = await self.controller.startSession({
80
83
  ...notebook,
81
- onMessage: messageBuffer.push,
84
+ onMessage: (message) => {
85
+ messageBuffer.push({
86
+ message,
87
+ sessionGeneration: opts.sessionGeneration,
88
+ });
89
+ },
82
90
  });
83
91
  if (activeSession) {
84
92
  (nextBridge as unknown as PyProxy).destroy();
@@ -236,7 +244,7 @@ export type WorkerSchema = RPCSchema<
236
244
  // Emitted when the worker is ready
237
245
  ready: {};
238
246
  // Emitted when the kernel sends a message
239
- kernelMessage: { message: JsonString<NotificationPayload> };
247
+ kernelMessage: IslandsKernelMessage;
240
248
  // Emitted when the Pyodide is initialized
241
249
  initialized: {};
242
250
  // Emitted when the Pyodide fails to initialize
@@ -1,7 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { useAtomValue } from "jotai";
3
4
  import { useEffect, useRef, useState } from "react";
4
5
  import { z } from "zod";
6
+ import { islandsHydratedAtom } from "@/core/islands/state";
5
7
  import { createPlugin } from "@/plugins/core/builder";
6
8
  import type { IPluginProps } from "@/plugins/types";
7
9
  import { ErrorBanner } from "../common/error-banner";
@@ -46,8 +48,12 @@ const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
46
48
  const { modelId } = props.data;
47
49
  const htmlRef = useRef<HTMLDivElement>(null);
48
50
  const [error, setError] = useState<Error>();
51
+ const canCreateView = useAtomValue(islandsHydratedAtom);
49
52
 
50
53
  useEffect(() => {
54
+ if (!canCreateView) {
55
+ return;
56
+ }
51
57
  const el = htmlRef.current;
52
58
  if (!el) {
53
59
  return;
@@ -64,7 +70,7 @@ const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
64
70
  }
65
71
  });
66
72
  return () => controller.abort();
67
- }, [modelId]);
73
+ }, [canCreateView, modelId]);
68
74
 
69
75
  return (
70
76
  <>
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { render, waitFor } from "@testing-library/react";
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import { Provider, createStore } from "jotai";
4
5
  import {
5
6
  afterEach,
6
7
  beforeEach,
@@ -10,6 +11,7 @@ import {
10
11
  type MockInstance,
11
12
  vi,
12
13
  } from "vitest";
14
+ import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
13
15
  import type { IPluginProps } from "@/plugins/types";
14
16
  import { visibleForTesting } from "../AnyWidgetPlugin";
15
17
  import { Model } from "../model";
@@ -88,6 +90,29 @@ describe("AnyWidgetSlot", () => {
88
90
  });
89
91
  });
90
92
 
93
+ it("waits for islands hydration before resolving the model", async () => {
94
+ const islandsStore = createStore();
95
+ islandsStore.set(islandsPendingInitialRunsAtom, new Set([1]));
96
+ const createViewSpy = vi.spyOn(WIDGET_REGISTRY, "createView");
97
+
98
+ render(
99
+ <Provider store={islandsStore}>
100
+ <AnyWidgetSlot {...props(modelId)} />
101
+ </Provider>,
102
+ );
103
+
104
+ expect(createViewSpy).not.toHaveBeenCalled();
105
+
106
+ act(() => {
107
+ islandsStore.set(islandsPendingInitialRunsAtom, new Set());
108
+ });
109
+
110
+ await waitFor(() => {
111
+ expect(createViewSpy).toHaveBeenCalledOnce();
112
+ });
113
+ createViewSpy.mockRestore();
114
+ });
115
+
91
116
  it("aborts the runtime-owned view on unmount", async () => {
92
117
  const { unmount } = render(<AnyWidgetSlot {...props(modelId)} />);
93
118
  await waitFor(() => {