@marimo-team/islands 0.23.3-dev4 → 0.23.3-dev41

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.
Files changed (37) hide show
  1. package/dist/{chat-ui-CTt4WX0V.js → chat-ui-DEd_Ndal.js} +82 -82
  2. package/dist/{html-to-image-BdsDysfl.js → html-to-image-DBosi5GK.js} +2243 -2217
  3. package/dist/main.js +1104 -1099
  4. package/dist/{process-output-COL2Pf5I.js → process-output-k-4WHpxz.js} +1 -1
  5. package/dist/{reveal-component-Cd5Y35Ny.js → reveal-component-agH2Be6_.js} +2 -2
  6. package/dist/{slide-BEerfanN.js → slide-CoAyRjHI.js} +693 -574
  7. package/package.json +2 -2
  8. package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +84 -2
  9. package/src/components/editor/file-tree/file-explorer.tsx +142 -203
  10. package/src/components/editor/file-tree/file-name-input.tsx +41 -0
  11. package/src/components/editor/file-tree/file-operations.tsx +266 -0
  12. package/src/components/editor/file-tree/renderers.tsx +1 -1
  13. package/src/components/editor/file-tree/requesting-tree.tsx +68 -49
  14. package/src/components/editor/output/JsonOutput.tsx +157 -4
  15. package/src/components/editor/output/__tests__/JsonOutput-mimetype.test.tsx +80 -0
  16. package/src/components/editor/output/__tests__/json-output.test.ts +147 -2
  17. package/src/components/home/state.ts +13 -1
  18. package/src/components/pages/home-page.tsx +116 -10
  19. package/src/core/islands/__tests__/bridge.test.ts +116 -5
  20. package/src/core/islands/bridge.ts +5 -1
  21. package/src/core/network/requests-network.ts +0 -3
  22. package/src/core/static/__tests__/export-context.test.ts +122 -0
  23. package/src/core/static/__tests__/static-state.test.ts +80 -0
  24. package/src/core/static/export-context.ts +84 -0
  25. package/src/core/static/static-state.ts +44 -6
  26. package/src/plugins/core/RenderHTML.tsx +23 -2
  27. package/src/plugins/core/__test__/RenderHTML.test.ts +86 -1
  28. package/src/plugins/core/__test__/trusted-url.test.ts +130 -18
  29. package/src/plugins/core/sanitize.ts +11 -5
  30. package/src/plugins/core/trusted-url.ts +32 -10
  31. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +29 -1
  32. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +34 -0
  33. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +35 -2
  34. package/src/utils/__tests__/path.test.ts +20 -0
  35. package/src/utils/pathUtils.test.ts +141 -1
  36. package/src/utils/pathUtils.ts +46 -0
  37. package/src/utils/paths.ts +9 -1
@@ -10,6 +10,7 @@ import { generateUUID } from "@/utils/uuid";
10
10
  import type { CommandMessage, NotificationPayload } from "../kernel/messages";
11
11
  import type { EditRequests, RunRequests } from "../network/types";
12
12
  import { store as defaultStore } from "../state/jotai";
13
+ import { getMarimoExportContext } from "../static/export-context";
13
14
  import { createMarimoFile, parseMarimoIslandApps } from "./parse";
14
15
  import { islandsInitializedAtom } from "./state";
15
16
  import type { WorkerSchema } from "./worker/worker";
@@ -123,8 +124,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
123
124
  `Starting sessions for ${apps.length} app(s):`,
124
125
  apps.map((a) => `${a.id} (${a.cells.length} cells)`),
125
126
  );
