@marimo-team/islands 0.24.3-dev39 → 0.24.3-dev40
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/{ConnectedDataExplorerComponent-DeiKXaGB.js → ConnectedDataExplorerComponent-KVhjDMSe.js} +1062 -1062
- package/dist/ErrorBoundary-CacTZz7K.js +200 -0
- package/dist/{abnfDiagram-VCTEODGH-DKNGRyhn.js → abnfDiagram-VCTEODGH-CpN0FRda.js} +2 -2
- package/dist/{any-language-editor-BdvsE_1G.js → any-language-editor-CycIVIeP.js} +13 -13
- package/dist/{architectureDiagram-5GKGNRK7-C9oCEyV-.js → architectureDiagram-5GKGNRK7-DDyj5SRB.js} +1 -1
- package/dist/{html-to-image-CZm_DMIE.js → cells-BSgNCBZ8.js} +5177 -8831
- package/dist/{chat-ui-D15itIKJ.js → chat-ui-CygzVJ9s.js} +1345 -1394
- package/dist/{check-mcQyZmFG.js → check-B7cIK6gb.js} +1 -1
- package/dist/{chunk-IMKFNOWR-ByL0a1rh.js → chunk-IMKFNOWR-BfwTebD0.js} +1 -1
- package/dist/{chunk-SHT3W25Y-D1Nb5X1c.js → chunk-SHT3W25Y-BspJEJ5H.js} +1 -1
- package/dist/{chunk-SVP7TREG-BShKxcfD.js → chunk-SVP7TREG-DX3rFNTe.js} +1 -1
- package/dist/{chunk-TICWLB2K-zpDbKPt8.js → chunk-TICWLB2K-fVF7Uujg.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-qcsoIIQw.js → classDiagram-ZZMXUADV-Fe-V14Sc.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-CAgMct5x.js → classDiagram-v2-VYDZK3BY-xTDg7lOs.js} +2 -2
- package/dist/code-visibility-BBNwvBcV.js +138 -0
- package/dist/{common-eNTtBSo2.js → common-DM3lblo7.js} +5531 -5730
- package/dist/copy-icon-CLdENY__.js +190 -0
- package/dist/{cynefinDiagram-5FMLGOSQ-BzBXigwN.js → cynefinDiagram-5FMLGOSQ-CFQrqzSX.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-B-75J283.js → dagre-GXQ25YYZ-xHNAUTyG.js} +1 -1
- package/dist/{data-grid-overlay-editor-DMu5X3F1.js → data-grid-overlay-editor-DjRvSwGI.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-Bzr5oWc0.js → diagram-S7CK7UJ4-Bt1rPbAx.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-CVIH_HJ_.js → diagram-UQ7AKVKN-D1sq5FoZ.js} +1 -1
- package/dist/{diagram-VX7I27RA-BmoMwYLC.js → diagram-VX7I27RA-BMwGFVD6.js} +1 -1
- package/dist/{diagram-Z3DM3KII-BW1w6mnj.js → diagram-Z3DM3KII--KLDR9Pl.js} +1 -1
- package/dist/dialog-CgNq6y9-.js +94 -0
- package/dist/{objectWithoutPropertiesLoose-BbiTb8zy.js → dist-CO1AwwG_.js} +516 -572
- package/dist/{dropdown-menu-WMrB1UnI.js → dropdown-menu-CZgjYSAh.js} +1437 -2793
- package/dist/{ebnfDiagram-PWID7BFC-90Q6gkC0.js → ebnfDiagram-PWID7BFC-WnG4cvSQ.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-BZkLy3TR.js → erDiagram-RLTQ6QDP-DhzKiYHc.js} +1 -1
- package/dist/error-banner-B94dcDmP.js +391 -0
- package/dist/error-entries-iJn-Nryx.js +56 -0
- package/dist/{esm-qM9GvHZi.js → esm-h8f7-1CJ.js} +316 -315
- package/dist/feedback-button-BkFFL-wb.js +380 -0
- package/dist/{flowDiagram-HODETNUW-CD1F_wj5.js → flowDiagram-HODETNUW-6dMdB8pR.js} +2 -2
- package/dist/{formats-B-gajYGs.js → formats-BR5iGb4s.js} +1 -1
- package/dist/{glide-data-editor-DzFEatOq.js → glide-data-editor-CeAOHG6x.js} +117 -116
- package/dist/{glide-utils-4WmLlefP.js → glide-utils-B3rUT5PI.js} +2 -2
- package/dist/globals-C74RodJy.js +19 -0
- package/dist/html-to-image-kpuRfdfZ.js +3253 -0
- package/dist/{infoDiagram-27XIBGKW-t0zIIRVa.js → infoDiagram-27XIBGKW-BagtCX_5.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DtQUIYgx.js → ishikawaDiagram-5VMMS53U-DByDehYy.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CUMmwuNB.js → kanban-definition-UXKFOSKX-BxwkbBI_.js} +1 -1
- package/dist/{label-e37ixur5.js → label-BJ49YTvk.js} +1 -1
- package/dist/main.js +4717 -4728
- package/dist/{mermaid-CMLxoJ0m.js → mermaid-DPNuhemn.js} +35 -35
- package/dist/{mindmap-definition-YA3MSWOX-DnbkiQla.js → mindmap-definition-YA3MSWOX-Cgvojw3y.js} +1 -1
- package/dist/{number-overlay-editor-lGqRHkFt.js → number-overlay-editor-DDyIkEk3.js} +1 -1
- package/dist/objectWithoutPropertiesLoose-BRDcTsfB.js +23 -0
- package/dist/{pegDiagram-XKGWAZYB-qgqAYMdv.js → pegDiagram-XKGWAZYB-DahHhYyN.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-DJUB6w6B.js → pieDiagram-E7YTZNPT-CCtX0oCK.js} +1 -1
- package/dist/{process-output-DuupNR0m.js → process-output-DFe2tVIY.js} +7 -6
- package/dist/{railroadDiagram-O6MQD6OU-C0dtw4sR.js → railroadDiagram-O6MQD6OU-Du5NsPUb.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DqaTsUuC.js → requirementDiagram-BXWQKSXE-0pmmxL6_.js} +1 -1
- package/dist/{reveal-component-CNqmC7b6.js → reveal-component-CRy68FxJ.js} +625 -625
- package/dist/{select-UMOSCa3W.js → select-C6plhmZr.js} +308 -553
- package/dist/{sequenceDiagram-WJ2MYXX4-D2NyWGuu.js → sequenceDiagram-WJ2MYXX4-DB-ChWwU.js} +1 -1
- package/dist/{spec-BiD5y7ft.js → spec-CAYwFeV6.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-JChw35bc.js → stateDiagram-D77RDMKH-orN6AwMx.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-BfVY7OxA.js → stateDiagram-v2-MP3YSRHH-o05_rJN8.js} +2 -2
- package/dist/{swimlanes-42K2YHIH-eAB6utyY.js → swimlanes-42K2YHIH-BkSDcqDT.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-Cnpd2YtA.js → swimlanesDiagram-VR7AAH4N-D_nFQfRc.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-DlnODX3U.js → timeline-definition-24CTP7MA-BuqJUIRZ.js} +1 -1
- package/dist/{toDate-0cxDmdsp.js → toDate-WXg1yLcI.js} +2 -2
- package/dist/toast-DvY70eFX.js +1834 -0
- package/dist/tooltip-qB8NlSqV.js +1742 -0
- package/dist/{schemas-BDZr-nP0.js → use-restore-focus-D5D9xEjZ.js} +1385 -1166
- package/dist/{useAsyncData-DCIhDLCe.js → useAsyncData-BD6AStMt.js} +1 -1
- package/dist/{useDateFormatter-BKpQHt-W.js → useDateFormatter-BRTPsUK7.js} +2 -2
- package/dist/{useDeepCompareMemoize-CLaZB7Gc.js → useDeepCompareMemoize-D_HNdyvI.js} +1 -1
- package/dist/{useLifecycle-C1ViBACC.js → useLifecycle-DPgiiwwU.js} +2 -2
- package/dist/useNumberFormatter-BfDpa21j.js +251 -0
- package/dist/{useTheme-Da9LJvrQ.js → useTheme-DzXmYZ5V.js} +83 -83
- package/dist/{vega-component-Bt6d8B4Q.js → vega-component-D0QXopU8.js} +27 -27
- package/dist/{vennDiagram-4TSXK5OY-B_m5KxUg.js → vennDiagram-4TSXK5OY-CTUlJ2PK.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-Dw-Lgv3D.js → wardleyDiagram-VM6X3IG4-3evLUb0M.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-BjfRms1e.js → xychartDiagram-S5SC5T6Z-DL2E3K8-.js} +1 -1
- package/package.json +1 -1
- package/src/components/editor/boundary/ErrorBoundary.tsx +38 -16
- package/src/components/editor/boundary/__tests__/ErrorBoundary.test.tsx +311 -0
- package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +94 -0
- package/src/components/editor/chrome/components/feedback-button.tsx +53 -33
- package/src/components/ui/use-toast.ts +8 -0
- package/dist/ErrorBoundary-B3IKS4qY.js +0 -125
- package/dist/copy-C0N4m1lm.js +0 -141
- package/dist/dist-6ChWbo8z.js +0 -1360
- package/dist/error-banner-hCaH06e3.js +0 -611
- package/dist/tooltip-CdE8k0pS.js +0 -390
|
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
|
|
|
9
9
|
import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BUgAHvnM.js";
|
|
10
10
|
import "./chunk-PWAF6VOD-CTFCL0jI.js";
|
|
11
11
|
import { t as b } from "./chunk-GMAD6QVW-Czynz8aA.js";
|
|
12
|
-
import { p as x } from "./mermaid-
|
|
12
|
+
import { p as x } from "./mermaid-DPNuhemn.js";
|
|
13
13
|
//#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
|
|
14
14
|
function S() {
|
|
15
15
|
var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
|
package/package.json
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import
|
|
2
|
+
import { Provider } from "jotai";
|
|
3
|
+
import { type PropsWithChildren, lazy, Suspense, useState } from "react";
|
|
3
4
|
import {
|
|
4
5
|
type FallbackProps,
|
|
5
6
|
ErrorBoundary as ReactErrorBoundary,
|
|
6
7
|
} from "react-error-boundary";
|
|
7
|
-
import {
|
|
8
|
+
import { store } from "@/core/state/jotai";
|
|
8
9
|
import { Button } from "../../ui/button";
|
|
10
|
+
import { Dialog, DialogTrigger } from "../../ui/dialog";
|
|
11
|
+
import { Toaster } from "../../ui/toaster";
|
|
12
|
+
import { TooltipProvider } from "../../ui/tooltip";
|
|
13
|
+
|
|
14
|
+
const FeedbackModal = lazy(() =>
|
|
15
|
+
import("../chrome/components/feedback-button").then((mod) => ({
|
|
16
|
+
default: mod.FeedbackModal,
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
9
19
|
|
|
10
20
|
export const ErrorBoundary: React.FC<PropsWithChildren> = (props) => {
|
|
11
21
|
return (
|
|
@@ -16,26 +26,38 @@ export const ErrorBoundary: React.FC<PropsWithChildren> = (props) => {
|
|
|
16
26
|
};
|
|
17
27
|
|
|
18
28
|
const FallbackComponent: React.FC<FallbackProps> = (props) => {
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
|
|
19
31
|
return (
|
|
20
|
-
<div className="flex-1 flex items-center justify-center flex-col space-y-4 max-w-2xl mx-auto px-6">
|
|
32
|
+
<div className="flex-1 flex items-center justify-center flex-col space-y-4 max-w-2xl mx-auto px-6 pb-6">
|
|
21
33
|
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
|
22
34
|
<pre className="text-xs bg-muted/40 border rounded-md p-4 max-w-[80%] whitespace-normal">
|
|
23
35
|
{props.error?.message}
|
|
24
36
|
</pre>
|
|
25
|
-
<div>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
38
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
39
|
+
<DialogTrigger asChild={true}>
|
|
40
|
+
<Button variant="outline">Report an issue</Button>
|
|
41
|
+
</DialogTrigger>
|
|
42
|
+
{open && (
|
|
43
|
+
<Provider store={store}>
|
|
44
|
+
<TooltipProvider>
|
|
45
|
+
<Suspense fallback={null}>
|
|
46
|
+
<FeedbackModal />
|
|
47
|
+
</Suspense>
|
|
48
|
+
<Toaster />
|
|
49
|
+
</TooltipProvider>
|
|
50
|
+
</Provider>
|
|
51
|
+
)}
|
|
52
|
+
</Dialog>
|
|
53
|
+
<Button
|
|
54
|
+
data-testid="reset-error-boundary-button"
|
|
55
|
+
onClick={props.resetErrorBoundary}
|
|
56
|
+
variant="outline"
|
|
57
|
+
>
|
|
58
|
+
Try again
|
|
59
|
+
</Button>
|
|
31
60
|
</div>
|
|
32
|
-
<Button
|
|
33
|
-
data-testid="reset-error-boundary-button"
|
|
34
|
-
onClick={props.resetErrorBoundary}
|
|
35
|
-
variant="outline"
|
|
36
|
-
>
|
|
37
|
-
Try again
|
|
38
|
-
</Button>
|
|
39
61
|
</div>
|
|
40
62
|
);
|
|
41
63
|
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { PropsWithChildren, ReactNode } from "react";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
7
|
+
import { ModalProvider } from "@/components/modal/ImperativeModal";
|
|
8
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
9
|
+
import { clearToasts } from "@/components/ui/use-toast";
|
|
10
|
+
import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
|
|
11
|
+
import { viewStateAtom } from "@/core/mode";
|
|
12
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
13
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
14
|
+
import type { EnvironmentInfo } from "@/core/network/types";
|
|
15
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
16
|
+
import { store } from "@/core/state/jotai";
|
|
17
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
18
|
+
import { ErrorBoundary } from "../ErrorBoundary";
|
|
19
|
+
|
|
20
|
+
vi.mock("@/utils/copy", () => ({
|
|
21
|
+
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const environment: EnvironmentInfo = {
|
|
25
|
+
marimo: "1.2.3",
|
|
26
|
+
editable: false,
|
|
27
|
+
location: "~/.venv/site-packages/marimo",
|
|
28
|
+
OS: "Darwin",
|
|
29
|
+
"OS Version": "25.0",
|
|
30
|
+
Processor: "arm",
|
|
31
|
+
"Python Version": "3.12.9",
|
|
32
|
+
Locale: "en_US",
|
|
33
|
+
Binaries: { Browser: "chrome 140", Node: "v22", uv: "0.11" },
|
|
34
|
+
Dependencies: { click: "8.4.2" },
|
|
35
|
+
"Optional Dependencies": { pandas: "3.0.0" },
|
|
36
|
+
"Experimental Flags": {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CRASH_MESSAGE = "cell output crashed";
|
|
40
|
+
|
|
41
|
+
function CrashingChild(): never {
|
|
42
|
+
throw new Error(CRASH_MESSAGE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function RecoverableChild({ crash }: { crash: boolean }) {
|
|
46
|
+
if (crash) {
|
|
47
|
+
throw new Error(CRASH_MESSAGE);
|
|
48
|
+
}
|
|
49
|
+
return <p>recovered child</p>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function DescendantProviders({ children }: PropsWithChildren) {
|
|
53
|
+
return (
|
|
54
|
+
<TooltipProvider>
|
|
55
|
+
<ModalProvider>{children}</ModalProvider>
|
|
56
|
+
</TooltipProvider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isExpectedReactExceptionOutput(args: unknown[]): boolean {
|
|
61
|
+
return args.some((arg) => {
|
|
62
|
+
if (arg instanceof Error) {
|
|
63
|
+
return arg.message.includes(CRASH_MESSAGE);
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
typeof arg === "string" &&
|
|
67
|
+
(arg.includes(CRASH_MESSAGE) ||
|
|
68
|
+
arg.includes("The above error occurred") ||
|
|
69
|
+
arg.includes("React will try to recreate this component tree"))
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function suppressExpectedReactException() {
|
|
75
|
+
return vi.spyOn(console, "error").mockImplementation((...args) => {
|
|
76
|
+
if (isExpectedReactExceptionOutput(args)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unexpected console.error: ${args.map(String).join(" ")}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let consoleError: ReturnType<typeof vi.spyOn> | undefined;
|
|
84
|
+
|
|
85
|
+
function renderCrashedBoundary(child: ReactNode = <CrashingChild />) {
|
|
86
|
+
consoleError?.mockRestore();
|
|
87
|
+
consoleError = suppressExpectedReactException();
|
|
88
|
+
return render(<ErrorBoundary>{child}</ErrorBoundary>);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setRequestClient(
|
|
92
|
+
overrides?: Parameters<typeof MockRequestClient.create>[0],
|
|
93
|
+
) {
|
|
94
|
+
const client = MockRequestClient.create(overrides);
|
|
95
|
+
store.set(requestClientAtom, client);
|
|
96
|
+
return client;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function openReportDialog() {
|
|
100
|
+
const trigger = screen.getByRole("button", { name: "Report an issue" });
|
|
101
|
+
trigger.focus();
|
|
102
|
+
fireEvent.click(trigger);
|
|
103
|
+
return screen.findByRole("dialog");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function expectReportButtonFocused() {
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(
|
|
109
|
+
screen.getByRole("button", { name: "Report an issue" }),
|
|
110
|
+
).toHaveFocus();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function closeReportDialog(method: "escape" | "close") {
|
|
115
|
+
const dialog = await screen.findByRole("dialog");
|
|
116
|
+
if (method === "escape") {
|
|
117
|
+
fireEvent.keyDown(dialog, { key: "Escape" });
|
|
118
|
+
} else {
|
|
119
|
+
fireEvent.click(screen.getByRole("button", { name: "Close" }));
|
|
120
|
+
}
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
await expectReportButtonFocused();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function expectRealReportModal() {
|
|
128
|
+
const dialog = await openReportDialog();
|
|
129
|
+
expect(dialog).toHaveTextContent("Report an issue");
|
|
130
|
+
expect(screen.getByRole("link", { name: "Open GitHub issue" })).toBeVisible();
|
|
131
|
+
expect(
|
|
132
|
+
screen.getByRole("checkbox", { name: "Include errors" }),
|
|
133
|
+
).toBeVisible();
|
|
134
|
+
expect(
|
|
135
|
+
screen.getByRole("checkbox", { name: "Include notebook code" }),
|
|
136
|
+
).toBeVisible();
|
|
137
|
+
await screen.findByText("Environment details");
|
|
138
|
+
return dialog;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resetSharedState() {
|
|
142
|
+
localStorage.clear();
|
|
143
|
+
clearToasts();
|
|
144
|
+
store.set(requestClientAtom, null);
|
|
145
|
+
store.set(notebookAtom, initialNotebookState());
|
|
146
|
+
store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
|
|
147
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
148
|
+
store.set(filenameAtom, "/project/example.py");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe("ErrorBoundary report dialog", () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
resetSharedState();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
consoleError?.mockRestore();
|
|
158
|
+
consoleError = undefined;
|
|
159
|
+
resetSharedState();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("opens the report modal after a child render exception", async () => {
|
|
163
|
+
const { getEnvironmentInfo } = setRequestClient({
|
|
164
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
renderCrashedBoundary();
|
|
168
|
+
|
|
169
|
+
expect(screen.getByText("Something went wrong")).toBeVisible();
|
|
170
|
+
expect(screen.getByText("cell output crashed")).toBeVisible();
|
|
171
|
+
expect(screen.getByTestId("reset-error-boundary-button")).toBeVisible();
|
|
172
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
173
|
+
expect(getEnvironmentInfo).not.toHaveBeenCalled();
|
|
174
|
+
|
|
175
|
+
await expectRealReportModal();
|
|
176
|
+
await waitFor(() => expect(getEnvironmentInfo).toHaveBeenCalledOnce());
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("still opens one report dialog after descendant providers disappear", async () => {
|
|
180
|
+
const { getEnvironmentInfo } = setRequestClient({
|
|
181
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
renderCrashedBoundary(
|
|
185
|
+
<DescendantProviders>
|
|
186
|
+
<CrashingChild />
|
|
187
|
+
</DescendantProviders>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await expectRealReportModal();
|
|
191
|
+
expect(screen.getAllByRole("dialog")).toHaveLength(1);
|
|
192
|
+
await waitFor(() => expect(getEnvironmentInfo).toHaveBeenCalledOnce());
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("shows partial diagnostics when the request client is missing", async () => {
|
|
196
|
+
renderCrashedBoundary();
|
|
197
|
+
|
|
198
|
+
await openReportDialog();
|
|
199
|
+
await screen.findByText("Environment details");
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
screen.getByText("Server environment information unavailable"),
|
|
203
|
+
).toBeVisible();
|
|
204
|
+
expect(
|
|
205
|
+
screen.queryByText("Loading environment details…"),
|
|
206
|
+
).not.toBeInTheDocument();
|
|
207
|
+
expect(
|
|
208
|
+
screen.queryByRole("button", { name: "Retry" }),
|
|
209
|
+
).not.toBeInTheDocument();
|
|
210
|
+
expect(
|
|
211
|
+
screen.getByRole("checkbox", { name: "Include notebook code" }),
|
|
212
|
+
).toBeDisabled();
|
|
213
|
+
expect(screen.getByText("Notebook source is unavailable.")).toBeVisible();
|
|
214
|
+
const link = screen.getByRole("link", { name: "Open GitHub issue" });
|
|
215
|
+
expect(link.getAttribute("href") ?? "").toContain("&env=");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("does not request environment or source while the report dialog is closed", async () => {
|
|
219
|
+
localStorage.setItem(
|
|
220
|
+
"marimo:issue-report:include-code",
|
|
221
|
+
JSON.stringify(true),
|
|
222
|
+
);
|
|
223
|
+
const { getEnvironmentInfo, readCode } = setRequestClient({
|
|
224
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
225
|
+
readCode: vi.fn().mockResolvedValue({ contents: "import marimo" }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
renderCrashedBoundary();
|
|
229
|
+
|
|
230
|
+
expect(screen.getByText("Something went wrong")).toBeVisible();
|
|
231
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
232
|
+
expect(getEnvironmentInfo).not.toHaveBeenCalled();
|
|
233
|
+
expect(readCode).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("restores focus to the report button after closing with Escape", async () => {
|
|
237
|
+
setRequestClient({
|
|
238
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
239
|
+
});
|
|
240
|
+
renderCrashedBoundary();
|
|
241
|
+
await openReportDialog();
|
|
242
|
+
await closeReportDialog("escape");
|
|
243
|
+
expect(screen.getByText(CRASH_MESSAGE)).toBeVisible();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("restores focus to the report button after closing with the close control", async () => {
|
|
247
|
+
setRequestClient({
|
|
248
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
249
|
+
});
|
|
250
|
+
renderCrashedBoundary();
|
|
251
|
+
await openReportDialog();
|
|
252
|
+
await closeReportDialog("close");
|
|
253
|
+
expect(screen.getByText(CRASH_MESSAGE)).toBeVisible();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("reopens the report dialog while the original error remains", async () => {
|
|
257
|
+
setRequestClient({
|
|
258
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
259
|
+
});
|
|
260
|
+
renderCrashedBoundary();
|
|
261
|
+
await openReportDialog();
|
|
262
|
+
await closeReportDialog("escape");
|
|
263
|
+
expect(screen.getByText(CRASH_MESSAGE)).toBeVisible();
|
|
264
|
+
await expectRealReportModal();
|
|
265
|
+
expect(screen.getByText(CRASH_MESSAGE)).toBeVisible();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("restores the child after try again once the failure is cleared", () => {
|
|
269
|
+
setRequestClient({
|
|
270
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
271
|
+
});
|
|
272
|
+
const view = renderCrashedBoundary(<RecoverableChild crash={true} />);
|
|
273
|
+
expect(screen.getByText("Something went wrong")).toBeVisible();
|
|
274
|
+
|
|
275
|
+
view.rerender(
|
|
276
|
+
<ErrorBoundary>
|
|
277
|
+
<RecoverableChild crash={false} />
|
|
278
|
+
</ErrorBoundary>,
|
|
279
|
+
);
|
|
280
|
+
fireEvent.click(screen.getByTestId("reset-error-boundary-button"));
|
|
281
|
+
|
|
282
|
+
expect(screen.getByText("recovered child")).toBeVisible();
|
|
283
|
+
expect(screen.queryByText("Something went wrong")).not.toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not read notebook source when opened with fresh inclusion preferences", async () => {
|
|
287
|
+
const { readCode } = setRequestClient({
|
|
288
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
289
|
+
readCode: vi.fn().mockResolvedValue({ contents: "import marimo" }),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
renderCrashedBoundary();
|
|
293
|
+
await expectRealReportModal();
|
|
294
|
+
|
|
295
|
+
expect(readCode).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("renders the copy toast after the ancestor toaster is gone", async () => {
|
|
299
|
+
setRequestClient({
|
|
300
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
301
|
+
});
|
|
302
|
+
renderCrashedBoundary();
|
|
303
|
+
await expectRealReportModal();
|
|
304
|
+
|
|
305
|
+
fireEvent.click(
|
|
306
|
+
screen.getByRole("button", { name: "Copy environment JSON" }),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(await screen.findByText("Environment details copied")).toBeVisible();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -208,4 +208,98 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
208
208
|
expect(href).toContain(encodeURIComponent('"marimo": "1.2.3"'));
|
|
209
209
|
});
|
|
210
210
|
});
|
|
211
|
+
|
|
212
|
+
it("shows partial environment details when the request client is missing", async () => {
|
|
213
|
+
store.set(requestClientAtom, null);
|
|
214
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
215
|
+
|
|
216
|
+
await screen.findByText("Environment details");
|
|
217
|
+
expect(
|
|
218
|
+
screen.getByText("Server environment information unavailable"),
|
|
219
|
+
).toBeInTheDocument();
|
|
220
|
+
expect(
|
|
221
|
+
screen.queryByText("Loading environment details…"),
|
|
222
|
+
).not.toBeInTheDocument();
|
|
223
|
+
expect(
|
|
224
|
+
screen.queryByRole("button", { name: "Retry" }),
|
|
225
|
+
).not.toBeInTheDocument();
|
|
226
|
+
|
|
227
|
+
const link = screen.getByRole("link", { name: "Open GitHub issue" });
|
|
228
|
+
const href = link.getAttribute("href") ?? "";
|
|
229
|
+
expect(href).toContain("&env=");
|
|
230
|
+
expect(href).toContain(encodeURIComponent("Environment Collection Error"));
|
|
231
|
+
|
|
232
|
+
fireEvent.click(
|
|
233
|
+
screen.getByRole("button", { name: "Copy environment JSON" }),
|
|
234
|
+
);
|
|
235
|
+
await waitFor(() =>
|
|
236
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
237
|
+
expect.stringContaining("Environment Collection Error"),
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("disables source collection when the request client is missing", async () => {
|
|
243
|
+
localStorage.setItem(
|
|
244
|
+
"marimo:issue-report:include-code",
|
|
245
|
+
JSON.stringify(true),
|
|
246
|
+
);
|
|
247
|
+
store.set(connectionAtom, { state: WebSocketState.CONNECTING });
|
|
248
|
+
store.set(requestClientAtom, null);
|
|
249
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
250
|
+
|
|
251
|
+
await screen.findByText("Environment details");
|
|
252
|
+
expect(
|
|
253
|
+
screen.getByRole("checkbox", { name: "Include notebook code" }),
|
|
254
|
+
).toBeDisabled();
|
|
255
|
+
expect(
|
|
256
|
+
screen.getByText("Notebook source is unavailable."),
|
|
257
|
+
).toBeInTheDocument();
|
|
258
|
+
expect(
|
|
259
|
+
screen.queryByText("Connect the notebook to include its source."),
|
|
260
|
+
).not.toBeInTheDocument();
|
|
261
|
+
const link = screen.getByRole("link", { name: "Open GitHub issue" });
|
|
262
|
+
expect(link.getAttribute("href") ?? "").not.toContain("reproduction-code=");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("preserves partial diagnostics when the environment request throws", async () => {
|
|
266
|
+
store.set(
|
|
267
|
+
requestClientAtom,
|
|
268
|
+
MockRequestClient.create({
|
|
269
|
+
getEnvironmentInfo: vi.fn().mockImplementation(() => {
|
|
270
|
+
throw new Error("offline");
|
|
271
|
+
}),
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
275
|
+
|
|
276
|
+
await screen.findByText("Server environment information unavailable");
|
|
277
|
+
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
|
|
278
|
+
expect(
|
|
279
|
+
screen.getByText(/Environment Collection Error/),
|
|
280
|
+
).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("replaces partial diagnostics after a successful environment retry", async () => {
|
|
284
|
+
const getEnvironmentInfo = vi
|
|
285
|
+
.fn()
|
|
286
|
+
.mockRejectedValueOnce(new Error("offline"))
|
|
287
|
+
.mockResolvedValueOnce(environment);
|
|
288
|
+
store.set(
|
|
289
|
+
requestClientAtom,
|
|
290
|
+
MockRequestClient.create({ getEnvironmentInfo }),
|
|
291
|
+
);
|
|
292
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
293
|
+
|
|
294
|
+
await screen.findByText("Server environment information unavailable");
|
|
295
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
|
296
|
+
|
|
297
|
+
await screen.findByText(/"marimo": "1.2.3"/);
|
|
298
|
+
expect(
|
|
299
|
+
screen.queryByText("Server environment information unavailable"),
|
|
300
|
+
).not.toBeInTheDocument();
|
|
301
|
+
expect(
|
|
302
|
+
screen.queryByRole("button", { name: "Retry" }),
|
|
303
|
+
).not.toBeInTheDocument();
|
|
304
|
+
});
|
|
211
305
|
});
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
37
37
|
import { getMarimoVersion } from "@/core/meta/globals";
|
|
38
38
|
import { connectionAtom } from "@/core/network/connection";
|
|
39
|
-
import {
|
|
39
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
40
40
|
import { filenameAtom } from "@/core/saving/file-state";
|
|
41
41
|
import { store } from "@/core/state/jotai";
|
|
42
42
|
import { WebSocketState } from "@/core/websocket/types";
|
|
@@ -74,6 +74,27 @@ const CollapsiblePreview: React.FC<{ content: string }> = ({ content }) => {
|
|
|
74
74
|
);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
function getNotebookSourceUnavailableReason(args: {
|
|
78
|
+
hasRequestClient: boolean;
|
|
79
|
+
filename: string | null;
|
|
80
|
+
codeAvailable: boolean;
|
|
81
|
+
connectionState: WebSocketState;
|
|
82
|
+
}): string | undefined {
|
|
83
|
+
if (!args.hasRequestClient) {
|
|
84
|
+
return "Notebook source is unavailable.";
|
|
85
|
+
}
|
|
86
|
+
if (args.filename === null) {
|
|
87
|
+
return "Save the notebook first.";
|
|
88
|
+
}
|
|
89
|
+
if (!args.codeAvailable) {
|
|
90
|
+
return "Notebook source is hidden in this view.";
|
|
91
|
+
}
|
|
92
|
+
if (args.connectionState !== WebSocketState.OPEN) {
|
|
93
|
+
return "Connect the notebook to include its source.";
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
|
|
78
99
|
const { openModal, closeModal } = useImperativeModal();
|
|
79
100
|
|
|
@@ -85,13 +106,15 @@ export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
85
106
|
};
|
|
86
107
|
|
|
87
108
|
export const FeedbackModal: React.FC<{
|
|
88
|
-
onClose
|
|
109
|
+
onClose?: () => void;
|
|
89
110
|
}> = () => {
|
|
90
|
-
const
|
|
91
|
-
const environmentRequest = useAsyncData(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
const requestClient = useAtomValue(requestClientAtom);
|
|
112
|
+
const environmentRequest = useAsyncData(async () => {
|
|
113
|
+
if (requestClient == null) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return requestClient.getEnvironmentInfo();
|
|
117
|
+
}, [requestClient]);
|
|
95
118
|
|
|
96
119
|
const notebook = useAtomValue(notebookAtom);
|
|
97
120
|
const errors = getCellErrorEntries(store);
|
|
@@ -102,18 +125,13 @@ export const FeedbackModal: React.FC<{
|
|
|
102
125
|
const codeAvailable = useNotebookCodeAvailable(cells);
|
|
103
126
|
const filename = useAtomValue(filenameAtom);
|
|
104
127
|
const connection = useAtomValue(connectionAtom);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: filename === null
|
|
113
|
-
? "Save the notebook first."
|
|
114
|
-
: !codeAvailable
|
|
115
|
-
? "Notebook source is hidden in this view."
|
|
116
|
-
: "Connect the notebook to include its source.";
|
|
128
|
+
const notebookSourceReason = getNotebookSourceUnavailableReason({
|
|
129
|
+
hasRequestClient: requestClient != null,
|
|
130
|
+
filename,
|
|
131
|
+
codeAvailable,
|
|
132
|
+
connectionState: connection.state,
|
|
133
|
+
});
|
|
134
|
+
const notebookSourceAvailable = notebookSourceReason === undefined;
|
|
117
135
|
|
|
118
136
|
const [includeErrors, setIncludeErrors] = useLocalStorage(
|
|
119
137
|
"marimo:issue-report:include-errors",
|
|
@@ -127,7 +145,7 @@ export const FeedbackModal: React.FC<{
|
|
|
127
145
|
const environment: EnvironmentDiagnostics | undefined =
|
|
128
146
|
environmentRequest.data
|
|
129
147
|
? enrichEnvironment(environmentRequest.data, navigator.userAgent)
|
|
130
|
-
: environmentRequest.status === "error"
|
|
148
|
+
: environmentRequest.status === "error" || requestClient == null
|
|
131
149
|
? createPartialEnvironment(
|
|
132
150
|
getMarimoVersion(),
|
|
133
151
|
navigator.userAgent,
|
|
@@ -137,12 +155,12 @@ export const FeedbackModal: React.FC<{
|
|
|
137
155
|
: undefined;
|
|
138
156
|
|
|
139
157
|
const codeRequest = useAsyncData(async () => {
|
|
140
|
-
if (!includeCode || !notebookSourceAvailable) {
|
|
158
|
+
if (!includeCode || !notebookSourceAvailable || requestClient == null) {
|
|
141
159
|
return undefined;
|
|
142
160
|
}
|
|
143
|
-
const { contents } = await readCode();
|
|
161
|
+
const { contents } = await requestClient.readCode();
|
|
144
162
|
return contents;
|
|
145
|
-
}, [includeCode, notebookSourceAvailable,
|
|
163
|
+
}, [includeCode, notebookSourceAvailable, requestClient]);
|
|
146
164
|
|
|
147
165
|
let githubIssueUrl = Constants.bugReportUrl;
|
|
148
166
|
let omitted: string[] = [];
|
|
@@ -255,7 +273,7 @@ export const FeedbackModal: React.FC<{
|
|
|
255
273
|
)}
|
|
256
274
|
</div>
|
|
257
275
|
|
|
258
|
-
{environmentRequest.status === "pending" && (
|
|
276
|
+
{environmentRequest.status === "pending" && requestClient != null && (
|
|
259
277
|
<div className="flex flex-col gap-2">
|
|
260
278
|
<span className="text-sm text-muted-foreground">
|
|
261
279
|
Loading environment details…
|
|
@@ -266,18 +284,20 @@ export const FeedbackModal: React.FC<{
|
|
|
266
284
|
</div>
|
|
267
285
|
)}
|
|
268
286
|
|
|
269
|
-
{environmentRequest.status === "error" && (
|
|
287
|
+
{(environmentRequest.status === "error" || requestClient == null) && (
|
|
270
288
|
<div className="flex items-center gap-2 text-sm">
|
|
271
289
|
<TriangleAlertIcon className="w-4 h-4 text-(--yellow-11) shrink-0" />
|
|
272
290
|
<span>Server environment information unavailable</span>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
{requestClient != null && (
|
|
292
|
+
<Button
|
|
293
|
+
type="button"
|
|
294
|
+
variant="link"
|
|
295
|
+
size="xs"
|
|
296
|
+
onClick={() => environmentRequest.refetch()}
|
|
297
|
+
>
|
|
298
|
+
Retry
|
|
299
|
+
</Button>
|
|
300
|
+
)}
|
|
281
301
|
</div>
|
|
282
302
|
)}
|
|
283
303
|
|
|
@@ -161,6 +161,14 @@ function dispatch(action: Action) {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
export function clearToasts() {
|
|
165
|
+
for (const timeout of toastTimeouts.values()) {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
}
|
|
168
|
+
toastTimeouts.clear();
|
|
169
|
+
dispatch({ type: "REMOVE_TOAST" });
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
type Toast = Omit<ToasterToast, "id">;
|
|
165
173
|
|
|
166
174
|
function toast({
|