@marimo-team/islands 0.23.17-dev1 → 0.23.17-dev4
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.
- package/dist/{common-BGCQJb-W.js → common-BcQ4kHDP.js} +4 -4
- package/dist/main.js +2 -2
- package/dist/{reveal-component-NNi374so.js → reveal-component-9QGZq7uF.js} +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +1 -1
- package/src/components/editor/actions/export-dialog/__tests__/export-command.test.ts +115 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +423 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-notebook.test.ts +178 -0
- package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +234 -0
- package/src/components/editor/actions/export-dialog/__tests__/use-export-dialog.test.tsx +287 -0
- package/src/components/editor/actions/export-dialog/export-command.ts +149 -0
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +190 -0
- package/src/components/editor/actions/export-dialog/export-notebook.ts +98 -0
- package/src/components/editor/actions/export-dialog/format-notice.tsx +138 -0
- package/src/components/editor/actions/export-dialog/format-options.tsx +531 -0
- package/src/components/editor/actions/export-dialog/state.ts +321 -0
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +380 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +1 -21
- package/src/components/editor/actions/useNotebookActions.tsx +72 -170
- package/src/components/editor/connections/add-connection-dialog.tsx +1 -1
- package/src/components/editor/connections/quick-add-data-sources.tsx +14 -7
- package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +187 -0
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
- package/src/utils/__tests__/download.test.tsx +6 -4
- package/src/utils/download.ts +3 -1
- package/src/utils/shell.ts +17 -0
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { getExportCommand } from "../export-command";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_EXPORT_OPTIONS,
|
|
7
|
+
type ExportFormat,
|
|
8
|
+
type ExportOptions,
|
|
9
|
+
} from "../state";
|
|
10
|
+
|
|
11
|
+
function options(overrides: Partial<ExportOptions> = {}): ExportOptions {
|
|
12
|
+
return {
|
|
13
|
+
...DEFAULT_EXPORT_OPTIONS,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function command(
|
|
19
|
+
format: ExportFormat,
|
|
20
|
+
filename: string | null,
|
|
21
|
+
overrides: Partial<ExportOptions> = {},
|
|
22
|
+
) {
|
|
23
|
+
return getExportCommand({ format, filename, options: options(overrides) });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("getExportCommand", () => {
|
|
27
|
+
it("includes the selected HTML code setting and derived output path", () => {
|
|
28
|
+
expect(
|
|
29
|
+
command("html", "/tmp/my notebook.py", {
|
|
30
|
+
html: { includeCode: false },
|
|
31
|
+
}),
|
|
32
|
+
).toBe(
|
|
33
|
+
"marimo export html '/tmp/my notebook.py' --no-include-code -o '/tmp/my notebook.html'",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses the selected Markdown flavor and its extension", () => {
|
|
38
|
+
expect(
|
|
39
|
+
command("markdown", "report.py", {
|
|
40
|
+
markdown: { flavor: "mystmd" },
|
|
41
|
+
}),
|
|
42
|
+
).toBe("marimo export md report.py --flavor=mystmd -o report.myst.md");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
["report.md", "report.export.md"],
|
|
47
|
+
["report.markdown", "report.md"],
|
|
48
|
+
["report.qmd", "report.export.qmd"],
|
|
49
|
+
["report.myst.md", "report.export.myst.md"],
|
|
50
|
+
["report.mdx", "report.export.mdx"],
|
|
51
|
+
])(
|
|
52
|
+
"keeps the automatic Markdown output distinct from %s",
|
|
53
|
+
(filename, output) => {
|
|
54
|
+
expect(command("markdown", filename)).toBe(
|
|
55
|
+
`marimo export md ${filename} -o ${output}`,
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
it("includes every configurable document PDF setting", () => {
|
|
61
|
+
expect(
|
|
62
|
+
command("pdf", "report.py", {
|
|
63
|
+
pdf: {
|
|
64
|
+
preset: "document",
|
|
65
|
+
includeInputs: false,
|
|
66
|
+
includeOutputs: false,
|
|
67
|
+
webpdf: false,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
).toBe(
|
|
71
|
+
"marimo export pdf report.py --as=document --no-include-inputs --no-include-outputs --no-webpdf -o report.pdf",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("includes IPYNB cell order and output selection", () => {
|
|
76
|
+
expect(
|
|
77
|
+
command("ipynb", "report.py", {
|
|
78
|
+
ipynb: { sortMode: "top-down", includeOutputs: true },
|
|
79
|
+
}),
|
|
80
|
+
).toBe(
|
|
81
|
+
"marimo export ipynb report.py --sort=top-down --include-outputs -o report.ipynb",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("omits the fixed WebPDF engine flag for slide PDFs", () => {
|
|
86
|
+
expect(
|
|
87
|
+
command("pdf", "slides.py", {
|
|
88
|
+
pdf: {
|
|
89
|
+
preset: "slides",
|
|
90
|
+
includeInputs: false,
|
|
91
|
+
includeOutputs: false,
|
|
92
|
+
webpdf: false,
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
).toBe(
|
|
96
|
+
"marimo export pdf slides.py --as=slides --no-include-inputs --no-include-outputs -o slides.pdf",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("uses the script export filename contract", () => {
|
|
101
|
+
expect(
|
|
102
|
+
command("script", "analysis.py", {
|
|
103
|
+
script: { type: "flat" },
|
|
104
|
+
}),
|
|
105
|
+
).toBe("marimo export script analysis.py -o analysis.script.py");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it.each([
|
|
109
|
+
["editable notebook source", "script", "analysis.py"],
|
|
110
|
+
["unnamed notebook", "html", null],
|
|
111
|
+
["PNG capture", "png", "analysis.py"],
|
|
112
|
+
] as const)("has no command for %s", (_case, format, filename) => {
|
|
113
|
+
expect(command(format, filename)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import { Provider } from "jotai";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
14
|
+
import { Dialog } from "@/components/ui/dialog";
|
|
15
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
16
|
+
import { viewStateAtom } from "@/core/mode";
|
|
17
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
18
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
19
|
+
import { store } from "@/core/state/jotai";
|
|
20
|
+
import { isWasm } from "@/core/wasm/utils";
|
|
21
|
+
import * as copyModule from "@/utils/copy";
|
|
22
|
+
import { ExportDialog } from "../export-dialog";
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_EXPORT_OPTIONS,
|
|
25
|
+
type ExportFormat,
|
|
26
|
+
exportOptionsAtom,
|
|
27
|
+
lastExportFormatAtom,
|
|
28
|
+
} from "../state";
|
|
29
|
+
|
|
30
|
+
const { exportNotebookMock } = vi.hoisted(() => ({
|
|
31
|
+
exportNotebookMock: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("@/utils/copy", () => ({
|
|
35
|
+
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("@/core/wasm/utils", async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal<typeof import("@/core/wasm/utils")>();
|
|
40
|
+
return { ...actual, isWasm: vi.fn(() => false) };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
vi.mock("../export-notebook", () => ({
|
|
44
|
+
exportNotebook: exportNotebookMock,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
function wrapper({ children }: { children: React.ReactNode }) {
|
|
48
|
+
return (
|
|
49
|
+
<Provider store={store}>
|
|
50
|
+
<TooltipProvider>
|
|
51
|
+
<Dialog open={true}>{children}</Dialog>
|
|
52
|
+
</TooltipProvider>
|
|
53
|
+
</Provider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderDialog(initialFormat?: ExportFormat, onClose = vi.fn()) {
|
|
58
|
+
return render(
|
|
59
|
+
<ExportDialog initialFormat={initialFormat} onClose={onClose} />,
|
|
60
|
+
{ wrapper },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function waitForExportEnabled() {
|
|
65
|
+
await waitFor(() =>
|
|
66
|
+
expect(screen.getByTestId("export-submit")).toBeEnabled(),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("ExportDialog", () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
vi.clearAllMocks();
|
|
73
|
+
localStorage.clear();
|
|
74
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
75
|
+
store.set(filenameAtom, "/project/notebook.py");
|
|
76
|
+
store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
|
|
77
|
+
store.set(exportOptionsAtom, DEFAULT_EXPORT_OPTIONS);
|
|
78
|
+
store.set(lastExportFormatAtom, "html");
|
|
79
|
+
vi.mocked(isWasm).mockReturnValue(false);
|
|
80
|
+
vi.stubGlobal("matchMedia", () => ({
|
|
81
|
+
matches: false,
|
|
82
|
+
addEventListener: vi.fn(),
|
|
83
|
+
removeEventListener: vi.fn(),
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.unstubAllGlobals();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("locks format and option controls while exporting", async () => {
|
|
92
|
+
let finishExport: (() => void) | undefined;
|
|
93
|
+
exportNotebookMock.mockImplementationOnce(
|
|
94
|
+
() =>
|
|
95
|
+
new Promise<void>((resolve) => {
|
|
96
|
+
finishExport = resolve;
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
const onClose = vi.fn();
|
|
100
|
+
renderDialog("pdf", onClose);
|
|
101
|
+
await waitForExportEnabled();
|
|
102
|
+
|
|
103
|
+
fireEvent.click(screen.getByTestId("export-submit"));
|
|
104
|
+
await waitFor(() => expect(exportNotebookMock).toHaveBeenCalledOnce());
|
|
105
|
+
|
|
106
|
+
expect(screen.getByTestId("export-submit")).toHaveAttribute(
|
|
107
|
+
"aria-busy",
|
|
108
|
+
"true",
|
|
109
|
+
);
|
|
110
|
+
expect(screen.getByTestId("export-format-html")).toBeDisabled();
|
|
111
|
+
expect(screen.getByRole("radio", { name: "Document" })).toBeDisabled();
|
|
112
|
+
expect(screen.getByRole("switch", { name: "Include code" })).toBeDisabled();
|
|
113
|
+
|
|
114
|
+
finishExport?.();
|
|
115
|
+
await waitFor(() => expect(onClose).toHaveBeenCalledOnce());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("describes the selected PDF options", async () => {
|
|
119
|
+
renderDialog("pdf");
|
|
120
|
+
await waitForExportEnabled();
|
|
121
|
+
|
|
122
|
+
expect(
|
|
123
|
+
screen.getByRole("radiogroup", { name: "Layout" }),
|
|
124
|
+
).toHaveAccessibleDescription("Creates pages for reading or printing.");
|
|
125
|
+
expect(
|
|
126
|
+
screen.getByRole("switch", { name: "Include code" }),
|
|
127
|
+
).toHaveAccessibleDescription("Includes code cells in the PDF.");
|
|
128
|
+
const method = screen.getByRole("radiogroup", { name: "Method" });
|
|
129
|
+
expect(method).toHaveAccessibleDescription(
|
|
130
|
+
"Uses browser rendering to preserve the notebook's visual output.",
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByRole("radio", { name: "Browser" })).toBeChecked();
|
|
133
|
+
|
|
134
|
+
fireEvent.click(screen.getByRole("radio", { name: "LaTeX first" }));
|
|
135
|
+
expect(method).toHaveAccessibleDescription(
|
|
136
|
+
"Uses Pandoc and TeX when available, then falls back to browser rendering.",
|
|
137
|
+
);
|
|
138
|
+
expect(screen.getByRole("radio", { name: "LaTeX first" })).toBeChecked();
|
|
139
|
+
|
|
140
|
+
fireEvent.click(screen.getByRole("switch", { name: "Include code" }));
|
|
141
|
+
expect(
|
|
142
|
+
screen.getByRole("switch", { name: "Include code" }),
|
|
143
|
+
).toHaveAccessibleDescription("Leaves code cells out of the PDF.");
|
|
144
|
+
|
|
145
|
+
fireEvent.click(screen.getByRole("radio", { name: "Slides" }));
|
|
146
|
+
expect(
|
|
147
|
+
screen.getByRole("radiogroup", { name: "Layout" }),
|
|
148
|
+
).toHaveAccessibleDescription(
|
|
149
|
+
"Creates presentation slides from the notebook.",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("describes the selected Markdown format", () => {
|
|
154
|
+
renderDialog("markdown");
|
|
155
|
+
|
|
156
|
+
expect(
|
|
157
|
+
screen.getByRole("combobox", { name: "Format" }),
|
|
158
|
+
).toHaveAccessibleDescription(
|
|
159
|
+
"Chooses a format from the notebook filename. Defaults to Markdown (.md).",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
act(() => {
|
|
163
|
+
store.set(exportOptionsAtom, {
|
|
164
|
+
...store.get(exportOptionsAtom),
|
|
165
|
+
markdown: { flavor: "qmd" },
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(
|
|
170
|
+
screen.getByRole("combobox", { name: "Format" }),
|
|
171
|
+
).toHaveAccessibleDescription("Creates a .qmd file for Quarto.");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("describes the selected Jupyter cell order", () => {
|
|
175
|
+
renderDialog("ipynb");
|
|
176
|
+
|
|
177
|
+
expect(
|
|
178
|
+
screen.getByRole("radiogroup", { name: "Cell order" }),
|
|
179
|
+
).toHaveAccessibleDescription(
|
|
180
|
+
"Reorders cells so each cell appears after the cells it depends on.",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
fireEvent.click(screen.getByRole("radio", { name: "Top to bottom" }));
|
|
184
|
+
|
|
185
|
+
expect(
|
|
186
|
+
screen.getByRole("radiogroup", { name: "Cell order" }),
|
|
187
|
+
).toHaveAccessibleDescription(
|
|
188
|
+
"Keeps cells in the same order as this notebook.",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("shows every format and updates the equivalent command with options", async () => {
|
|
193
|
+
renderDialog();
|
|
194
|
+
|
|
195
|
+
for (const format of [
|
|
196
|
+
"html",
|
|
197
|
+
"markdown",
|
|
198
|
+
"ipynb",
|
|
199
|
+
"pdf",
|
|
200
|
+
"script",
|
|
201
|
+
"png",
|
|
202
|
+
]) {
|
|
203
|
+
expect(screen.getByTestId(`export-format-${format}`)).toBeVisible();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await waitForExportEnabled();
|
|
207
|
+
expect(screen.getByTestId("export-cli-command")).toHaveTextContent(
|
|
208
|
+
"marimo export html /project/notebook.py --include-code",
|
|
209
|
+
);
|
|
210
|
+
expect(
|
|
211
|
+
screen.getByText(
|
|
212
|
+
"Uses the current session state. The copied command exports the saved notebook.",
|
|
213
|
+
),
|
|
214
|
+
).toBeVisible();
|
|
215
|
+
|
|
216
|
+
fireEvent.click(screen.getByRole("switch", { name: "Include code" }));
|
|
217
|
+
expect(screen.getByTestId("export-cli-command")).toHaveTextContent(
|
|
218
|
+
"--no-include-code",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("matches tab keyboard orientation to the responsive layout", () => {
|
|
223
|
+
const listeners = new Set<() => void>();
|
|
224
|
+
const media = {
|
|
225
|
+
matches: false,
|
|
226
|
+
addEventListener: vi.fn((_event: "change", listener: () => void) =>
|
|
227
|
+
listeners.add(listener),
|
|
228
|
+
),
|
|
229
|
+
removeEventListener: vi.fn((_event: "change", listener: () => void) =>
|
|
230
|
+
listeners.delete(listener),
|
|
231
|
+
),
|
|
232
|
+
};
|
|
233
|
+
vi.stubGlobal(
|
|
234
|
+
"matchMedia",
|
|
235
|
+
vi.fn(() => media),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
renderDialog();
|
|
239
|
+
const tablist = screen.getByRole("tablist", { name: "Export format" });
|
|
240
|
+
expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
|
|
241
|
+
|
|
242
|
+
act(() => {
|
|
243
|
+
media.matches = true;
|
|
244
|
+
for (const listener of listeners) {
|
|
245
|
+
listener();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("keeps unavailable formats visible and names missing packages", async () => {
|
|
252
|
+
store.set(
|
|
253
|
+
requestClientAtom,
|
|
254
|
+
MockRequestClient.create({
|
|
255
|
+
getExportAvailability: vi.fn().mockResolvedValue({
|
|
256
|
+
source: "server",
|
|
257
|
+
formats: [
|
|
258
|
+
{
|
|
259
|
+
format: "pdf",
|
|
260
|
+
dependenciesAvailable: false,
|
|
261
|
+
missingPackages: ["nbconvert[webpdf]"],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
renderDialog("pdf");
|
|
269
|
+
|
|
270
|
+
expect(await screen.findByText("nbconvert[webpdf]")).toBeVisible();
|
|
271
|
+
expect(
|
|
272
|
+
screen.getByText(/where marimo is running to use this export/),
|
|
273
|
+
).toBeVisible();
|
|
274
|
+
expect(screen.getByTestId("export-submit")).toBeDisabled();
|
|
275
|
+
expect(screen.getByTestId("export-format-pdf")).toBeVisible();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("announces requirement checks as status updates", () => {
|
|
279
|
+
store.set(
|
|
280
|
+
requestClientAtom,
|
|
281
|
+
MockRequestClient.create({
|
|
282
|
+
getExportAvailability: vi.fn(() => new Promise(() => undefined)),
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
renderDialog("pdf");
|
|
287
|
+
|
|
288
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
289
|
+
"Checking PDF requirements…",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("hides the document renderer when exporting slides", async () => {
|
|
294
|
+
renderDialog("pdf");
|
|
295
|
+
await waitForExportEnabled();
|
|
296
|
+
|
|
297
|
+
fireEvent.click(screen.getByRole("radio", { name: "Slides" }));
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
screen.queryByRole("radiogroup", { name: "Method" }),
|
|
301
|
+
).not.toBeInTheDocument();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("requires a saved file for notebook source but not flat script", () => {
|
|
305
|
+
store.set(filenameAtom, null);
|
|
306
|
+
|
|
307
|
+
renderDialog("script");
|
|
308
|
+
|
|
309
|
+
expect(
|
|
310
|
+
screen.getByText("Name and save this notebook before exporting."),
|
|
311
|
+
).toBeVisible();
|
|
312
|
+
expect(screen.getByTestId("export-submit")).toBeDisabled();
|
|
313
|
+
|
|
314
|
+
fireEvent.click(screen.getByRole("radio", { name: "Flat script" }));
|
|
315
|
+
|
|
316
|
+
expect(
|
|
317
|
+
screen.queryByText("Name and save this notebook before exporting."),
|
|
318
|
+
).not.toBeInTheDocument();
|
|
319
|
+
expect(screen.getByTestId("export-submit")).toBeEnabled();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("allows an export attempt when requirements cannot be checked", async () => {
|
|
323
|
+
store.set(
|
|
324
|
+
requestClientAtom,
|
|
325
|
+
MockRequestClient.create({
|
|
326
|
+
getExportAvailability: vi
|
|
327
|
+
.fn()
|
|
328
|
+
.mockRejectedValue(new Error("unavailable")),
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
renderDialog("pdf");
|
|
333
|
+
|
|
334
|
+
expect(
|
|
335
|
+
await screen.findByText(
|
|
336
|
+
"Couldn't check whether this export is available. You can still try it.",
|
|
337
|
+
),
|
|
338
|
+
).toBeVisible();
|
|
339
|
+
expect(screen.getByTestId("export-submit")).toBeEnabled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("shows browser printing for PDF export in WebAssembly", () => {
|
|
343
|
+
vi.mocked(isWasm).mockReturnValue(true);
|
|
344
|
+
|
|
345
|
+
renderDialog("pdf");
|
|
346
|
+
|
|
347
|
+
expect(
|
|
348
|
+
screen.getByText(
|
|
349
|
+
"Use your browser's print dialog to save the current app view as a PDF.",
|
|
350
|
+
),
|
|
351
|
+
).toBeVisible();
|
|
352
|
+
expect(screen.queryByRole("radio", { name: "Document" })).toBeNull();
|
|
353
|
+
expect(screen.queryByTestId("export-cli-command")).not.toBeInTheDocument();
|
|
354
|
+
expect(screen.getByRole("button", { name: "Print to PDF" })).toBeEnabled();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("copies the displayed CLI command", async () => {
|
|
358
|
+
store.set(exportOptionsAtom, {
|
|
359
|
+
...DEFAULT_EXPORT_OPTIONS,
|
|
360
|
+
script: { type: "flat" },
|
|
361
|
+
});
|
|
362
|
+
renderDialog("script");
|
|
363
|
+
await waitForExportEnabled();
|
|
364
|
+
|
|
365
|
+
fireEvent.click(
|
|
366
|
+
screen.getByRole("button", { name: "Copy POSIX shell command" }),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
await waitFor(() =>
|
|
370
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
371
|
+
"marimo export script /project/notebook.py -o /project/notebook.script.py",
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("describes and selects each Python format", () => {
|
|
377
|
+
renderDialog("script");
|
|
378
|
+
|
|
379
|
+
expect(
|
|
380
|
+
screen.getByRole("radio", { name: "Notebook source" }),
|
|
381
|
+
).toBeChecked();
|
|
382
|
+
expect(
|
|
383
|
+
screen.getByRole("radiogroup", { name: "Format" }),
|
|
384
|
+
).toHaveAccessibleDescription(
|
|
385
|
+
"Downloads the saved notebook source for continued editing in marimo.",
|
|
386
|
+
);
|
|
387
|
+
expect(
|
|
388
|
+
screen.getByRole("button", { name: "Export notebook source" }),
|
|
389
|
+
).toBeVisible();
|
|
390
|
+
expect(screen.queryByTestId("export-cli-command")).not.toBeInTheDocument();
|
|
391
|
+
|
|
392
|
+
fireEvent.click(screen.getByRole("radio", { name: "Flat script" }));
|
|
393
|
+
|
|
394
|
+
expect(
|
|
395
|
+
screen.getByRole("radiogroup", { name: "Format" }),
|
|
396
|
+
).toHaveAccessibleDescription(
|
|
397
|
+
"Reorders cells so the Python script runs from top to bottom.",
|
|
398
|
+
);
|
|
399
|
+
expect(
|
|
400
|
+
screen.getByRole("button", { name: "Export flat script" }),
|
|
401
|
+
).toBeVisible();
|
|
402
|
+
expect(screen.getByTestId("export-cli-command")).toHaveTextContent(
|
|
403
|
+
"marimo export script /project/notebook.py",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("hides the shell command until the notebook is saved", () => {
|
|
408
|
+
store.set(filenameAtom, null);
|
|
409
|
+
store.set(exportOptionsAtom, {
|
|
410
|
+
...DEFAULT_EXPORT_OPTIONS,
|
|
411
|
+
script: { type: "flat" },
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
renderDialog("script");
|
|
415
|
+
|
|
416
|
+
expect(screen.queryByTestId("export-cli-command")).not.toBeInTheDocument();
|
|
417
|
+
expect(
|
|
418
|
+
screen.getByText(
|
|
419
|
+
"Save the notebook to copy an equivalent shell command.",
|
|
420
|
+
),
|
|
421
|
+
).toBeVisible();
|
|
422
|
+
});
|
|
423
|
+
});
|