@marimo-team/islands 0.19.7-dev34 → 0.19.7-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.
Files changed (32) hide show
  1. package/dist/{glide-data-editor-DHsjQhtP.js → glide-data-editor-C3T7HsLi.js} +1 -1
  2. package/dist/main.js +33 -22
  3. package/dist/style.css +1 -1
  4. package/dist/{types-DBsIRhMX.js → types-CzEZ3EWT.js} +1 -1
  5. package/package.json +1 -1
  6. package/src/components/data-table/TableActions.tsx +5 -3
  7. package/src/components/data-table/download-actions.tsx +7 -2
  8. package/src/components/data-table/pagination.tsx +4 -4
  9. package/src/components/debug/indicator.tsx +1 -1
  10. package/src/components/editor/actions/useNotebookActions.tsx +4 -2
  11. package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +1 -1
  12. package/src/components/editor/chrome/wrapper/app-chrome.tsx +4 -4
  13. package/src/components/editor/chrome/wrapper/footer.tsx +1 -1
  14. package/src/components/editor/chrome/wrapper/sidebar.tsx +1 -1
  15. package/src/components/editor/controls/Controls.tsx +2 -2
  16. package/src/components/editor/controls/notebook-menu-dropdown.tsx +1 -1
  17. package/src/components/editor/file-tree/file-explorer.tsx +1 -1
  18. package/src/components/editor/header/status.tsx +1 -1
  19. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +13 -4
  20. package/src/components/home/components.tsx +1 -1
  21. package/src/components/static-html/static-banner.tsx +1 -1
  22. package/src/components/ui/dropdown-menu.tsx +1 -1
  23. package/src/components/ui/table.tsx +1 -1
  24. package/src/core/config/feature-flag.tsx +1 -1
  25. package/src/core/export/__tests__/hooks.test.ts +60 -58
  26. package/src/core/export/hooks.ts +71 -31
  27. package/src/css/app/print.css +0 -14
  28. package/src/utils/__tests__/async-capture-tracker.test.ts +353 -0
  29. package/src/utils/__tests__/download.test.tsx +5 -114
  30. package/src/utils/async-capture-tracker.ts +168 -0
  31. package/src/utils/download.ts +17 -57
  32. package/src/utils/html-to-image.ts +9 -12