127
+ const exportContext =
128
+ apps.length === 1 ? getMarimoExportContext() : undefined;
129
+ const notebookCode = exportContext?.notebookCode;
126
130
  for (const app of apps) {
127
- const file = createMarimoFile(app);
131
+ const file = notebookCode || createMarimoFile(app);
128
132
  Logger.debug(`App ${app.id} marimo file:\n`, file);
129
133
  this.startSession({
130
134
  code: file,
@@ -305,7 +305,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
305
305
  .then(handleResponse);
306
306
  },
307
307
  sendDeleteFileOrFolder: async (request) => {
308
- await waitForConnectionOpen();
309
308
  return getClient()
310
309
  .POST("/api/files/delete", {
311
310
  body: request,
@@ -313,7 +312,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
313
312
  .then(handleResponse);
314
313
  },
315
314
  sendCopyFileOrFolder: async (request) => {
316
- await waitForConnectionOpen();
317
315
  return getClient()
318
316
  .POST("/api/files/copy", {
319
317
  body: request,
@@ -321,7 +319,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
321
319
  .then(handleResponse);
322
320
  },
323
321
  sendRenameFileOrFolder: async (request) => {
324
- await waitForConnectionOpen();
325
322
  return getClient()
326
323
  .POST("/api/files/move", {
327
324
  body: request,
@@ -0,0 +1,122 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import type { ExtractAtomValue } from "jotai";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
7
+ import { userConfigAtom } from "@/core/config/config";
8
+ import { parseUserConfig } from "@/core/config/config-schema";
9
+ import { initialModeAtom } from "@/core/mode";
10
+ import { store } from "@/core/state/jotai";
11
+ import {
12
+ getMarimoExportContext,
13
+ hasTrustedExportContext,
14
+ hasTrustedNotebookContext,
15
+ } from "../export-context";
16
+
17
+ type ExportContextWindow = Window & {
18
+ __MARIMO_EXPORT_CONTEXT__?: {
19
+ trusted: boolean;
20
+ notebookCode?: string;
21
+ };
22
+ };
23
+
24
+ function setAutoInstantiate(value: boolean) {
25
+ const cleared = parseUserConfig({});
26
+ store.set(userConfigAtom, {
27
+ ...cleared,
28
+ runtime: { ...cleared.runtime, auto_instantiate: value },
29
+ });
30
+ }
31
+
32
+ describe("hasTrustedNotebookContext", () => {
33
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
34
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
35
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
36
+ let w: ExportContextWindow;
37
+
38
+ beforeEach(() => {
39
+ w = window as ExportContextWindow;
40
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
41
+ previousConfig = store.get(userConfigAtom);
42
+ previousMode = store.get(initialModeAtom);
43
+ store.set(hasRunAnyCellAtom, false);
44
+ setAutoInstantiate(false);
45
+ store.set(initialModeAtom, "edit");
46
+ delete w.__MARIMO_EXPORT_CONTEXT__;
47
+ });
48
+
49
+ afterEach(() => {
50
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
51
+ store.set(userConfigAtom, previousConfig);
52
+ store.set(initialModeAtom, previousMode);
53
+ delete w.__MARIMO_EXPORT_CONTEXT__;
54
+ });
55
+
56
+ it("returns false in untrusted edit mode before interaction", () => {
57
+ expect(hasTrustedNotebookContext()).toBe(false);
58
+ });
59
+
60
+ it("returns true once the user has run a cell", () => {
61
+ store.set(hasRunAnyCellAtom, true);
62
+ expect(hasTrustedNotebookContext()).toBe(true);
63
+ });
64
+
65
+ it("returns true when a trusted export context is installed", () => {
66
+ w.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
67
+ expect(hasTrustedNotebookContext()).toBe(true);
68
+ });
69
+
70
+ it("returns true when auto_instantiate is enabled", () => {
71
+ setAutoInstantiate(true);
72
+ expect(hasTrustedNotebookContext()).toBe(true);
73
+ });
74
+
75
+ it("returns true in read mode", () => {
76
+ store.set(initialModeAtom, "read");
77
+ expect(hasTrustedNotebookContext()).toBe(true);
78
+ });
79
+
80
+ it("returns false if initialMode throws (config not yet applied)", () => {
81
+ store.set(initialModeAtom, undefined);
82
+ expect(hasTrustedNotebookContext()).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe("hasTrustedExportContext / getMarimoExportContext shape validation", () => {
87
+ let w: ExportContextWindow;
88
+
89
+ beforeEach(() => {
90
+ w = window as ExportContextWindow;
91
+ delete w.__MARIMO_EXPORT_CONTEXT__;
92
+ });
93
+
94
+ afterEach(() => {
95
+ delete w.__MARIMO_EXPORT_CONTEXT__;
96
+ });
97
+
98
+ it("accepts a valid context", () => {
99
+ w.__MARIMO_EXPORT_CONTEXT__ = { trusted: true, notebookCode: "x = 1" };
100
+ expect(hasTrustedExportContext()).toBe(true);
101
+ expect(getMarimoExportContext()).toEqual({
102
+ trusted: true,
103
+ notebookCode: "x = 1",
104
+ });
105
+ });
106
+
107
+ it("rejects a context where `trusted` is not exactly true", () => {
108
+ w.__MARIMO_EXPORT_CONTEXT__ = {
109
+ trusted: "yes" as unknown as true,
110
+ };
111
+ expect(hasTrustedExportContext()).toBe(false);
112
+ expect(getMarimoExportContext()).toBeUndefined();
113
+ });
114
+
115
+ it("rejects a context with non-string notebookCode", () => {
116
+ w.__MARIMO_EXPORT_CONTEXT__ = {
117
+ trusted: true,
118
+ notebookCode: 42 as unknown as string,
119
+ };
120
+ expect(getMarimoExportContext()).toBeUndefined();
121
+ });
122
+ });
@@ -0,0 +1,80 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import {
6
+ getStaticModelNotifications,
7
+ getStaticVirtualFiles,
8
+ isStaticNotebook,
9
+ } from "../static-state";
10
+
11
+ function setMarimoStatic(value: unknown): void {
12
+ (window as unknown as { __MARIMO_STATIC__?: unknown }).__MARIMO_STATIC__ =
13
+ value;
14
+ }
15
+
16
+ function clearMarimoStatic(): void {
17
+ delete (window as unknown as { __MARIMO_STATIC__?: unknown })
18
+ .__MARIMO_STATIC__;
19
+ }
20
+
21
+ describe("static-state shape validation", () => {
22
+ beforeEach(() => {
23
+ clearMarimoStatic();
24
+ });
25
+
26
+ afterEach(() => {
27
+ clearMarimoStatic();
28
+ });
29
+
30
+ it("treats an absent global as not-a-static-notebook", () => {
31
+ expect(isStaticNotebook()).toBe(false);
32
+ expect(getStaticModelNotifications()).toBeUndefined();
33
+ });
34
+
35
+ it("accepts a well-formed state", () => {
36
+ setMarimoStatic({
37
+ files: { "/@file/a.txt": "data:text/plain;base64,YQ==" },
38
+ modelNotifications: [],
39
+ });
40
+ expect(isStaticNotebook()).toBe(true);
41
+ expect(getStaticVirtualFiles()).toEqual({
42
+ "/@file/a.txt": "data:text/plain;base64,YQ==",
43
+ });
44
+ expect(getStaticModelNotifications()).toEqual([]);
45
+ });
46
+
47
+ it("rejects a malformed global (missing files)", () => {
48
+ setMarimoStatic({ modelNotifications: [] });
49
+ expect(isStaticNotebook()).toBe(false);
50
+ expect(getStaticModelNotifications()).toBeUndefined();
51
+ });
52
+
53
+ it("rejects a malformed global (non-array modelNotifications)", () => {
54
+ setMarimoStatic({ files: {}, modelNotifications: "oops" });
55
+ expect(isStaticNotebook()).toBe(false);
56
+ });
57
+
58
+ it("rejects a non-object global", () => {
59
+ setMarimoStatic("pwned");
60
+ expect(isStaticNotebook()).toBe(false);
61
+ });
62
+
63
+ it("rejects an array as the state", () => {
64
+ setMarimoStatic([]);
65
+ expect(isStaticNotebook()).toBe(false);
66
+ });
67
+
68
+ it("rejects files when it is an array", () => {
69
+ setMarimoStatic({ files: [], modelNotifications: [] });
70
+ expect(isStaticNotebook()).toBe(false);
71
+ });
72
+
73
+ it("rejects files that contain non-string values", () => {
74
+ setMarimoStatic({
75
+ files: { "/@file/a.txt": 42 },
76
+ modelNotifications: [],
77
+ });
78
+ expect(isStaticNotebook()).toBe(false);
79
+ });
80
+ });
@@ -0,0 +1,84 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
+ import { autoInstantiateAtom } from "@/core/config/config";
5
+ import { getInitialAppMode } from "@/core/mode";
6
+ import { store } from "@/core/state/jotai";
7
+
8
+ export interface MarimoExportContext {
9
+ trusted: true;
10
+ notebookCode?: string;
11
+ }
12
+
13
+ declare global {
14
+ interface Window {
15
+ __MARIMO_EXPORT_CONTEXT__?: Readonly<MarimoExportContext>;
16
+ }
17
+ }
18
+
19
+ function isMarimoExportContext(
20
+ value: unknown,
21
+ ): value is Readonly<MarimoExportContext> {
22
+ if (typeof value !== "object" || value === null) {
23
+ return false;
24
+ }
25
+
26
+ const candidate = value as MarimoExportContext;
27
+ if (candidate.trusted !== true) {
28
+ return false;
29
+ }
30
+ if (
31
+ candidate.notebookCode !== undefined &&
32
+ typeof candidate.notebookCode !== "string"
33
+ ) {
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+
39
+ export function getMarimoExportContext():
40
+ | Readonly<MarimoExportContext>
41
+ | undefined {
42
+ const context = window?.__MARIMO_EXPORT_CONTEXT__;
43
+ return isMarimoExportContext(context) ? context : undefined;
44
+ }
45
+
46
+ export function hasTrustedExportContext(): boolean {
47
+ return getMarimoExportContext()?.trusted === true;
48
+ }
49
+
50
+ /**
51
+ * True when the current page is a context where notebook-authored script
52
+ * execution is expected, and therefore the user has consented (explicitly or
53
+ * by the nature of the page) to running arbitrary notebook content:
54
+ *
55
+ * - the user has run at least one cell, OR
56
+ * - a first-party exported notebook page installed a trusted export context
57
+ * (islands / static exports / Quarto islands), OR
58
+ * - `auto_instantiate` is enabled (the notebook runs on page load by user
59
+ * configuration), OR
60
+ * - the page was loaded in `read` / app mode (served by marimo as an app).
61
+ *
62
+ * Edit mode before any user interaction is intentionally NOT trusted — that
63
+ * is the only surface where we must prevent notebook-authored content from
64
+ * loading scripts or bypassing HTML sanitization.
65
+ */
66
+ export function hasTrustedNotebookContext(): boolean {
67
+ if (store.get(hasRunAnyCellAtom)) {
68
+ return true;
69
+ }
70
+ if (hasTrustedExportContext()) {
71
+ return true;
72
+ }
73
+ if (store.get(autoInstantiateAtom)) {
74
+ return true;
75
+ }
76
+ try {
77
+ if (getInitialAppMode() === "read") {
78
+ return true;
79
+ }
80
+ } catch {
81
+ // getInitialAppMode throws before mount config is applied; treat as untrusted.
82
+ }
83
+ return false;
84
+ }
@@ -5,20 +5,58 @@ import type { MarimoStaticState, StaticVirtualFiles } from "./types";
5
5
 
6
6
  declare global {
7
7
  interface Window {
8
- __MARIMO_STATIC__?: MarimoStaticState;
8
+ __MARIMO_STATIC__?: Readonly<MarimoStaticState>;
9
9
  }
10
10
  }
11
11
 
12
+ function isStringToStringRecord(
13
+ value: unknown,
14
+ ): value is Record<string, string> {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
16
+ return false;
17
+ }
18
+ for (const entry of Object.values(value)) {
19
+ if (typeof entry !== "string") {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+
26
+ function isMarimoStaticState(
27
+ value: unknown,
28
+ ): value is Readonly<MarimoStaticState> {
29
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
30
+ return false;
31
+ }
32
+ const candidate = value as MarimoStaticState;
33
+ if (!isStringToStringRecord(candidate.files)) {
34
+ return false;
35
+ }
36
+ if (
37
+ candidate.modelNotifications !== undefined &&
38
+ !Array.isArray(candidate.modelNotifications)
39
+ ) {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ function getMarimoStaticState(): Readonly<MarimoStaticState> | undefined {
46
+ const state = window?.__MARIMO_STATIC__;
47
+ return isMarimoStaticState(state) ? state : undefined;
48
+ }
49
+
12
50
  export function isStaticNotebook(): boolean {
13
- return window?.__MARIMO_STATIC__ !== undefined;
51
+ return getMarimoStaticState() !== undefined;
14
52
  }
15
53
 
16
54
  export function getStaticVirtualFiles(): StaticVirtualFiles {
17
- invariant(window.__MARIMO_STATIC__ !== undefined, "Not a static notebook");
18
-
19
- return window.__MARIMO_STATIC__.files;
55
+ const state = getMarimoStaticState();
56
+ invariant(state !== undefined, "Not a static notebook");
57
+ return state.files;
20
58
  }
21
59
 
22
60
  export function getStaticModelNotifications(): ModelLifecycle[] | undefined {
23
- return window?.__MARIMO_STATIC__?.modelNotifications;
61
+ return getMarimoStaticState()?.modelNotifications;
24
62
  }
@@ -16,6 +16,8 @@ import { CopyClipboardIcon } from "@/components/icons/copy-icon";
16
16
  import { QueryParamPreservingLink } from "@/components/ui/query-param-preserving-link";
17
17
  import { Tooltip } from "@/components/ui/tooltip";
18
18
  import { DocHoverTarget } from "@/core/documentation/DocHoverTarget";
19
+ import { hasTrustedNotebookContext } from "@/core/static/export-context";
20
+ import { Logger } from "@/utils/Logger";
19
21
  import { sanitizeHtml, useSanitizeHtml } from "./sanitize";
20
22
 
21
23
  type ReplacementFn = NonNullable<HTMLReactParserOptions["replace"]>;
@@ -98,8 +100,27 @@ const replaceSrcScripts = (domNode: DOMNode): JSX.Element | undefined => {
98
100
  if (!src) {
99
101
  return;
100
102
  }
101
- // Check if script already exists
102
- if (!document.querySelector(`script[src="${src}"]`)) {
103
+ // Only append notebook-authored scripts when the page is a trusted
104
+ // context (the user has run a cell, the page is a trusted export, or
105
+ // we're running in read/app mode). In untrusted edit mode before any
106
+ // user interaction, drop the script and log a warning. Outer
107
+ // sanitization will normally strip <script> tags already; this is
108
+ // defense-in-depth for flows that reparse children with
109
+ // alwaysSanitizeHtml: false (see registerReactComponent.getChildren).
110
+ if (!hasTrustedNotebookContext()) {
111
+ Logger.warn(
112
+ `[RenderHTML] refusing <script src> in untrusted context: ${src}`,
113
+ );
114
+ // oxlint-disable-next-line react/jsx-no-useless-fragment
115
+ return <></>;
116
+ }
117
+ // Check if script already exists. Avoid building a CSS selector from
118
+ // notebook-provided input, which can throw for valid URLs containing
119
+ // selector-significant characters (e.g. IPv6 hosts with `[`/`]`).
120
+ const scriptExists = [...document.querySelectorAll("script[src]")].some(
121
+ (existingScript) => existingScript.getAttribute("src") === src,
122
+ );
123
+ if (!scriptExists) {
103
124
  const script = document.createElement("script");
104
125
  script.src = src;
105
126
  document.head.append(script);
@@ -1,5 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { describe, expect, test } from "vitest";
2
+ import type { ExtractAtomValue } from "jotai";
3
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
4
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
5
+ import { userConfigAtom } from "@/core/config/config";
6
+ import { parseUserConfig } from "@/core/config/config-schema";
7
+ import { initialModeAtom } from "@/core/mode";
8
+ import { store } from "@/core/state/jotai";
3
9
  import { visibleForTesting } from "../RenderHTML";
4
10
 
5
11
  const { parseHtml } = visibleForTesting;
@@ -197,6 +203,85 @@ describe("parseHtml", () => {
197
203
  });
198
204
  });
199
205
 
206
+ describe("replaceSrcScripts trust gate", () => {
207
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
208
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
209
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
210
+ const windowWithExport = window as Window & {
211
+ __MARIMO_EXPORT_CONTEXT__?: unknown;
212
+ };
213
+
214
+ function clearTrustSignals() {
215
+ const cleared = parseUserConfig({});
216
+ store.set(hasRunAnyCellAtom, false);
217
+ store.set(userConfigAtom, {
218
+ ...cleared,
219
+ runtime: { ...cleared.runtime, auto_instantiate: false },
220
+ });
221
+ store.set(initialModeAtom, "edit");
222
+ delete windowWithExport.__MARIMO_EXPORT_CONTEXT__;
223
+ }
224
+
225
+ beforeEach(() => {
226
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
227
+ previousConfig = store.get(userConfigAtom);
228
+ previousMode = store.get(initialModeAtom);
229
+ clearTrustSignals();
230
+ for (const s of document.head.querySelectorAll(
231
+ 'script[src^="https://cdn.example.com/"]',
232
+ )) {
233
+ s.remove();
234
+ }
235
+ });
236
+
237
+ afterEach(() => {
238
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
239
+ store.set(userConfigAtom, previousConfig);
240
+ store.set(initialModeAtom, previousMode);
241
+ delete windowWithExport.__MARIMO_EXPORT_CONTEXT__;
242
+ for (const s of document.head.querySelectorAll(
243
+ 'script[src^="https://cdn.example.com/"]',
244
+ )) {
245
+ s.remove();
246
+ }
247
+ });
248
+
249
+ test("drops <script src> in untrusted edit mode", () => {
250
+ parseHtml({
251
+ html: '<script src="https://cdn.example.com/unrun.js"></script>',
252
+ });
253
+ expect(
254
+ document.head.querySelector(
255
+ 'script[src="https://cdn.example.com/unrun.js"]',
256
+ ),
257
+ ).toBeNull();
258
+ });
259
+
260
+ test("loads <script src> once a cell has been run", () => {
261
+ store.set(hasRunAnyCellAtom, true);
262
+ parseHtml({
263
+ html: '<script src="https://cdn.example.com/ok.js"></script>',
264
+ });
265
+ expect(
266
+ document.head.querySelector(
267
+ 'script[src="https://cdn.example.com/ok.js"]',
268
+ ),
269
+ ).not.toBeNull();
270
+ });
271
+
272
+ test("loads <script src> when a trusted export context is installed", () => {
273
+ windowWithExport.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
274
+ parseHtml({
275
+ html: '<script src="https://cdn.example.com/export.js"></script>',
276
+ });
277
+ expect(
278
+ document.head.querySelector(
279
+ 'script[src="https://cdn.example.com/export.js"]',
280
+ ),
281
+ ).not.toBeNull();
282
+ });
283
+ });
284
+
200
285
  describe("wrapTooltipTargets", () => {
201
286
  test("data-tooltip wraps element in Tooltip component", () => {
202
287
  const html = '<span data-tooltip="Hello world">Hover me</span>';