@marimo-team/islands 0.24.1-dev2 → 0.24.1-dev20
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/{chat-ui-o-tATfmW.js → chat-ui-ChBRy1iT.js} +3 -3
- package/dist/{common-DjnMIXVY.js → common-3k3erMZf.js} +4 -4
- package/dist/{formats-HSfYlcA8.js → formats-Dz0QNUEa.js} +1 -1
- package/dist/{html-to-image-BOiFHx3u.js → html-to-image-BtNr4zrO.js} +1 -1
- package/dist/main.js +1100 -1006
- package/dist/{process-output-HrsM52f8.js → process-output-Dch8KCi8.js} +1 -1
- package/dist/{reveal-component-BufNyZPY.js → reveal-component-CCuZFOFo.js} +3 -3
- package/dist/style.css +1 -1
- package/dist/{toDate-BGM7kFtQ.js → toDate-CjUYDkFI.js} +2 -2
- package/dist/{vega-component-CJug-VdR.js → vega-component-CioKuDut.js} +2 -2
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +4 -0
- package/src/components/app-config/optional-features.test.ts +78 -0
- package/src/components/app-config/optional-features.tsx +33 -6
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +206 -34
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +9 -3
- package/src/components/editor/actions/export-dialog/format-notice.tsx +69 -24
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +27 -2
- package/src/components/ui/toaster.tsx +14 -1
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
- package/src/core/codemirror/code-lens/popover.tsx +11 -5
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/mode.ts +6 -1
- package/src/core/network/__tests__/requests-lazy.test.ts +40 -0
- package/src/core/network/__tests__/requests-network.test.ts +13 -0
- package/src/core/network/requests-lazy.ts +11 -4
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +5 -0
- package/src/core/wasm/bridge.ts +1 -0
- package/src/plugins/impl/MatrixPlugin.tsx +256 -35
- package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
|
@@ -54,8 +54,8 @@ const viewStateAtom = atom({
|
|
|
54
54
|
cellAnchor: null
|
|
55
55
|
}), initialModeAtom = atom(void 0), kioskModeAtom = atom(false);
|
|
56
56
|
function useInstallAllowed() {
|
|
57
|
-
let { mode: e } = useAtomValue(viewStateAtom);
|
|
58
|
-
return e !== "read";
|
|
57
|
+
let { mode: e } = useAtomValue(viewStateAtom), S = useAtomValue(kioskModeAtom), C = new URLSearchParams(window.location.search).get(KnownQueryParams.kiosk) === "true";
|
|
58
|
+
return e !== "read" && !S && !C;
|
|
59
59
|
}
|
|
60
60
|
const WebSocketState = {
|
|
61
61
|
NOT_STARTED: "NOT_STARTED",
|
|
@@ -2,7 +2,7 @@ import { o as __toESM } from "./chunk-Bfp04NdC.js";
|
|
|
2
2
|
import { g as Logger, h as cn, m as Events } from "./button-Dr2uAwhT.js";
|
|
3
3
|
import { t as require_react } from "./react-DhGsCS90.js";
|
|
4
4
|
import { t as require_compiler_runtime } from "./compiler-runtime-DsR6imPN.js";
|
|
5
|
-
import { c as asRemoteURL, w as CircleQuestionMark } from "./toDate-
|
|
5
|
+
import { c as asRemoteURL, w as CircleQuestionMark } from "./toDate-CjUYDkFI.js";
|
|
6
6
|
import "./react-dom-CmENFV8Q.js";
|
|
7
7
|
import { t as require_jsx_runtime } from "./jsx-runtime-Dp_oQwaf.js";
|
|
8
8
|
import "./zod-BFY2SnzC.js";
|
|
@@ -12,7 +12,7 @@ import { t as debounce_default } from "./debounce-D784bpms.js";
|
|
|
12
12
|
import { A as useEvent_default, n as useTheme } from "./useTheme-BzEXeAR5.js";
|
|
13
13
|
import { s as uniq } from "./arrays-C56grfjL.js";
|
|
14
14
|
import { t as Objects } from "./objects-UzwQd5gx.js";
|
|
15
|
-
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-
|
|
15
|
+
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-Dz0QNUEa.js";
|
|
16
16
|
import { A as getContainerWidth, n as formats } from "./vega-loader.browser-CGb-2Kwi.js";
|
|
17
17
|
import { a as tooltipHandler, n as vegaLoadData } from "./loader-DTnbNSVZ.js";
|
|
18
18
|
import { t as j } from "./react-vega-zyfTF9SY.js";
|
package/package.json
CHANGED
|
@@ -100,6 +100,10 @@ export const MockRequestClient = {
|
|
|
100
100
|
},
|
|
101
101
|
],
|
|
102
102
|
}),
|
|
103
|
+
installExportRequirements: vi.fn().mockResolvedValue({
|
|
104
|
+
source: "server",
|
|
105
|
+
formats: [],
|
|
106
|
+
}),
|
|
103
107
|
exportAsHTML: vi.fn().mockResolvedValue({
|
|
104
108
|
contents: "",
|
|
105
109
|
filename: "notebook.html",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { isPackageRequirementInstalled } from "./optional-features";
|
|
5
|
+
|
|
6
|
+
describe("isPackageRequirementInstalled", () => {
|
|
7
|
+
const installedPackages = [
|
|
8
|
+
{ name: "mcp", version: "2.0.0" },
|
|
9
|
+
{ name: "polars", version: "1.8.2" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it("requires the package to be installed", () => {
|
|
13
|
+
expect(
|
|
14
|
+
isPackageRequirementInstalled({ name: "missing" }, installedPackages),
|
|
15
|
+
).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("accepts an installed package without a minimum version", () => {
|
|
19
|
+
expect(
|
|
20
|
+
isPackageRequirementInstalled({ name: "polars" }, installedPackages),
|
|
21
|
+
).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects an installed package below the minimum version", () => {
|
|
25
|
+
expect(
|
|
26
|
+
isPackageRequirementInstalled(
|
|
27
|
+
{ name: "mcp", minVersion: "2.1.0" },
|
|
28
|
+
installedPackages,
|
|
29
|
+
),
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each(["2.0.0rc1", "2.0.0.dev1", "2.0.0-beta1"])(
|
|
34
|
+
"rejects the prerelease version %s",
|
|
35
|
+
(version) => {
|
|
36
|
+
expect(
|
|
37
|
+
isPackageRequirementInstalled({ name: "mcp", minVersion: "2.0.0" }, [
|
|
38
|
+
{ name: "mcp", version },
|
|
39
|
+
]),
|
|
40
|
+
).toBe(false);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it.each(["2.1.0rc1", "3.0.0rc1"])(
|
|
45
|
+
"accepts the later prerelease version %s",
|
|
46
|
+
(version) => {
|
|
47
|
+
expect(
|
|
48
|
+
isPackageRequirementInstalled({ name: "mcp", minVersion: "2.0.0" }, [
|
|
49
|
+
{ name: "mcp", version },
|
|
50
|
+
]),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
it("accepts an installed package at or above the minimum version", () => {
|
|
56
|
+
expect(
|
|
57
|
+
isPackageRequirementInstalled(
|
|
58
|
+
{ name: "mcp", minVersion: "2.0.0" },
|
|
59
|
+
installedPackages,
|
|
60
|
+
),
|
|
61
|
+
).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
isPackageRequirementInstalled(
|
|
64
|
+
{ name: "mcp", minVersion: "1.9.0" },
|
|
65
|
+
installedPackages,
|
|
66
|
+
),
|
|
67
|
+
).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("normalizes extras in the requirement name", () => {
|
|
71
|
+
expect(
|
|
72
|
+
isPackageRequirementInstalled(
|
|
73
|
+
{ name: "polars[pyarrow]" },
|
|
74
|
+
installedPackages,
|
|
75
|
+
),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -20,6 +20,7 @@ import { isWasm } from "@/core/wasm/utils";
|
|
|
20
20
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
21
21
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
22
22
|
import { cn } from "@/utils/cn";
|
|
23
|
+
import { semverSort } from "@/utils/versions";
|
|
23
24
|
import { SettingSubtitle } from "./common";
|
|
24
25
|
|
|
25
26
|
interface Package {
|
|
@@ -27,6 +28,35 @@ interface Package {
|
|
|
27
28
|
minVersion?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
interface InstalledPackage {
|
|
32
|
+
name: string;
|
|
33
|
+
version: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PEP_440_PRERELEASE =
|
|
37
|
+
/^((?:\d+!)?\d+(?:\.\d+)*)(?:[-_.]?(?:a|b|c|rc|alpha|beta|pre|preview|dev)\d*)/i;
|
|
38
|
+
|
|
39
|
+
export function isPackageRequirementInstalled(
|
|
40
|
+
requirement: Package,
|
|
41
|
+
installedPackages: InstalledPackage[],
|
|
42
|
+
): boolean {
|
|
43
|
+
const packageName = requirement.name.split("[")[0];
|
|
44
|
+
const installedPackage = installedPackages.find(
|
|
45
|
+
(pkg) => pkg.name === packageName,
|
|
46
|
+
);
|
|
47
|
+
if (!installedPackage) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!requirement.minVersion) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const prerelease = installedPackage.version.match(PEP_440_PRERELEASE);
|
|
54
|
+
if (prerelease) {
|
|
55
|
+
return semverSort(prerelease[1], requirement.minVersion) > 0;
|
|
56
|
+
}
|
|
57
|
+
return semverSort(installedPackage.version, requirement.minVersion) >= 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
interface OptionalFeature {
|
|
31
61
|
id: string;
|
|
32
62
|
/**
|
|
@@ -77,8 +107,8 @@ const OPTIONAL_DEPENDENCIES: OptionalFeature[] = [
|
|
|
77
107
|
},
|
|
78
108
|
{
|
|
79
109
|
id: "mcp",
|
|
80
|
-
packagesRequired: [{ name: "mcp", minVersion: "
|
|
81
|
-
additionalPackageInstalls: [
|
|
110
|
+
packagesRequired: [{ name: "mcp", minVersion: "2.0.0" }],
|
|
111
|
+
additionalPackageInstalls: [],
|
|
82
112
|
description: "Connect to MCP servers",
|
|
83
113
|
},
|
|
84
114
|
{
|
|
@@ -123,9 +153,6 @@ export const OptionalFeatures: React.FC = () => {
|
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
const installedPackages = data?.packages || [];
|
|
126
|
-
const installedPackageNames = new Set(
|
|
127
|
-
installedPackages.map((pkg) => pkg.name),
|
|
128
|
-
);
|
|
129
156
|
|
|
130
157
|
return (
|
|
131
158
|
<div className="flex-1 flex flex-col overflow-hidden gap-2">
|
|
@@ -149,7 +176,7 @@ export const OptionalFeatures: React.FC = () => {
|
|
|
149
176
|
<TableBody>
|
|
150
177
|
{OPTIONAL_DEPENDENCIES.map((dep) => {
|
|
151
178
|
const isInstalled = dep.packagesRequired.every((pkg) =>
|
|
152
|
-
|
|
179
|
+
isPackageRequirementInstalled(pkg, installedPackages),
|
|
153
180
|
);
|
|
154
181
|
const packageSpec = dep.packagesRequired
|
|
155
182
|
.map((pkg) => pkg.name)
|
|
@@ -13,12 +13,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
13
13
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
14
14
|
import { Dialog } from "@/components/ui/dialog";
|
|
15
15
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
16
|
-
import { viewStateAtom } from "@/core/mode";
|
|
16
|
+
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
17
17
|
import { requestClientAtom } from "@/core/network/requests";
|
|
18
|
+
import { createErrorToastingRequests } from "@/core/network/requests-toasting";
|
|
19
|
+
import type { ExportAvailabilityResponse } from "@/core/network/types";
|
|
18
20
|
import { filenameAtom } from "@/core/saving/file-state";
|
|
19
21
|
import { store } from "@/core/state/jotai";
|
|
20
22
|
import { isWasm } from "@/core/wasm/utils";
|
|
21
23
|
import * as copyModule from "@/utils/copy";
|
|
24
|
+
import { Deferred } from "@/utils/Deferred";
|
|
25
|
+
import { HTTPError } from "@/utils/errors";
|
|
22
26
|
import { ExportDialog } from "../export-dialog";
|
|
23
27
|
import {
|
|
24
28
|
DEFAULT_EXPORT_OPTIONS,
|
|
@@ -27,14 +31,22 @@ import {
|
|
|
27
31
|
lastExportFormatAtom,
|
|
28
32
|
} from "../state";
|
|
29
33
|
|
|
30
|
-
const { exportNotebookMock } = vi.hoisted(() => ({
|
|
34
|
+
const { exportNotebookMock, toastMock } = vi.hoisted(() => ({
|
|
31
35
|
exportNotebookMock: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
toastMock: vi.fn(() => ({
|
|
37
|
+
dismiss: vi.fn(),
|
|
38
|
+
update: vi.fn(),
|
|
39
|
+
})),
|
|
32
40
|
}));
|
|
33
41
|
|
|
34
42
|
vi.mock("@/utils/copy", () => ({
|
|
35
43
|
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
36
44
|
}));
|
|
37
45
|
|
|
46
|
+
vi.mock("@/components/ui/use-toast", () => ({
|
|
47
|
+
toast: toastMock,
|
|
48
|
+
}));
|
|
49
|
+
|
|
38
50
|
vi.mock("@/core/wasm/utils", async (importOriginal) => {
|
|
39
51
|
const actual = await importOriginal<typeof import("@/core/wasm/utils")>();
|
|
40
52
|
return { ...actual, isWasm: vi.fn(() => false) };
|
|
@@ -67,6 +79,32 @@ async function waitForExportEnabled() {
|
|
|
67
79
|
);
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
type FormatAvailability = ExportAvailabilityResponse["formats"][number];
|
|
83
|
+
|
|
84
|
+
function formatAvailability(
|
|
85
|
+
format: FormatAvailability["format"],
|
|
86
|
+
overrides: Partial<Omit<FormatAvailability, "format">> = {},
|
|
87
|
+
): FormatAvailability {
|
|
88
|
+
return {
|
|
89
|
+
format,
|
|
90
|
+
dependenciesAvailable: true,
|
|
91
|
+
missingPackages: [],
|
|
92
|
+
missingSetup: [],
|
|
93
|
+
...overrides,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function serverAvailability(
|
|
98
|
+
...formats: FormatAvailability[]
|
|
99
|
+
): ExportAvailabilityResponse {
|
|
100
|
+
return { source: "server", formats };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const PLAYWRIGHT_SETUP = {
|
|
104
|
+
name: "playwright-chromium",
|
|
105
|
+
command: "uv run playwright install chromium",
|
|
106
|
+
} as const;
|
|
107
|
+
|
|
70
108
|
describe("ExportDialog", () => {
|
|
71
109
|
beforeEach(() => {
|
|
72
110
|
vi.clearAllMocks();
|
|
@@ -74,8 +112,10 @@ describe("ExportDialog", () => {
|
|
|
74
112
|
store.set(requestClientAtom, MockRequestClient.create());
|
|
75
113
|
store.set(filenameAtom, "/project/notebook.py");
|
|
76
114
|
store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
|
|
115
|
+
store.set(kioskModeAtom, false);
|
|
77
116
|
store.set(exportOptionsAtom, DEFAULT_EXPORT_OPTIONS);
|
|
78
117
|
store.set(lastExportFormatAtom, "html");
|
|
118
|
+
window.history.replaceState(null, "", "/");
|
|
79
119
|
vi.mocked(isWasm).mockReturnValue(false);
|
|
80
120
|
vi.stubGlobal("matchMedia", () => ({
|
|
81
121
|
matches: false,
|
|
@@ -248,54 +288,80 @@ describe("ExportDialog", () => {
|
|
|
248
288
|
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
|
|
249
289
|
});
|
|
250
290
|
|
|
251
|
-
it("
|
|
291
|
+
it("installs missing packages and applies refreshed availability", async () => {
|
|
292
|
+
const install = new Deferred<ExportAvailabilityResponse>();
|
|
293
|
+
const installExportRequirements = vi.fn(() => install.promise);
|
|
294
|
+
const getExportAvailability = vi.fn().mockResolvedValue(
|
|
295
|
+
serverAvailability(
|
|
296
|
+
formatAvailability("ipynb", {
|
|
297
|
+
dependenciesAvailable: false,
|
|
298
|
+
missingPackages: ["nbformat"],
|
|
299
|
+
}),
|
|
300
|
+
formatAvailability("pdf", {
|
|
301
|
+
dependenciesAvailable: false,
|
|
302
|
+
missingPackages: ["nbconvert[webpdf]", "nbformat"],
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
252
306
|
store.set(
|
|
253
307
|
requestClientAtom,
|
|
254
308
|
MockRequestClient.create({
|
|
255
|
-
getExportAvailability
|
|
256
|
-
|
|
257
|
-
formats: [
|
|
258
|
-
{
|
|
259
|
-
format: "pdf",
|
|
260
|
-
dependenciesAvailable: false,
|
|
261
|
-
missingPackages: ["nbconvert[webpdf]"],
|
|
262
|
-
missingSetup: [],
|
|
263
|
-
},
|
|
264
|
-
],
|
|
265
|
-
}),
|
|
309
|
+
getExportAvailability,
|
|
310
|
+
installExportRequirements,
|
|
266
311
|
}),
|
|
267
312
|
);
|
|
268
313
|
|
|
269
314
|
renderDialog("pdf");
|
|
270
315
|
|
|
271
|
-
|
|
272
|
-
expect(
|
|
273
|
-
|
|
274
|
-
)
|
|
316
|
+
const packages = await screen.findByText("nbconvert[webpdf], nbformat");
|
|
317
|
+
expect(packages.parentElement).toHaveTextContent(
|
|
318
|
+
"PDF export requires nbconvert[webpdf], nbformat.",
|
|
319
|
+
);
|
|
275
320
|
expect(screen.getByTestId("export-submit")).toBeDisabled();
|
|
276
321
|
expect(screen.getByTestId("export-format-pdf")).toBeVisible();
|
|
322
|
+
|
|
323
|
+
const installButton = screen.getByRole("button", { name: "Install" });
|
|
324
|
+
const pdfTab = screen.getByTestId("export-format-pdf");
|
|
325
|
+
const htmlTab = screen.getByTestId("export-format-html");
|
|
326
|
+
|
|
327
|
+
fireEvent.click(installButton);
|
|
328
|
+
|
|
329
|
+
await waitFor(() =>
|
|
330
|
+
expect(installExportRequirements).toHaveBeenCalledWith({ format: "pdf" }),
|
|
331
|
+
);
|
|
332
|
+
expect(installButton).toHaveAttribute("aria-busy", "true");
|
|
333
|
+
expect(htmlTab).toBeDisabled();
|
|
334
|
+
|
|
335
|
+
fireEvent.click(installButton);
|
|
336
|
+
fireEvent.click(htmlTab);
|
|
337
|
+
expect(installExportRequirements).toHaveBeenCalledOnce();
|
|
338
|
+
expect(pdfTab).toHaveAttribute("data-state", "active");
|
|
339
|
+
|
|
340
|
+
install.resolve(
|
|
341
|
+
serverAvailability(
|
|
342
|
+
formatAvailability("ipynb"),
|
|
343
|
+
formatAvailability("pdf"),
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
await waitForExportEnabled();
|
|
347
|
+
expect(getExportAvailability).toHaveBeenCalledOnce();
|
|
348
|
+
|
|
349
|
+
fireEvent.click(screen.getByTestId("export-format-ipynb"));
|
|
350
|
+
expect(screen.getByTestId("export-submit")).toBeEnabled();
|
|
277
351
|
});
|
|
278
352
|
|
|
279
353
|
it("explains missing Playwright Chromium setup", async () => {
|
|
280
354
|
store.set(
|
|
281
355
|
requestClientAtom,
|
|
282
356
|
MockRequestClient.create({
|
|
283
|
-
getExportAvailability: vi.fn().mockResolvedValue(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
format: "pdf",
|
|
357
|
+
getExportAvailability: vi.fn().mockResolvedValue(
|
|
358
|
+
serverAvailability(
|
|
359
|
+
formatAvailability("pdf", {
|
|
288
360
|
dependenciesAvailable: false,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
command: "uv run playwright install chromium",
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
},
|
|
297
|
-
],
|
|
298
|
-
}),
|
|
361
|
+
missingSetup: [PLAYWRIGHT_SETUP],
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
),
|
|
299
365
|
}),
|
|
300
366
|
);
|
|
301
367
|
|
|
@@ -305,9 +371,115 @@ describe("ExportDialog", () => {
|
|
|
305
371
|
await screen.findByText("PDF export requires Playwright Chromium."),
|
|
306
372
|
).toBeVisible();
|
|
307
373
|
expect(
|
|
308
|
-
screen.
|
|
374
|
+
screen.queryByText("uv run playwright install chromium"),
|
|
375
|
+
).not.toBeInTheDocument();
|
|
376
|
+
expect(screen.getByTestId("export-submit")).toBeDisabled();
|
|
377
|
+
expect(screen.getByRole("button", { name: "Install" })).toBeVisible();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it.each([
|
|
381
|
+
["read mode", "read", false, false],
|
|
382
|
+
["connected kiosk mode", "edit", true, false],
|
|
383
|
+
["pre-connection kiosk mode", "edit", false, true],
|
|
384
|
+
] as const)(
|
|
385
|
+
"shows the setup command when installation is not allowed in %s",
|
|
386
|
+
async (_label, mode, kioskMode, kioskRequested) => {
|
|
387
|
+
store.set(viewStateAtom, { mode, cellAnchor: null });
|
|
388
|
+
store.set(kioskModeAtom, kioskMode);
|
|
389
|
+
if (kioskRequested) {
|
|
390
|
+
window.history.replaceState(null, "", "/?kiosk=true");
|
|
391
|
+
}
|
|
392
|
+
store.set(
|
|
393
|
+
requestClientAtom,
|
|
394
|
+
MockRequestClient.create({
|
|
395
|
+
getExportAvailability: vi.fn().mockResolvedValue(
|
|
396
|
+
serverAvailability(
|
|
397
|
+
formatAvailability("pdf", {
|
|
398
|
+
dependenciesAvailable: false,
|
|
399
|
+
missingSetup: [PLAYWRIGHT_SETUP],
|
|
400
|
+
}),
|
|
401
|
+
),
|
|
402
|
+
),
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
renderDialog("pdf");
|
|
407
|
+
|
|
408
|
+
expect(
|
|
409
|
+
await screen.findByText("PDF export requires Playwright Chromium."),
|
|
410
|
+
).toBeVisible();
|
|
411
|
+
expect(
|
|
412
|
+
screen.getByText("uv run playwright install chromium"),
|
|
413
|
+
).toBeVisible();
|
|
414
|
+
expect(
|
|
415
|
+
screen.queryByRole("button", { name: "Install" }),
|
|
416
|
+
).not.toBeInTheDocument();
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
it("allows requirement installation when kiosk is false", async () => {
|
|
421
|
+
window.history.replaceState(null, "", "/?kiosk=false");
|
|
422
|
+
store.set(
|
|
423
|
+
requestClientAtom,
|
|
424
|
+
MockRequestClient.create({
|
|
425
|
+
getExportAvailability: vi.fn().mockResolvedValue(
|
|
426
|
+
serverAvailability(
|
|
427
|
+
formatAvailability("pdf", {
|
|
428
|
+
dependenciesAvailable: false,
|
|
429
|
+
missingSetup: [PLAYWRIGHT_SETUP],
|
|
430
|
+
}),
|
|
431
|
+
),
|
|
432
|
+
),
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
renderDialog("pdf");
|
|
437
|
+
|
|
438
|
+
expect(
|
|
439
|
+
await screen.findByRole("button", { name: "Install" }),
|
|
309
440
|
).toBeVisible();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("restores the install action when installation fails", async () => {
|
|
444
|
+
const error = new HTTPError(500, "Server Error", {
|
|
445
|
+
detail: "Playwright Chromium installation failed. Check the server logs.",
|
|
446
|
+
});
|
|
447
|
+
const installExportRequirements = vi.fn().mockRejectedValue(error);
|
|
448
|
+
const getExportAvailability = vi.fn().mockResolvedValue(
|
|
449
|
+
serverAvailability(
|
|
450
|
+
formatAvailability("pdf", {
|
|
451
|
+
dependenciesAvailable: false,
|
|
452
|
+
missingPackages: ["nbconvert[webpdf]"],
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
store.set(
|
|
457
|
+
requestClientAtom,
|
|
458
|
+
createErrorToastingRequests(
|
|
459
|
+
MockRequestClient.create({
|
|
460
|
+
getExportAvailability,
|
|
461
|
+
installExportRequirements,
|
|
462
|
+
}),
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
renderDialog("pdf");
|
|
467
|
+
|
|
468
|
+
const install = await screen.findByRole("button", { name: "Install" });
|
|
469
|
+
fireEvent.click(install);
|
|
470
|
+
|
|
471
|
+
await waitFor(() =>
|
|
472
|
+
expect(toastMock).toHaveBeenCalledWith({
|
|
473
|
+
title: "Failed to install export requirements",
|
|
474
|
+
description:
|
|
475
|
+
"Playwright Chromium installation failed. Check the server logs.",
|
|
476
|
+
variant: "danger",
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
expect(install).toBeEnabled();
|
|
480
|
+
expect(install).toHaveAttribute("aria-busy", "false");
|
|
310
481
|
expect(screen.getByTestId("export-submit")).toBeDisabled();
|
|
482
|
+
expect(getExportAvailability).toHaveBeenCalledOnce();
|
|
311
483
|
});
|
|
312
484
|
|
|
313
485
|
it("announces requirement checks as status updates", () => {
|
|
@@ -28,13 +28,17 @@ export const ExportDialog: React.FC<{
|
|
|
28
28
|
const {
|
|
29
29
|
dialogRef,
|
|
30
30
|
isExporting,
|
|
31
|
+
isInstalling,
|
|
32
|
+
canInstall,
|
|
31
33
|
formats,
|
|
32
34
|
options,
|
|
33
35
|
selected,
|
|
34
36
|
selectFormat,
|
|
35
37
|
updateOptions,
|
|
38
|
+
installRequirements,
|
|
36
39
|
submit,
|
|
37
40
|
} = useExportDialog({ initialFormat, onClose });
|
|
41
|
+
const isBusy = isExporting || isInstalling;
|
|
38
42
|
const desktopLayout = useMediaQuery(DESKTOP_LAYOUT_QUERY);
|
|
39
43
|
const {
|
|
40
44
|
format,
|
|
@@ -86,7 +90,7 @@ export const ExportDialog: React.FC<{
|
|
|
86
90
|
<TabsTrigger
|
|
87
91
|
key={candidate}
|
|
88
92
|
value={candidate}
|
|
89
|
-
disabled={
|
|
93
|
+
disabled={isBusy}
|
|
90
94
|
className="min-w-0 justify-start gap-2 px-2.5 py-2 text-xs data-[state=active]:shadow-xs sm:w-full sm:text-sm"
|
|
91
95
|
data-testid={`export-format-${candidate}`}
|
|
92
96
|
>
|
|
@@ -119,13 +123,15 @@ export const ExportDialog: React.FC<{
|
|
|
119
123
|
format={format}
|
|
120
124
|
formatLabel={definition.label}
|
|
121
125
|
status={status}
|
|
126
|
+
onInstall={canInstall ? installRequirements : undefined}
|
|
127
|
+
isInstalling={isInstalling}
|
|
122
128
|
/>
|
|
123
129
|
|
|
124
130
|
{usesBrowserPrint ? null : (
|
|
125
131
|
<FormatOptions
|
|
126
132
|
options={options}
|
|
127
133
|
updateOptions={updateOptions}
|
|
128
|
-
disabled={
|
|
134
|
+
disabled={isBusy}
|
|
129
135
|
/>
|
|
130
136
|
)}
|
|
131
137
|
</TabsContent>
|
|
@@ -173,7 +179,7 @@ export const ExportDialog: React.FC<{
|
|
|
173
179
|
) : null}
|
|
174
180
|
<Button
|
|
175
181
|
type="button"
|
|
176
|
-
disabled={!status.available ||
|
|
182
|
+
disabled={!status.available || isBusy}
|
|
177
183
|
aria-busy={isExporting}
|
|
178
184
|
onClick={submit}
|
|
179
185
|
className={cn(
|