@@ -5183,7 +5183,7 @@ var DropdownMenuContent = import_react.forwardRef((e4, t) => {
5183
5183
  let n = (0, import_compiler_runtime$3.c)(17), r, i, a, o;
5184
5184
  n[0] === e4 ? (r = n[1], i = n[2], a = n[3], o = n[4]) : ({ className: r, scrollable: a, sideOffset: o, ...i } = e4, n[0] = e4, n[1] = r, n[2] = i, n[3] = a, n[4] = o);
5185
5185
  let s = a === void 0 ? true : a, c = o === void 0 ? 4 : o, l;
5186
- n[5] !== r || n[6] !== s ? (l = cn(menuContentCommon(), "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", s && "overflow-auto", r), n[5] = r, n[6] = s, n[7] = l) : l = n[7];
5186
+ n[5] !== r || n[6] !== s ? (l = cn(menuContentCommon(), "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 print:hidden", s && "overflow-auto", r), n[5] = r, n[6] = s, n[7] = l) : l = n[7];
5187
5187
  let u = s ? `calc(var(--radix-dropdown-menu-content-available-height) - ${MAX_HEIGHT_OFFSET}px)` : void 0, d;
5188
5188
  n[8] !== i.style || n[9] !== u ? (d = {
5189
5189
  ...i.style,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.7-dev34",
3
+ "version": "0.19.7-dev36",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -107,13 +107,13 @@ export const TableActions = <TData,>({
107
107
  };
108
108
 
109
109
  return (
110
- <div className="flex items-center shrink-0 pt-1 no-print">
110
+ <div className="flex items-center shrink-0 pt-1">
111
111
  {onSearchQueryChange && enableSearch && (
112
112
  <Tooltip content="Search">
113
113
  <Button
114
114
  variant="text"
115
115
  size="xs"
116
- className="mb-0"
116
+ className="mb-0 print:hidden"
117
117
  onClick={() => setIsSearchEnabled(!isSearchEnabled)}
118
118
  >
119
119
  <SearchIcon className="w-4 h-4 text-muted-foreground" />
@@ -125,7 +125,7 @@ export const TableActions = <TData,>({
125
125
  <Button
126
126
  variant="text"
127
127
  size="xs"
128
- className="mb-0"
128
+ className="mb-0 print:hidden"
129
129
  onClick={toggleDisplayHeader}
130
130
  >
131
131
  <ChartSplineIcon className="w-4 h-4 text-muted-foreground" />
@@ -140,6 +140,7 @@ export const TableActions = <TData,>({
140
140
  variant="text"
141
141
  size="xs"
142
142
  onClick={() => togglePanel("row-viewer")}
143
+ className="print:hidden"
143
144
  >
144
145
  <PanelRightIcon
145
146
  className={cn(
@@ -156,6 +157,7 @@ export const TableActions = <TData,>({
156
157
  variant="text"
157
158
  size="xs"
158
159
  onClick={() => togglePanel("column-explorer")}
160
+ className="print:hidden"
159
161
  >
160
162
  <ChartColumnStacked
161
163
  className={cn(
@@ -87,7 +87,12 @@ export const DownloadAs: React.FC<DownloadActionProps> = (props) => {
87
87
  const { locale } = useLocale();
88
88
 
89
89
  const button = (
90
- <Button data-testid="download-as-button" size="xs" variant="link">
90
+ <Button
91
+ data-testid="download-as-button"
92
+ size="xs"
93
+ variant="link"
94
+ className="print:hidden"
95
+ >
91
96
  Download <ChevronDownIcon className="w-3 h-3 ml-1" />
92
97
  </Button>
93
98
  );
@@ -146,7 +151,7 @@ export const DownloadAs: React.FC<DownloadActionProps> = (props) => {
146
151
  return (
147
152
  <DropdownMenu modal={false}>
148
153
  <DropdownMenuTrigger asChild={true}>{button}</DropdownMenuTrigger>
149
- <DropdownMenuContent side="bottom" className="no-print">
154
+ <DropdownMenuContent side="bottom" className="print:hidden">
150
155
  {options.map((option) => (
151
156
  <DropdownMenuItem
152
157
  key={option.label}
@@ -67,7 +67,7 @@ export const DataTablePagination = <TData,>({
67
67
  size="xs"
68
68
  data-testid="select-all-button"
69
69
  variant="link"
70
- className="h-4"
70
+ className="h-4 print:hidden"
71
71
  onMouseDown={Events.preventFocus}
72
72
  onClick={() => {
73
73
  if (onSelectAllRowsChange) {
@@ -91,7 +91,7 @@ export const DataTablePagination = <TData,>({
91
91
  size="xs"
92
92
  data-testid="clear-selection-button"
93
93
  variant="link"
94
- className="h-4"
94
+ className="h-4 print:hidden"
95
95
  onMouseDown={Events.preventFocus}
96
96
  onClick={() => {
97
97
  if (!isCellSelection) {
@@ -139,7 +139,7 @@ export const DataTablePagination = <TData,>({
139
139
 
140
140
  const renderPageSizeSelector = () => {
141
141
  return (
142
- <div className="flex items-center gap-1 text-xs whitespace-nowrap mr-1">
142
+ <div className="flex items-center gap-1 text-xs whitespace-nowrap mr-1 print:hidden">
143
143
  <Select
144
144
  value={pageSize.toString()}
145
145
  onValueChange={(value) => table.setPageSize(Number(value))}
@@ -173,7 +173,7 @@ export const DataTablePagination = <TData,>({
173
173
  {showPageSizeSelector && renderPageSizeSelector()}
174
174
  </div>
175
175
 
176
- <div className="flex items-end space-x-2">
176
+ <div className="flex items-end space-x-2 print:hidden">
177
177
  <Button
178
178
  size="xs"
179
179
  variant="outline"
@@ -5,7 +5,7 @@ export const TailwindIndicator = () => {
5
5
  }
6
6
 
7
7
  return (
8
- <div className="fixed bottom-10 right-0 z-50 flex items-center justify-center bg-gray-800 py-[2px] px-1 font-mono text-[10px] text-white font-semibold">
8
+ <div className="fixed bottom-10 right-0 z-50 flex items-center justify-center bg-gray-800 py-[2px] px-1 font-mono text-[10px] text-white font-semibold print:hidden">
9
9
  <div className="block sm:hidden">xs</div>
10
10
  <div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
11
11
  sm
@@ -70,6 +70,7 @@ import { createShareableLink } from "@/core/wasm/share";
70
70
  import { isWasm } from "@/core/wasm/utils";
71
71
  import { copyToClipboard } from "@/utils/copy";
72
72
  import {
73
+ ADD_PRINTING_CLASS,
73
74
  downloadAsPDF,
74
75
  downloadBlob,
75
76
  downloadHTMLAsImage,
@@ -219,6 +220,8 @@ export function useNotebookActions() {
219
220
  await downloadHTMLAsImage({
220
221
  element: app,
221
222
  filename: document.title,
223
+ // Add body.printing ONLY when converting the whole notebook to a screenshot
224
+ prepare: ADD_PRINTING_CLASS,
222
225
  });
223
226
  },
224
227
  },
@@ -241,8 +244,7 @@ export function useNotebookActions() {
241
244
 
242
245
  const downloadPDF = async (progress: ProgressState) => {
243
246
  await updateCellOutputsWithScreenshots({
244
- takeScreenshots: () =>
245
- takeScreenshots({ progress, snappy: false }),
247
+ takeScreenshots: () => takeScreenshots({ progress }),
246
248
  updateCellOutputs,
247
249
  });
248
250
  await downloadAsPDF({
@@ -123,7 +123,7 @@ export const ContextAwarePanel: React.FC = () => {
123
123
  <>
124
124
  <PanelResizeHandle
125
125
  onDragging={handleDragging}
126
- className="resize-handle border-border z-20 no-print border-l"
126
+ className="resize-handle border-border z-20 print:hidden border-l"
127
127
  />
128
128
  <Panel defaultSize={20} minSize={15} maxSize={80}>
129
129
  {renderBody()}
@@ -227,7 +227,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
227
227
  <PanelResizeHandle
228
228
  onDragging={handleDragging}
229
229
  className={cn(
230
- "border-border no-print z-10",
230
+ "border-border print:hidden z-10",
231
231
  isSidebarOpen ? "resize-handle" : "resize-handle-collapsed",
232
232
  "vertical",
233
233
  )}
@@ -238,7 +238,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
238
238
  <PanelResizeHandle
239
239
  onDragging={handleDragging}
240
240
  className={cn(
241
- "border-border no-print z-20",
241
+ "border-border print:hidden z-20",
242
242
  isDeveloperPanelOpen ? "resize-handle" : "resize-handle-collapsed",
243
243
  "horizontal",
244
244
  )}
@@ -382,7 +382,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
382
382
  collapsedSize={0}
383
383
  collapsible={true}
384
384
  className={cn(
385
- "dark:bg-(--slate-1) no-print print:hidden hide-on-fullscreen",
385
+ "dark:bg-(--slate-1) print:hidden hide-on-fullscreen",
386
386
  isSidebarOpen && "border-r border-l border-(--slate-7)",
387
387
  )}
388
388
  minSize={10}
@@ -427,7 +427,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
427
427
  collapsedSize={0}
428
428
  collapsible={true}
429
429
  className={cn(
430
- "dark:bg-(--slate-1) no-print print:hidden hide-on-fullscreen",
430
+ "dark:bg-(--slate-1) print:hidden hide-on-fullscreen",
431
431
  isDeveloperPanelOpen && "border-t",
432
432
  )}
433
433
  minSize={10}
@@ -57,7 +57,7 @@ export const Footer: React.FC = () => {
57
57
  });
58
58
 
59
59
  return (
60
- <footer className="h-10 py-1 gap-1 bg-background flex items-center text-muted-foreground text-md pl-2 pr-1 border-t border-border select-none no-print text-sm z-50 print:hidden hide-on-fullscreen overflow-x-auto overflow-y-hidden scrollbar-thin">
60
+ <footer className="h-10 py-1 gap-1 bg-background flex items-center text-muted-foreground text-md pl-2 pr-1 border-t border-border select-none print:hidden text-sm z-50 hide-on-fullscreen overflow-x-auto overflow-y-hidden scrollbar-thin">
61
61
  <FooterItem
62
62
  className="h-full"
63
63
  tooltip={
@@ -115,7 +115,7 @@ export const Sidebar: React.FC = () => {
115
115
  ]);
116
116
 
117
117
  return (
118
- <div className="h-full pt-4 pb-1 px-1 flex flex-col items-start text-muted-foreground text-md select-none no-print text-sm z-50 dark:bg-background print:hidden hide-on-fullscreen">
118
+ <div className="h-full pt-4 pb-1 px-1 flex flex-col items-start text-muted-foreground text-md select-none text-sm z-50 dark:bg-background print:hidden hide-on-fullscreen">
119
119
  <ReorderableList<PanelDescriptor>
120
120
  value={sidebarItems}
121
121
  setValue={handleSetSidebarItems}
@@ -212,7 +212,7 @@ const StopControlButton = ({
212
212
  };
213
213
 
214
214
  const topRightControls =
215
- "absolute top-3 right-5 m-0 flex items-center gap-2 min-h-[28px] no-print pointer-events-auto z-30 print:hidden";
215
+ "absolute top-3 right-5 m-0 flex items-center gap-2 min-h-[28px] print:hidden pointer-events-auto z-30";
216
216
 
217
217
  const bottomRightControls =
218
- "absolute bottom-5 right-5 flex flex-col gap-2 items-center no-print pointer-events-auto z-30 print:hidden";
218
+ "absolute bottom-5 right-5 flex flex-col gap-2 items-center print:hidden pointer-events-auto z-30";
@@ -111,7 +111,7 @@ export const NotebookMenuDropdown: React.FC<Props> = ({
111
111
  <DropdownMenuTrigger asChild={true} disabled={disabled}>
112
112
  {button}
113
113
  </DropdownMenuTrigger>
114
- <DropdownMenuContent align="end" className="no-print w-[240px]">
114
+ <DropdownMenuContent align="end" className="print:hidden w-[240px]">
115
115
  {actions.map((action) => {
116
116
  if (action.hidden || action.redundant) {
117
117
  return null;
@@ -510,7 +510,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
510
510
  return (
511
511
  <DropdownMenuContent
512
512
  align="end"
513
- className="no-print w-[220px]"
513
+ className="print:hidden w-[220px]"
514
514
  onClick={(e) => e.stopPropagation()}
515
515
  onCloseAutoFocus={(e) => e.preventDefault()}
516
516
  >
@@ -34,7 +34,7 @@ export const StatusOverlay: React.FC<{
34
34
  );
35
35
  };
36
36
 
37
- const topLeftStatus = "no-print pointer-events-auto hover:cursor-pointer";
37
+ const topLeftStatus = "print:hidden pointer-events-auto hover:cursor-pointer";
38
38
 
39
39
  const DisconnectedIcon = () => (
40
40
  <Tooltip content="App disconnected">
@@ -41,7 +41,11 @@ import { downloadAsHTML } from "@/core/static/download-html";
41
41
  import { isStaticNotebook } from "@/core/static/static-state";
42
42
  import { isWasm } from "@/core/wasm/utils";
43
43
  import { cn } from "@/utils/cn";
44
- import { downloadBlob, downloadHTMLAsImage } from "@/utils/download";
44
+ import {
45
+ ADD_PRINTING_CLASS,
46
+ downloadBlob,
47
+ downloadHTMLAsImage,
48
+ } from "@/utils/download";
45
49
  import { Filenames } from "@/utils/filenames";
46
50
  import { FloatingOutline } from "../../chrome/panels/outline/floating-outline";
47
51
  import { cellDomProps } from "../../common";
@@ -185,7 +189,12 @@ const ActionButtons: React.FC<{
185
189
  if (!app) {
186
190
  return;
187
191
  }
188
- await downloadHTMLAsImage({ element: app, filename: document.title });
192
+ await downloadHTMLAsImage({
193
+ element: app,
194
+ filename: document.title,
195
+ // Add body.printing ONLY when converting the whole notebook to a screenshot
196
+ prepare: ADD_PRINTING_CLASS,
197
+ });
189
198
  };
190
199
 
191
200
  const handleDownloadAsHTML = async () => {
@@ -271,7 +280,7 @@ const ActionButtons: React.FC<{
271
280
  <div
272
281
  data-testid="notebook-actions-dropdown"
273
282
  className={cn(
274
- "right-0 top-0 z-50 m-4 no-print flex gap-2 print:hidden",
283
+ "right-0 top-0 z-50 m-4 print:hidden flex gap-2",
275
284
  // If the notebook is static, we have a banner at the top, so
276
285
  // we can't use fixed positioning. Ideally this is sticky, but the
277
286
  // current dom structure makes that difficult.
@@ -284,7 +293,7 @@ const ActionButtons: React.FC<{
284
293
  <MoreHorizontalIcon className="w-4 h-4" />
285
294
  </Button>
286
295
  </DropdownMenuTrigger>
287
- <DropdownMenuContent align="end" className="no-print w-[220px]">
296
+ <DropdownMenuContent align="end" className="print:hidden w-[220px]">
288
297
  {actions}
289
298
  </DropdownMenuContent>
290
299
  </DropdownMenu>
@@ -80,7 +80,7 @@ export const OpenTutorialDropDown: React.FC = () => {
80
80
  <CaretDownIcon className="w-3 h-3 ml-1" />
81
81
  </Button>
82
82
  </DropdownMenuTrigger>
83
- <DropdownMenuContent side="bottom" align="end" className="no-print">
83
+ <DropdownMenuContent side="bottom" align="end" className="print:hidden">
84
84
  {Objects.entries(TUTORIALS).map(
85
85
  ([tutorialId, [label, Icon, description]]) => (
86
86
  <DropdownMenuItem
@@ -36,7 +36,7 @@ export const StaticBanner: React.FC = () => {
36
36
 
37
37
  return (
38
38
  <div
39
- className="px-4 py-2 bg-(--sky-2) border-b border-(--sky-7) text-(--sky-11) flex justify-between items-center gap-4 no-print text-sm"
39
+ className="px-4 py-2 bg-(--sky-2) border-b border-(--sky-7) text-(--sky-11) flex justify-between items-center gap-4 print:hidden text-sm"
40
40
  data-testid="static-notebook-banner"
41
41
  >
42
42
  <span>
@@ -83,7 +83,7 @@ const DropdownMenuContent = React.forwardRef<
83
83
  sideOffset={sideOffset}
84
84
  className={cn(
85
85
  menuContentCommon(),
86
- "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
86
+ "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 print:hidden",
87
87
  scrollable && "overflow-auto",
88
88
  className,
89
89
  )}
@@ -7,7 +7,7 @@ const Table = React.forwardRef<
7
7
  HTMLTableElement,
8
8
  React.HTMLAttributes<HTMLTableElement>
9
9
  >(({ className, ...props }, ref) => (
10
- <div className="w-full overflow-auto scrollbar-thin flex-1">
10
+ <div className="w-full overflow-auto scrollbar-thin flex-1 print:overflow-hidden">
11
11
  <table
12
12
  ref={ref}
13
13
  className={cn("w-full caption-bottom text-sm", className)}
@@ -25,7 +25,7 @@ const defaultValues: ExperimentalFeatures = {
25
25
  chat_modes: false,
26
26
  cache_panel: false,
27
27
  external_agents: import.meta.env.DEV,
28
- server_side_pdf_export: false,
28
+ server_side_pdf_export: true,
29
29
  };
30
30
 
31
31
  export function getFeatureFlag<T extends keyof ExperimentalFeatures>(
@@ -10,6 +10,7 @@ import { CellOutputId } from "@/core/cells/ids";
10
10
  import type { CellRuntimeState } from "@/core/cells/types";
11
11
  import { ProgressState } from "@/utils/progress";
12
12
  import {
13
+ captureTracker,
13
14
  updateCellOutputsWithScreenshots,
14
15
  useEnrichCellOutputs,
15
16
  } from "../hooks";
@@ -52,6 +53,7 @@ describe("useEnrichCellOutputs", () => {
52
53
  beforeEach(() => {
53
54
  vi.clearAllMocks();
54
55
  store = createStore();
56
+ captureTracker.reset();
55
57
  });
56
58
 
57
59
  const wrapper = ({ children }: { children: ReactNode }) =>
@@ -103,7 +105,7 @@ describe("useEnrichCellOutputs", () => {
103
105
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
104
106
 
105
107
  const takeScreenshots = result.current;
106
- const output = await takeScreenshots({ progress, snappy: false });
108
+ const output = await takeScreenshots({ progress });
107
109
 
108
110
  expect(output).toEqual({});
109
111
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -135,7 +137,7 @@ describe("useEnrichCellOutputs", () => {
135
137
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
136
138
 
137
139
  const takeScreenshots = result.current;
138
- const output = await takeScreenshots({ progress, snappy: false });
140
+ const output = await takeScreenshots({ progress });
139
141
 
140
142
  expect(document.getElementById).toHaveBeenCalledWith(
141
143
  CellOutputId.create(cellId),
@@ -152,50 +154,6 @@ describe("useEnrichCellOutputs", () => {
152
154
  });
153
155
  });
154
156
 
155
- it("should pass snappy=true to toPng with includeStyleProperties", async () => {
156
- const cellId = "cell-1" as CellId;
157
- const mockElement = document.createElement("div");
158
- const mockDataUrl = "data:image/png;base64,mockImageData";
159
-
160
- // Mock document.getElementById
161
- vi.spyOn(document, "getElementById").mockReturnValue(mockElement);
162
- vi.mocked(toPng).mockResolvedValue(mockDataUrl);
163
-
164
- setCellsRuntime(
165
- createMockCellRuntimes({
166
- [cellId]: {
167
- output: {
168
- channel: "output",
169
- mimetype: "text/html",
170
- data: "<div>Chart</div>",
171
- timestamp: 0,
172
- },
173
- },
174
- }),
175
- );
176
-
177
- const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
178
-
179
- const takeScreenshots = result.current;
180
- const output = await takeScreenshots({ progress, snappy: true });
181
-
182
- expect(document.getElementById).toHaveBeenCalledWith(
183
- CellOutputId.create(cellId),
184
- );
185
- // When snappy=true, includeStyleProperties should be set
186
- expect(toPng).toHaveBeenCalledWith(
187
- mockElement,
188
- expect.objectContaining({
189
- filter: expect.any(Function),
190
- onImageErrorHandler: expect.any(Function),
191
- includeStyleProperties: expect.any(Array),
192
- }),
193
- );
194
- expect(output).toEqual({
195
- [cellId]: ["image/png", mockDataUrl],
196
- });
197
- });
198
-
199
157
  it("should skip cells where output has not changed", async () => {
200
158
  const cellId = "cell-1" as CellId;
201
159
  const mockElement = document.createElement("div");
@@ -224,7 +182,7 @@ describe("useEnrichCellOutputs", () => {
224
182
 
225
183
  // First call - should capture
226
184
  let takeScreenshots = result.current;
227
- let output = await takeScreenshots({ progress, snappy: false });
185
+ let output = await takeScreenshots({ progress });
228
186
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
229
187
  expect(toPng).toHaveBeenCalledTimes(1);
230
188
 
@@ -233,7 +191,7 @@ describe("useEnrichCellOutputs", () => {
233
191
 
234
192
  // Second call with same output - should not capture again
235
193
  takeScreenshots = result.current;
236
- output = await takeScreenshots({ progress, snappy: false });
194
+ output = await takeScreenshots({ progress });
237
195
  expect(output).toEqual({}); // Empty because output hasn't changed
238
196
  expect(toPng).toHaveBeenCalledTimes(1); // Still only 1 call
239
197
  });
@@ -262,7 +220,7 @@ describe("useEnrichCellOutputs", () => {
262
220
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
263
221
 
264
222
  const takeScreenshots = result.current;
265
- const output = await takeScreenshots({ progress, snappy: false });
223
+ const output = await takeScreenshots({ progress });
266
224
 
267
225
  expect(output).toEqual({}); // Failed screenshot should be filtered out
268
226
  expect(Logger.error).toHaveBeenCalledWith(
@@ -271,6 +229,50 @@ describe("useEnrichCellOutputs", () => {
271
229
  );
272
230
  });
273
231
 
232
+ it("should retry failed screenshots on next call", async () => {
233
+ const cellId = "cell-1" as CellId;
234
+ const mockElement = document.createElement("div");
235
+ const error = new Error("Screenshot failed");
236
+ const mockDataUrl = "data:image/png;base64,retrySuccess";
237
+
238
+ vi.spyOn(document, "getElementById").mockReturnValue(mockElement);
239
+ // First call fails, second call succeeds
240
+ vi.mocked(toPng)
241
+ .mockRejectedValueOnce(error)
242
+ .mockResolvedValueOnce(mockDataUrl);
243
+
244
+ setCellsRuntime(
245
+ createMockCellRuntimes({
246
+ [cellId]: {
247
+ output: {
248
+ channel: "output",
249
+ mimetype: "text/html",
250
+ data: "<div>Chart</div>",
251
+ timestamp: 0,
252
+ },
253
+ },
254
+ }),
255
+ );
256
+
257
+ const { result, rerender } = renderHook(() => useEnrichCellOutputs(), {
258
+ wrapper,
259
+ });
260
+
261
+ // First call - screenshot fails
262
+ let takeScreenshots = result.current;
263
+ let output = await takeScreenshots({ progress });
264
+ expect(output).toEqual({});
265
+ expect(Logger.error).toHaveBeenCalled();
266
+
267
+ rerender();
268
+
269
+ // Second call - should retry since the first one failed
270
+ takeScreenshots = result.current;
271
+ output = await takeScreenshots({ progress });
272
+ expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
273
+ expect(toPng).toHaveBeenCalledTimes(2);
274
+ });
275
+
274
276
  it("should handle missing DOM elements", async () => {
275
277
  const cellId = "cell-1" as CellId;
276
278
 
@@ -292,7 +294,7 @@ describe("useEnrichCellOutputs", () => {
292
294
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
293
295
 
294
296
  const takeScreenshots = result.current;
295
- const output = await takeScreenshots({ progress, snappy: false });
297
+ const output = await takeScreenshots({ progress });
296
298
 
297
299
  expect(output).toEqual({});
298
300
  expect(Logger.error).toHaveBeenCalledWith(
@@ -341,7 +343,7 @@ describe("useEnrichCellOutputs", () => {
341
343
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
342
344
 
343
345
  const takeScreenshots = result.current;
344
- const output = await takeScreenshots({ progress, snappy: false });
346
+ const output = await takeScreenshots({ progress });
345
347
 
346
348
  expect(output).toEqual({
347
349
  [cell1]: ["image/png", mockDataUrl1],
@@ -387,7 +389,7 @@ describe("useEnrichCellOutputs", () => {
387
389
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
388
390
 
389
391
  const takeScreenshots = result.current;
390
- const output = await takeScreenshots({ progress, snappy: false });
392
+ const output = await takeScreenshots({ progress });
391
393
 
392
394
  // Only the successful screenshot should be in the result
393
395
  expect(output).toEqual({
@@ -429,13 +431,13 @@ describe("useEnrichCellOutputs", () => {
429
431
 
430
432
  // First screenshot
431
433
  let takeScreenshots = result.current;
432
- let output = await takeScreenshots({ progress, snappy: false });
434
+ let output = await takeScreenshots({ progress });
433
435
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl1] });
434
436
 
435
437
  // Second call - same output, should not be captured
436
438
  rerender();
437
439
  takeScreenshots = result.current;
438
- output = await takeScreenshots({ progress, snappy: false });
440
+ output = await takeScreenshots({ progress });
439
441
  expect(output).toEqual({});
440
442
 
441
443
  // Third call - output changed, should be captured
@@ -454,7 +456,7 @@ describe("useEnrichCellOutputs", () => {
454
456
 
455
457
  rerender();
456
458
  takeScreenshots = result.current;
457
- output = await takeScreenshots({ progress, snappy: false });
459
+ output = await takeScreenshots({ progress });
458
460
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl2] });
459
461
  expect(toPng).toHaveBeenCalledTimes(2);
460
462
  });
@@ -494,7 +496,7 @@ describe("useEnrichCellOutputs", () => {
494
496
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
495
497
 
496
498
  const takeScreenshots = result.current;
497
- const output = await takeScreenshots({ progress, snappy: false });
499
+ const output = await takeScreenshots({ progress });
498
500
 
499
501
  // None of these should trigger screenshots
500
502
  expect(output).toEqual({});
@@ -519,7 +521,7 @@ describe("useEnrichCellOutputs", () => {
519
521
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
520
522
 
521
523
  const takeScreenshots = result.current;
522
- const output = await takeScreenshots({ progress, snappy: false });
524
+ const output = await takeScreenshots({ progress });
523
525
 
524
526
  expect(output).toEqual({});
525
527
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -551,7 +553,7 @@ describe("useEnrichCellOutputs", () => {
551
553
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
552
554
 
553
555
  const takeScreenshots = result.current;
554
- const output = await takeScreenshots({ progress, snappy: false });
556
+ const output = await takeScreenshots({ progress });
555
557
 
556
558
  // Verify the exact return type structure
557
559
  expect(output).toHaveProperty(cellId);