@marimo-team/islands 0.24.3-dev32 → 0.24.3-dev36
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-BLO7UDpo.js → ConnectedDataExplorerComponent-DeiKXaGB.js} +4 -4
- package/dist/{ErrorBoundary-B7uSdDUm.js → ErrorBoundary-B3IKS4qY.js} +1 -1
- package/dist/{_baseMerge-Bnytch8P.js → _baseMerge-BiGRwuqv.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-DCb2K_RB.js → abnfDiagram-VCTEODGH-DKNGRyhn.js} +2 -2
- package/dist/{any-language-editor-C0dftc2Z.js → any-language-editor-BdvsE_1G.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-Bb_n642s.js → architectureDiagram-5GKGNRK7-C9oCEyV-.js} +1 -1
- package/dist/{chat-ui-DiMxe-GH.js → chat-ui-D15itIKJ.js} +8 -8
- package/dist/{chunk-IMKFNOWR-Du9BN2Ul.js → chunk-IMKFNOWR-ByL0a1rh.js} +1 -1
- package/dist/{chunk-SHT3W25Y-tUelricH.js → chunk-SHT3W25Y-D1Nb5X1c.js} +1 -1
- package/dist/{chunk-SVP7TREG-DK3egkfQ.js → chunk-SVP7TREG-BShKxcfD.js} +1 -1
- package/dist/{chunk-TICWLB2K-DRANTdnp.js → chunk-TICWLB2K-zpDbKPt8.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-7wCSVH7o.js → classDiagram-ZZMXUADV-qcsoIIQw.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-QwnvcnhB.js → classDiagram-v2-VYDZK3BY-CAgMct5x.js} +2 -2
- package/dist/{common-Dxt3H8za.js → common-CWnP5Rod.js} +7921 -11807
- package/dist/{cynefinDiagram-5FMLGOSQ-CB7O4IZB.js → cynefinDiagram-5FMLGOSQ-BzBXigwN.js} +1 -1
- package/dist/{dagre-DfYd3btw.js → dagre-BGSfXNEb.js} +21 -21
- package/dist/{dagre-GXQ25YYZ-C-a95ISW.js → dagre-GXQ25YYZ-B-75J283.js} +2 -2
- package/dist/{data-grid-overlay-editor-C5eUZVTo.js → data-grid-overlay-editor-DMu5X3F1.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BsKLYQaH.js → diagram-S7CK7UJ4-Bzr5oWc0.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-ppYdvuTN.js → diagram-UQ7AKVKN-CVIH_HJ_.js} +1 -1
- package/dist/{diagram-VX7I27RA-CAMK2l0E.js → diagram-VX7I27RA-BmoMwYLC.js} +1 -1
- package/dist/{diagram-Z3DM3KII-CxQ7MggT.js → diagram-Z3DM3KII-BW1w6mnj.js} +1 -1
- package/dist/{dropdown-menu-C73aTomr.js → dropdown-menu-WMrB1UnI.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-DzybELvk.js → ebnfDiagram-PWID7BFC-90Q6gkC0.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-BVxV97ld.js → erDiagram-RLTQ6QDP-BZkLy3TR.js} +1 -1
- package/dist/{esm-blALgIVy.js → esm-qM9GvHZi.js} +293 -307
- package/dist/{flowDiagram-HODETNUW-BKvXsWYX.js → flowDiagram-HODETNUW-CD1F_wj5.js} +2 -2
- package/dist/{formats-vDAoRpYs.js → formats-B-gajYGs.js} +1 -1
- package/dist/{glide-data-editor-Bcy8iaZa.js → glide-data-editor-DzFEatOq.js} +2218 -2209
- package/dist/{glide-utils-BMnFm0vZ.js → glide-utils-4WmLlefP.js} +11 -11
- package/dist/{html-to-image-mVIbKn_Y.js → html-to-image-CZm_DMIE.js} +15 -15
- package/dist/{infoDiagram-27XIBGKW-B_ucqo-U.js → infoDiagram-27XIBGKW-t0zIIRVa.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-BZFx9b-o.js → ishikawaDiagram-5VMMS53U-DtQUIYgx.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BpIfcu3G.js → kanban-definition-UXKFOSKX-CUMmwuNB.js} +1 -1
- package/dist/main.js +5274 -5283
- package/dist/{mermaid-B1Jb9HfY.js → mermaid-CMLxoJ0m.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-CmpAfv8K.js → mindmap-definition-YA3MSWOX-DnbkiQla.js} +1 -1
- package/dist/{number-overlay-editor-CFofZaOi.js → number-overlay-editor-lGqRHkFt.js} +1 -1
- package/dist/{extends-Do6pRuJa.js → objectWithoutPropertiesLoose-BbiTb8zy.js} +12 -1
- package/dist/{pegDiagram-XKGWAZYB-DO-0Q8SE.js → pegDiagram-XKGWAZYB-qgqAYMdv.js} +2 -2
- package/dist/{pick-B7vX8zxG.js → pick-CJeoxYOS.js} +1 -1
- package/dist/{pieDiagram-E7YTZNPT-B1MbuA_g.js → pieDiagram-E7YTZNPT-DJUB6w6B.js} +1 -1
- package/dist/{process-output-Cr6U5ULK.js → process-output-DuupNR0m.js} +1 -1
- package/dist/{railroadDiagram-O6MQD6OU-DPVOQUXC.js → railroadDiagram-O6MQD6OU-C0dtw4sR.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CC7wZ-tM.js → requirementDiagram-BXWQKSXE-DqaTsUuC.js} +1 -1
- package/dist/{reveal-component-Dli5fs0u.js → reveal-component-SdzpGkJs.js} +486 -486
- package/dist/{sequenceDiagram-WJ2MYXX4-DomSyoJI.js → sequenceDiagram-WJ2MYXX4-D2NyWGuu.js} +1 -1
- package/dist/{spec-g740fOWu.js → spec-BiD5y7ft.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-SQLLjdtF.js → stateDiagram-D77RDMKH-JChw35bc.js} +3 -3
- package/dist/{stateDiagram-v2-MP3YSRHH-Dup4KkIr.js → stateDiagram-v2-MP3YSRHH-BfVY7OxA.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-ZXbhyGv6.js → swimlanes-42K2YHIH-eAB6utyY.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CluwxV0g.js → swimlanesDiagram-VR7AAH4N-Cnpd2YtA.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B8i6B9RR.js → timeline-definition-24CTP7MA-DlnODX3U.js} +1 -1
- package/dist/{toDate-C3JOXlJT.js → toDate-0cxDmdsp.js} +1 -1
- package/dist/{useAsyncData-C0f7qnnN.js → useAsyncData-DCIhDLCe.js} +1 -1
- package/dist/{useDeepCompareMemoize-QCa0HbWB.js → useDeepCompareMemoize-CLaZB7Gc.js} +1 -1
- package/dist/{useLifecycle-BGRBkTFT.js → useLifecycle-C1ViBACC.js} +1 -1
- package/dist/{useTheme-iQ0VoptU.js → useTheme-Da9LJvrQ.js} +1 -1
- package/dist/{vega-component-DRI4BDLQ.js → vega-component-Bt6d8B4Q.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-CS7lHhxI.js → vennDiagram-4TSXK5OY-B_m5KxUg.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-BSm-L8JI.js → wardleyDiagram-VM6X3IG4-Dw-Lgv3D.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-8fKCJGrJ.js → xychartDiagram-S5SC5T6Z-BjfRms1e.js} +1 -1
- package/package.json +3 -6
- package/src/components/data-table/column-summary/chart-spec-model.tsx +1 -1
- package/src/components/data-table/columns.tsx +64 -67
- package/src/components/editor/header/filename-input.tsx +2 -0
- package/src/components/editor/output/JsonOutput.tsx +182 -632
- package/src/components/editor/output/Outputs.css +49 -17
- package/src/components/editor/output/__tests__/JsonOutput-interactions.test.tsx +254 -0
- package/src/components/editor/output/__tests__/JsonOutput-mimetype.test.tsx +41 -46
- package/src/components/editor/output/__tests__/{json-output.test.ts → json-output-formatting.test.ts} +113 -75
- package/src/components/editor/output/__tests__/json-output-pagination.test.ts +252 -0
- package/src/components/editor/output/json-output/formatting.ts +167 -0
- package/src/components/editor/output/json-output/pagination.ts +155 -0
- package/src/components/editor/output/json-output/renderers.tsx +196 -0
- package/src/components/editor/output/json-output/themes.ts +52 -0
- package/src/hooks/useDebounce.ts +2 -0
- package/src/plugins/core/BadPlugin.tsx +3 -14
- package/src/plugins/core/registerReactComponent.tsx +0 -1
- package/src/plugins/impl/FormPlugin.tsx +2 -0
- package/src/plugins/impl/MatrixPlugin.tsx +2 -0
- package/src/plugins/impl/RangeSliderPlugin.tsx +2 -0
- package/src/plugins/impl/SliderPlugin.tsx +2 -0
- package/src/plugins/layout/JsonOutputPlugin.tsx +7 -10
- package/dist/emotion-is-prop-valid.esm-M57VsT8j.js +0 -14
- package/src/components/editor/output/EmotionCacheProvider.tsx +0 -32
|
@@ -20,15 +20,13 @@
|
|
|
20
20
|
@apply text-link;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
.output {
|
|
24
|
-
position: relative;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
.output > * {
|
|
28
24
|
max-width: 100%;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
.output {
|
|
28
|
+
position: relative;
|
|
29
|
+
|
|
32
30
|
/* Unset max-width on .markdown. Instead, we will apply
|
|
33
31
|
to individual elements so it does not affect UI elements in markdown. */
|
|
34
32
|
.markdown {
|
|
@@ -65,31 +63,65 @@
|
|
|
65
63
|
.marimo-json-output {
|
|
66
64
|
padding-top: 2px;
|
|
67
65
|
padding-bottom: 2px;
|
|
68
|
-
/* content-visibility is set by material UI;
|
|
69
|
-
* when set on an element that has overflow auto, causes abrupt jumps when
|
|
70
|
-
* scrolling in Chrome */
|
|
71
|
-
content-visibility: unset;
|
|
72
|
-
/* Coarse hack to stop children from overflowing; doesn't seem to affect
|
|
73
|
-
* margin collapse. */
|
|
74
66
|
overflow: auto;
|
|
75
67
|
overscroll-behavior: contain;
|
|
76
68
|
@apply text-xs;
|
|
77
69
|
|
|
78
|
-
.
|
|
70
|
+
.w-rjv-wrap > :not(:last-child) {
|
|
79
71
|
margin-top: 0.2rem;
|
|
80
72
|
margin-bottom: 0.2rem;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
.
|
|
84
|
-
|
|
75
|
+
.w-rjv-wrap {
|
|
76
|
+
padding-left: 0.9rem !important;
|
|
77
|
+
margin-left: 0.3rem !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.w-rjv-line > span:first-child,
|
|
81
|
+
.w-rjv-colon,
|
|
82
|
+
.w-rjv-inner > span,
|
|
83
|
+
&.w-rjv-inner > span {
|
|
84
|
+
opacity: 0.8;
|
|
85
|
+
letter-spacing: 0.03125rem;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.w-rjv-inner > div:last-child:not(.w-rjv-wrap),
|
|
90
|
+
&.w-rjv-inner > div:last-child:not(.w-rjv-wrap) {
|
|
91
|
+
line-height: 1.5;
|
|
92
|
+
padding-left: 0 !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.w-rjv-line > button:only-child {
|
|
96
|
+
line-height: 1.5;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.w-rjv-arrow {
|
|
100
|
+
transition: transform 0.3s !important;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
@media (prefers-reduced-motion: reduce) {
|
|
104
|
+
.w-rjv-arrow {
|
|
105
|
+
transition: none !important;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.w-rjv-object-size {
|
|
110
|
+
padding-left: 4px !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.copy-button {
|
|
114
|
+
opacity: 0;
|
|
89
115
|
}
|
|
90
116
|
|
|
91
|
-
.
|
|
92
|
-
|
|
117
|
+
.copy-button:focus-visible,
|
|
118
|
+
.w-rjv-line:hover > .copy-button,
|
|
119
|
+
.w-rjv-inner:hover > span > .copy-button,
|
|
120
|
+
/* Root element has both .marimo-json-output and .w-rjv-inner on the same node */
|
|
121
|
+
&.w-rjv-inner:hover
|
|
122
|
+
> span
|
|
123
|
+
> .copy-button {
|
|
124
|
+
opacity: 1;
|
|
93
125
|
}
|
|
94
126
|
}
|
|
95
127
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { copyToClipboard } from "@/utils/copy";
|
|
5
|
+
import { JsonOutput } from "../JsonOutput";
|
|
6
|
+
|
|
7
|
+
vi.mock("@/utils/copy", () => ({
|
|
8
|
+
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("JsonOutput interactions", () => {
|
|
12
|
+
it("copies all descendants of a paginated parent and shows the real size", async () => {
|
|
13
|
+
const data = { rows: Array.from({ length: 120 }, (_, i) => i) };
|
|
14
|
+
const { getAllByRole, getByText } = render(
|
|
15
|
+
<JsonOutput data={data} valueTypes="json" />,
|
|
16
|
+
);
|
|
17
|
+
expect(getByText("120 Items")).toBeInTheDocument();
|
|
18
|
+
fireEvent.click(getAllByRole("button", { name: "Copy to clipboard" })[0]);
|
|
19
|
+
await waitFor(() =>
|
|
20
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(
|
|
21
|
+
JSON.stringify(data, null, 2),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("paginates independently and resets when the data changes", () => {
|
|
27
|
+
const values = Array.from({ length: 70 }, (_, i) => i);
|
|
28
|
+
const { getAllByRole, queryByRole, rerender } = render(
|
|
29
|
+
<JsonOutput data={{ "a.b": values, a: { b: values } }} />,
|
|
30
|
+
);
|
|
31
|
+
fireEvent.click(getAllByRole("button", { name: "... 40 more items" })[0]);
|
|
32
|
+
expect(getAllByRole("button", { name: "... 40 more items" })).toHaveLength(
|
|
33
|
+
1,
|
|
34
|
+
);
|
|
35
|
+
expect(
|
|
36
|
+
queryByRole("button", { name: "... 10 more items" }),
|
|
37
|
+
).toBeInTheDocument();
|
|
38
|
+
rerender(<JsonOutput data={{ "a.b": values, a: { b: values } }} />);
|
|
39
|
+
expect(
|
|
40
|
+
queryByRole("button", { name: "... 10 more items" }),
|
|
41
|
+
).not.toBeInTheDocument();
|
|
42
|
+
expect(getAllByRole("button", { name: "... 40 more items" })).toHaveLength(
|
|
43
|
+
2,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("exposes expansion controls and long text as buttons", () => {
|
|
48
|
+
const text = "a".repeat(150);
|
|
49
|
+
const { getByRole, getAllByRole, queryByText } = render(
|
|
50
|
+
<JsonOutput data={{ text, plain: `text/plain:${text}` }} />,
|
|
51
|
+
);
|
|
52
|
+
const root = getByRole("button", { name: "Collapse root" });
|
|
53
|
+
expect(root).toHaveAttribute("aria-expanded", "true");
|
|
54
|
+
fireEvent.click(root);
|
|
55
|
+
expect(queryByText("plain")).not.toBeInTheDocument();
|
|
56
|
+
const collapsedRoot = getByRole("button", { name: "Expand root" });
|
|
57
|
+
expect(collapsedRoot).toHaveAttribute("aria-expanded", "false");
|
|
58
|
+
fireEvent.click(collapsedRoot);
|
|
59
|
+
expect(getByRole("button", { name: "Collapse root" })).toHaveAttribute(
|
|
60
|
+
"aria-expanded",
|
|
61
|
+
"true",
|
|
62
|
+
);
|
|
63
|
+
expect(queryByText("plain")).toBeInTheDocument();
|
|
64
|
+
const buttons = getAllByRole("button", { name: `${"a".repeat(100)}...` });
|
|
65
|
+
expect(buttons).toHaveLength(2);
|
|
66
|
+
fireEvent.keyDown(buttons[0], { key: "Enter" });
|
|
67
|
+
expect(buttons[0]).toHaveAttribute("aria-expanded", "true");
|
|
68
|
+
fireEvent.keyDown(buttons[0], { key: " " });
|
|
69
|
+
expect(buttons[0]).toHaveAttribute("aria-expanded", "false");
|
|
70
|
+
});
|
|
71
|
+
it("initially collapses containers below the fifth level", () => {
|
|
72
|
+
const data = {
|
|
73
|
+
one: { two: { three: { four: { five: { leaf: "value" } } } } },
|
|
74
|
+
};
|
|
75
|
+
const { getByRole, queryByText, getByText } = render(
|
|
76
|
+
<JsonOutput data={data} />,
|
|
77
|
+
);
|
|
78
|
+
expect(queryByText("leaf")).not.toBeInTheDocument();
|
|
79
|
+
const collapsed = getByRole("button", { name: "Expand five" });
|
|
80
|
+
expect(collapsed).toHaveAttribute("aria-expanded", "false");
|
|
81
|
+
fireEvent.click(collapsed);
|
|
82
|
+
expect(getByText("leaf")).toBeInTheDocument();
|
|
83
|
+
const expanded = getByRole("button", { name: "Collapse five" });
|
|
84
|
+
expect(expanded).toHaveAttribute("aria-expanded", "true");
|
|
85
|
+
fireEvent.click(expanded);
|
|
86
|
+
expect(queryByText("leaf")).not.toBeInTheDocument();
|
|
87
|
+
expect(getByRole("button", { name: "Expand five" })).toHaveAttribute(
|
|
88
|
+
"aria-expanded",
|
|
89
|
+
"false",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
it.each([null, undefined, true, 42, Number.NaN, "text"])(
|
|
93
|
+
"renders scalar %j without tree state",
|
|
94
|
+
(data) => {
|
|
95
|
+
const { container } = render(<JsonOutput data={data} />);
|
|
96
|
+
expect(container.textContent).toBe(JSON.stringify(data) ?? "");
|
|
97
|
+
expect(container.querySelector(".marimo-json-output")).toBeNull();
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it("updates Python and JSON renderers when the mode changes", () => {
|
|
102
|
+
const data = {
|
|
103
|
+
yes: true,
|
|
104
|
+
no: false,
|
|
105
|
+
none: null,
|
|
106
|
+
"text/plain+int:2": "value",
|
|
107
|
+
};
|
|
108
|
+
const { container, rerender } = render(<JsonOutput data={data} />);
|
|
109
|
+
expect(container.textContent).toContain("True");
|
|
110
|
+
expect(container.textContent).not.toContain("text/plain+int:2");
|
|
111
|
+
rerender(<JsonOutput data={data} valueTypes="json" />);
|
|
112
|
+
expect(container.textContent).toContain("true");
|
|
113
|
+
expect(container.textContent).toContain("false");
|
|
114
|
+
expect(container.textContent).toContain("null");
|
|
115
|
+
expect(container.textContent).not.toContain("True");
|
|
116
|
+
expect(container.textContent).toContain('"text/plain+int:2"');
|
|
117
|
+
rerender(<JsonOutput data={data} valueTypes="python" />);
|
|
118
|
+
expect(container.textContent).toContain("True");
|
|
119
|
+
expect(container.textContent).not.toContain("text/plain+int:2");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.each(["python", "json"] as const)(
|
|
123
|
+
"copies marker-like user strings in %s mode",
|
|
124
|
+
async (valueTypes) => {
|
|
125
|
+
const data = {
|
|
126
|
+
marker: "\u0000show_more\u000042|$",
|
|
127
|
+
replacement: "<marimo-replace>None</marimo-replace>",
|
|
128
|
+
};
|
|
129
|
+
const { getAllByRole } = render(
|
|
130
|
+
<JsonOutput data={data} valueTypes={valueTypes} />,
|
|
131
|
+
);
|
|
132
|
+
expect(
|
|
133
|
+
getAllByRole("button", { name: "Copy to clipboard" }),
|
|
134
|
+
).toHaveLength(3);
|
|
135
|
+
fireEvent.click(getAllByRole("button", { name: "Copy to clipboard" })[0]);
|
|
136
|
+
await waitFor(() =>
|
|
137
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(
|
|
138
|
+
JSON.stringify(data, null, 2),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
it("paginates arrays after expanding beyond the former depth cutoff", () => {
|
|
145
|
+
const data = {
|
|
146
|
+
a: {
|
|
147
|
+
b: {
|
|
148
|
+
c: {
|
|
149
|
+
d: { e: { f: { rows: Array.from({ length: 100 }, (_, i) => i) } } },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const { getByRole, getByText, queryByRole } = render(
|
|
155
|
+
<JsonOutput data={data} />,
|
|
156
|
+
);
|
|
157
|
+
for (const name of ["e", "f", "rows"]) {
|
|
158
|
+
fireEvent.click(getByRole("button", { name: `Expand ${name}` }));
|
|
159
|
+
}
|
|
160
|
+
expect(getByText("100 Items")).toBeInTheDocument();
|
|
161
|
+
fireEvent.click(getByRole("button", { name: "... 70 more items" }));
|
|
162
|
+
expect(
|
|
163
|
+
getByRole("button", { name: "... 40 more items" }),
|
|
164
|
+
).toBeInTheDocument();
|
|
165
|
+
expect(
|
|
166
|
+
queryByRole("button", { name: "... 70 more items" }),
|
|
167
|
+
).not.toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("JsonOutput interaction regressions", () => {
|
|
172
|
+
it("removes the page control after the last page without collapsing the tree", () => {
|
|
173
|
+
const data = Array.from({ length: 31 }, (_, i) => i);
|
|
174
|
+
const { getByRole, queryByRole, getByText } = render(
|
|
175
|
+
<JsonOutput data={data} />,
|
|
176
|
+
);
|
|
177
|
+
fireEvent.click(getByRole("button", { name: "... 1 more items" }));
|
|
178
|
+
expect(
|
|
179
|
+
queryByRole("button", { name: /more items/ }),
|
|
180
|
+
).not.toBeInTheDocument();
|
|
181
|
+
expect(getByRole("button", { name: "Collapse root" })).toBeInTheDocument();
|
|
182
|
+
expect(getByText("31 Items")).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("does not reuse a previous dataset's page size", () => {
|
|
186
|
+
const matrix = Array.from({ length: 10 }, () =>
|
|
187
|
+
Array.from({ length: 60 }, (_, i) => i),
|
|
188
|
+
);
|
|
189
|
+
const { rerender, getByRole, queryByRole } = render(
|
|
190
|
+
<JsonOutput data={matrix} />,
|
|
191
|
+
);
|
|
192
|
+
const data = Array.from({ length: 70 }, (_, i) => i);
|
|
193
|
+
rerender(<JsonOutput data={data} />);
|
|
194
|
+
fireEvent.click(getByRole("button", { name: "... 40 more items" }));
|
|
195
|
+
expect(
|
|
196
|
+
getByRole("button", { name: "... 10 more items" }),
|
|
197
|
+
).toBeInTheDocument();
|
|
198
|
+
expect(
|
|
199
|
+
queryByRole("button", { name: "... 35 more items" }),
|
|
200
|
+
).not.toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("does not copy stale data or use the previous display mode", async () => {
|
|
204
|
+
const { rerender, getAllByRole, getByRole } = render(
|
|
205
|
+
<JsonOutput data={{ old: true }} />,
|
|
206
|
+
);
|
|
207
|
+
const data = { fresh: false, text: "text/plain:literal" };
|
|
208
|
+
rerender(<JsonOutput data={data} valueTypes="json" />);
|
|
209
|
+
fireEvent.click(getAllByRole("button", { name: "Copy to clipboard" })[0]);
|
|
210
|
+
await waitFor(() =>
|
|
211
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(
|
|
212
|
+
JSON.stringify(data, null, 2),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
expect(getByRole("button", { name: "Collapse root" })).toBeInTheDocument();
|
|
216
|
+
rerender(<JsonOutput data={data} valueTypes="python" />);
|
|
217
|
+
fireEvent.click(getAllByRole("button", { name: "Copy to clipboard" })[0]);
|
|
218
|
+
await waitFor(() =>
|
|
219
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(
|
|
220
|
+
'{\n "fresh": False,\n "text": "literal"\n}',
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("does not retain decoded MIME text after switching to JSON", () => {
|
|
226
|
+
const data = {
|
|
227
|
+
tuple: "text/plain+tuple:[42]",
|
|
228
|
+
html: "text/html:<b>rich</b>",
|
|
229
|
+
};
|
|
230
|
+
const { container, rerender, getAllByRole } = render(
|
|
231
|
+
<JsonOutput data={data} />,
|
|
232
|
+
);
|
|
233
|
+
expect(container.querySelector("b")).toHaveTextContent("rich");
|
|
234
|
+
expect(getAllByRole("button", { name: "Copy to clipboard" })).toHaveLength(
|
|
235
|
+
2,
|
|
236
|
+
);
|
|
237
|
+
rerender(<JsonOutput data={data} valueTypes="json" />);
|
|
238
|
+
expect(container.querySelector("b")).toBeNull();
|
|
239
|
+
expect(container.textContent).toContain('"tuple":"text/plain+tuple:[42]"');
|
|
240
|
+
expect(container.textContent).toContain('"html":"text/html:<b>rich</b>"');
|
|
241
|
+
expect(getAllByRole("button", { name: "Copy to clipboard" })).toHaveLength(
|
|
242
|
+
3,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("ignores unrelated keys on long-text controls", () => {
|
|
247
|
+
const text = "x".repeat(150);
|
|
248
|
+
const { getByRole } = render(<JsonOutput data={{ text }} />);
|
|
249
|
+
const button = getByRole("button", { name: `${"x".repeat(100)}...` });
|
|
250
|
+
fireEvent.keyDown(button, { key: "Escape" });
|
|
251
|
+
fireEvent.keyDown(button, { key: "ArrowDown" });
|
|
252
|
+
expect(button).toHaveAttribute("aria-expanded", "false");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { render } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { JsonOutput } from "../JsonOutput";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(window, "matchMedia", {
|
|
9
|
-
writable: true,
|
|
10
|
-
value: vi.fn().mockImplementation((query) => ({
|
|
11
|
-
matches: false,
|
|
12
|
-
media: query,
|
|
13
|
-
onchange: null,
|
|
14
|
-
addListener: vi.fn(),
|
|
15
|
-
removeListener: vi.fn(),
|
|
16
|
-
addEventListener: vi.fn(),
|
|
17
|
-
removeEventListener: vi.fn(),
|
|
18
|
-
dispatchEvent: vi.fn(),
|
|
19
|
-
})),
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("JsonOutput with enhanced mimetype handling", () => {
|
|
24
|
-
it("should render data with various mimetypes without crashing", () => {
|
|
6
|
+
describe("JsonOutput MIME rendering", () => {
|
|
7
|
+
it("renders text, rich content, and Python collections", () => {
|
|
25
8
|
const data = {
|
|
26
9
|
text: "text/plain:Hello",
|
|
27
10
|
html: "text/html:<strong>Bold</strong>",
|
|
@@ -35,16 +18,42 @@ describe("JsonOutput with enhanced mimetype handling", () => {
|
|
|
35
18
|
|
|
36
19
|
const { container } = render(<JsonOutput data={data} format="auto" />);
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
expect(container).
|
|
40
|
-
expect(container.querySelector("
|
|
21
|
+
expect(container.textContent).toContain('"text":Hello');
|
|
22
|
+
expect(container.querySelector("strong")).toHaveTextContent("Bold");
|
|
23
|
+
expect(container.querySelector("img")).toHaveAttribute(
|
|
24
|
+
"src",
|
|
25
|
+
"data:image/png;base64,xyz...",
|
|
26
|
+
);
|
|
27
|
+
expect(container.textContent).toContain('"set":{1, 2, 3}');
|
|
28
|
+
expect(container.textContent).toContain('"tuple":(10, 20)');
|
|
29
|
+
expect(container.textContent).toContain("application/custom:data");
|
|
30
|
+
expect(container.textContent).toContain('"boolean":True');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps MIME strings literal in JSON mode", () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<JsonOutput
|
|
36
|
+
data={{ value: "text/plain+tuple:[42]" }}
|
|
37
|
+
valueTypes="json"
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
expect(container.textContent).toContain('"value":"text/plain+tuple:[42]"');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders singleton tuples and escaped collection strings", () => {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<JsonOutput
|
|
46
|
+
data={{
|
|
47
|
+
tuple: "text/plain+tuple:[42]",
|
|
48
|
+
set: 'text/plain+set:["text/plain:literal"]',
|
|
49
|
+
}}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
expect(container.textContent).toContain('"tuple":(42,)');
|
|
53
|
+
expect(container.textContent).toContain('"set":{"text/plain:literal"}');
|
|
41
54
|
});
|
|
42
55
|
|
|
43
56
|
it("renders encoded non-string keys with Python-style affordances", () => {
|
|
44
|
-
// Server-side `_key_formatter` encodes non-string dict keys with
|
|
45
|
-
// mimetype prefixes; the frontend `keyRenderer` must decode them
|
|
46
|
-
// so users see the original Python types (unquoted ints, parens for
|
|
47
|
-
// tuples, etc.) instead of the raw encoded strings.
|
|
48
57
|
const data = {
|
|
49
58
|
"text/plain+int:2": "int_val",
|
|
50
59
|
"text/plain+float:2.5": "float_val",
|
|
@@ -59,34 +68,23 @@ describe("JsonOutput with enhanced mimetype handling", () => {
|
|
|
59
68
|
const { container } = render(<JsonOutput data={data} format="tree" />);
|
|
60
69
|
const text = container.textContent ?? "";
|
|
61
70
|
|
|
62
|
-
// `text/plain+str:` is the escape prefix — must never survive in output.
|
|
63
71
|
expect(text).not.toContain("text/plain+str:");
|
|
64
|
-
// Other encoded prefixes must not leak as-is. (They can still appear
|
|
65
|
-
// inside the unescaped original string key `"text/plain+int:2"`,
|
|
66
|
-
// which is intentional — but not for types *other* than int.)
|
|
67
72
|
expect(text).not.toContain("text/plain+bool:True");
|
|
68
73
|
expect(text).not.toContain("text/plain+tuple:[");
|
|
69
74
|
expect(text).not.toContain("text/plain+frozenset:[");
|
|
70
75
|
expect(text).not.toContain("text/plain+none:");
|
|
71
76
|
|
|
72
|
-
// Decoded visual forms are present with Python-style affordances.
|
|
73
77
|
expect(text).toContain('None:"none_val"');
|
|
74
78
|
expect(text).toContain('True:"bool_val"');
|
|
75
79
|
expect(text).toContain('2:"int_val"');
|
|
76
80
|
expect(text).toContain('2.5:"float_val"');
|
|
77
81
|
expect(text).toContain('(1, 2):"tuple_val"');
|
|
78
82
|
expect(text).toContain('frozenset({3, 4}):"fs_val"');
|
|
79
|
-
// Escaped str key renders as the original literal string (quoted).
|
|
80
83
|
expect(text).toContain('"text/plain+int:2":"escaped_str_val"');
|
|
81
|
-
// Plain string key unchanged.
|
|
82
84
|
expect(text).toContain('"plain":"unchanged"');
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
it("renders 1-tuple and empty-frozenset keys with correct Python syntax", () => {
|
|
86
|
-
// Regressions caught in review: `(1)` is not a tuple (needs `(1,)`),
|
|
87
|
-
// and `frozenset({})` reads like constructing from an empty dict
|
|
88
|
-
// (should be `frozenset()`). Locks in the tree-view rendering so these
|
|
89
|
-
// don't slip back.
|
|
90
88
|
const data = {
|
|
91
89
|
"text/plain+tuple:[42]": "one_tuple",
|
|
92
90
|
"text/plain+tuple:[]": "empty_tuple",
|
|
@@ -97,15 +95,13 @@ describe("JsonOutput with enhanced mimetype handling", () => {
|
|
|
97
95
|
const { container } = render(<JsonOutput data={data} format="tree" />);
|
|
98
96
|
const text = container.textContent ?? "";
|
|
99
97
|
|
|
100
|
-
expect(text).toContain('(42,):"one_tuple"');
|
|
98
|
+
expect(text).toContain('(42,):"one_tuple"');
|
|
101
99
|
expect(text).toContain('():"empty_tuple"');
|
|
102
|
-
expect(text).toContain('frozenset():"empty_fs"');
|
|
100
|
+
expect(text).toContain('frozenset():"empty_fs"');
|
|
103
101
|
expect(text).toContain('frozenset({1}):"one_fs"');
|
|
104
102
|
});
|
|
105
103
|
|
|
106
104
|
it("quotes integer-like string keys to distinguish them from int keys", () => {
|
|
107
|
-
// Without this, `"2"` and the decoded int `2` look identical — the
|
|
108
|
-
// textea viewer drops quotes from integer-like string keys by default.
|
|
109
105
|
const data = {
|
|
110
106
|
"2": "string_two",
|
|
111
107
|
"text/plain+int:2": "int_two",
|
|
@@ -114,9 +110,8 @@ describe("JsonOutput with enhanced mimetype handling", () => {
|
|
|
114
110
|
const { container } = render(<JsonOutput data={data} format="tree" />);
|
|
115
111
|
const text = container.textContent ?? "";
|
|
116
112
|
|
|
117
|
-
expect(text).toContain('"2":"string_two"');
|
|
118
|
-
expect(text).toContain('2:"int_two"');
|
|
119
|
-
|
|
120
|
-
expect(text).not.toContain("text/plain+"); // prefix stripped from int key
|
|
113
|
+
expect(text).toContain('"2":"string_two"');
|
|
114
|
+
expect(text).toContain('2:"int_two"');
|
|
115
|
+
expect(text).not.toContain("text/plain+");
|
|
121
116
|
});
|
|
122
117
|
});
|