@marimo-team/islands 0.23.9-dev4 → 0.23.9-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.
Files changed (96) hide show
  1. package/dist/{ConnectedDataExplorerComponent-OzrfMM5L.js → ConnectedDataExplorerComponent-CyV83R2m.js} +4 -4
  2. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +1 -0
  3. package/dist/assets/{worker-CpBbwbQo.js → worker-ip3AI_sN.js} +2 -2
  4. package/dist/{chat-ui-BDI3FMI8.js → chat-ui-ChD4VvCo.js} +3060 -3033
  5. package/dist/{code-visibility-VZebNmSs.js → code-visibility-CajOShec.js} +1634 -1314
  6. package/dist/{formats-DQ5qjo_Q.js → formats-DHxc-FdY.js} +1 -1
  7. package/dist/{glide-data-editor-DqRY9naW.js → glide-data-editor-BOmK9ETQ.js} +2 -2
  8. package/dist/{html-to-image-CiSinpSR.js → html-to-image-BHv7CEU_.js} +2145 -2153
  9. package/dist/{input-CZD2z6X2.js → input-_2sjvfne.js} +1 -1
  10. package/dist/main.js +1522 -1556
  11. package/dist/{mermaid-IU93XzmY.js → mermaid-lXOw5Py9.js} +2 -2
  12. package/dist/{process-output-5qJjMRKh.js → process-output-BvySRgli.js} +33 -25
  13. package/dist/{reveal-component-DZtPMEoM.js → reveal-component-B-fRdTqs.js} +17 -17
  14. package/dist/{spec-a6DaqW__.js → spec-B96zNUEA.js} +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/{toDate-ZVVIBmdk.js → toDate-x-WRDCH7.js} +1 -1
  17. package/dist/{useAsyncData-C008zUPi.js → useAsyncData-iRgKDT5s.js} +1 -1
  18. package/dist/{useDeepCompareMemoize-BrA3_n61.js → useDeepCompareMemoize-CkQ57VS2.js} +1 -1
  19. package/dist/{useLifecycle-BNaoJ5a4.js → useLifecycle-BBO9PIph.js} +1 -1
  20. package/dist/{useTheme-7O0YWlE5.js → useTheme-DHIrRQOe.js} +34 -21
  21. package/dist/{vega-component-DJNmOdUj.js → vega-component-Dq-SH463.js} +5 -5
  22. package/package.json +1 -1
  23. package/src/components/ai/__tests__/ai-utils.test.ts +43 -38
  24. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  25. package/src/components/app-config/ai-config.tsx +147 -16
  26. package/src/components/app-config/user-config-form.tsx +37 -1
  27. package/src/components/chat/__tests__/chat-utils.test.ts +269 -0
  28. package/src/components/chat/chat-panel.tsx +38 -5
  29. package/src/components/chat/chat-utils.ts +14 -58
  30. package/src/components/data-table/TableBottomBar.tsx +27 -6
  31. package/src/components/data-table/TableTopBar.tsx +7 -1
  32. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +73 -0
  33. package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
  34. package/src/components/data-table/__tests__/data-table.test.tsx +52 -1
  35. package/src/components/data-table/__tests__/header-items.test.tsx +257 -1
  36. package/src/components/data-table/__tests__/useColumnVisibility.test.ts +42 -0
  37. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +98 -26
  38. package/src/components/data-table/column-header.tsx +19 -12
  39. package/src/components/data-table/columns.tsx +3 -4
  40. package/src/components/data-table/data-table.tsx +37 -0
  41. package/src/components/data-table/export-actions.tsx +36 -18
  42. package/src/components/data-table/header-items.tsx +58 -17
  43. package/src/components/data-table/hooks/use-column-visibility.ts +56 -0
  44. package/src/components/data-table/pagination.tsx +16 -3
  45. package/src/components/data-table/schemas.ts +2 -2
  46. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
  47. package/src/components/databases/display.tsx +2 -0
  48. package/src/components/datasources/__tests__/utils.test.ts +82 -0
  49. package/src/components/datasources/utils.ts +16 -15
  50. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
  51. package/src/components/editor/actions/useCellActionButton.tsx +3 -3
  52. package/src/components/editor/actions/useNotebookActions.tsx +5 -2
  53. package/src/components/editor/cell/code/cell-editor.tsx +7 -4
  54. package/src/components/editor/chrome/types.ts +13 -6
  55. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  56. package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
  57. package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
  58. package/src/components/editor/errors/auto-fix.tsx +3 -3
  59. package/src/components/editor/errors/mangled-local-chip.tsx +50 -0
  60. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
  61. package/src/components/editor/navigation/navigation.ts +5 -0
  62. package/src/components/editor/output/MarimoErrorOutput.tsx +110 -27
  63. package/src/components/editor/output/MarimoTracebackOutput.tsx +55 -37
  64. package/src/components/editor/renderers/cell-array.tsx +27 -24
  65. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  66. package/src/components/slides/reveal-component.tsx +3 -3
  67. package/src/components/slides/slide-form.tsx +11 -3
  68. package/src/components/static-html/static-banner.tsx +28 -22
  69. package/src/core/ai/__tests__/model-registry.test.ts +72 -60
  70. package/src/core/ai/model-registry.ts +33 -28
  71. package/src/core/cells/__tests__/actions.test.ts +48 -0
  72. package/src/core/cells/actions.ts +5 -6
  73. package/src/core/codemirror/__tests__/setup.test.ts +29 -0
  74. package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
  75. package/src/core/codemirror/cm.ts +3 -2
  76. package/src/core/codemirror/format.ts +1 -0
  77. package/src/core/codemirror/keymaps/vim.ts +63 -0
  78. package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
  79. package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
  80. package/src/core/config/__tests__/config-schema.test.ts +4 -0
  81. package/src/core/config/config-schema.ts +4 -0
  82. package/src/core/config/config.ts +16 -0
  83. package/src/css/app/Cell.css +0 -1
  84. package/src/plugins/impl/DataTablePlugin.tsx +94 -33
  85. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +1 -0
  86. package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
  87. package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
  88. package/src/plugins/impl/chat/chat-ui.tsx +106 -59
  89. package/src/plugins/impl/chat/types.ts +5 -0
  90. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +8 -6
  91. package/src/stories/dataframe.stories.tsx +1 -0
  92. package/src/utils/__tests__/json-parser.test.ts +1 -69
  93. package/src/utils/__tests__/local-variables.test.ts +132 -0
  94. package/src/utils/json/json-parser.ts +0 -30
  95. package/src/utils/local-variables.ts +67 -0
  96. package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
@@ -0,0 +1,128 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ type ColumnDef,
5
+ getCoreRowModel,
6
+ useReactTable,
7
+ } from "@tanstack/react-table";
8
+ import { fireEvent, render, screen } from "@testing-library/react";
9
+ import { beforeAll, describe, expect, it, vi } from "vitest";
10
+ import { TooltipProvider } from "@/components/ui/tooltip";
11
+ import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer";
12
+ import type { FieldTypesWithExternalType } from "../types";
13
+
14
+ beforeAll(() => {
15
+ global.HTMLElement.prototype.scrollIntoView = () => {};
16
+ if (!global.HTMLElement.prototype.hasPointerCapture) {
17
+ global.HTMLElement.prototype.hasPointerCapture = () => false;
18
+ }
19
+ });
20
+
21
+ const FIELD_TYPES: FieldTypesWithExternalType = [
22
+ ["customer_name", ["string", "str"]],
23
+ ["cust_age", ["integer", "int"]],
24
+ ["order_total", ["number", "float"]],
25
+ ];
26
+
27
+ type Row = Record<string, unknown>;
28
+
29
+ const TEST_COLUMNS: ColumnDef<Row>[] = [
30
+ { id: "customer_name", accessorKey: "customer_name" },
31
+ { id: "cust_age", accessorKey: "cust_age" },
32
+ { id: "order_total", accessorKey: "order_total" },
33
+ ];
34
+
35
+ interface HarnessProps {
36
+ totalColumns?: number;
37
+ initiallyHidden?: string[];
38
+ }
39
+
40
+ function PanelHarness({
41
+ totalColumns = 3,
42
+ initiallyHidden = [],
43
+ }: HarnessProps) {
44
+ const table = useReactTable<Row>({
45
+ data: [],
46
+ columns: TEST_COLUMNS,
47
+ getCoreRowModel: getCoreRowModel(),
48
+ locale: "en-US",
49
+ state: {
50
+ columnVisibility: Object.fromEntries(
51
+ initiallyHidden.map((id) => [id, false]),
52
+ ),
53
+ },
54
+ });
55
+ return (
56
+ <ColumnExplorerPanel
57
+ previewColumn={vi.fn().mockResolvedValue({})}
58
+ fieldTypes={FIELD_TYPES}
59
+ totalRows={3}
60
+ totalColumns={totalColumns}
61
+ tableId="t1"
62
+ table={table}
63
+ />
64
+ );
65
+ }
66
+
67
+ function renderPanel(props?: HarnessProps) {
68
+ return render(
69
+ <TooltipProvider>
70
+ <PanelHarness {...(props ?? {})} />
71
+ </TooltipProvider>,
72
+ );
73
+ }
74
+
75
+ function getSearchInput() {
76
+ return screen.getByPlaceholderText("Search columns...");
77
+ }
78
+
79
+ describe("ColumnExplorerPanel search", () => {
80
+ it("shows all columns when search is empty", () => {
81
+ renderPanel();
82
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
83
+ expect(screen.getByText("cust_age")).toBeInTheDocument();
84
+ expect(screen.getByText("order_total")).toBeInTheDocument();
85
+ });
86
+
87
+ it("matches a word prefix against any column word", () => {
88
+ renderPanel();
89
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
90
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
91
+ expect(screen.getByText("cust_age")).toBeInTheDocument();
92
+ expect(screen.queryByText("order_total")).not.toBeInTheDocument();
93
+ });
94
+
95
+ it("matches multi-word queries across column words in any order", () => {
96
+ renderPanel();
97
+ fireEvent.change(getSearchInput(), { target: { value: "name cust" } });
98
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
99
+ expect(screen.queryByText("cust_age")).not.toBeInTheDocument();
100
+ expect(screen.queryByText("order_total")).not.toBeInTheDocument();
101
+ });
102
+
103
+ it("filters out columns that don't match any needle word", () => {
104
+ renderPanel();
105
+ fireEvent.change(getSearchInput(), { target: { value: "xyz" } });
106
+ expect(screen.queryByText("customer_name")).not.toBeInTheDocument();
107
+ expect(screen.queryByText("cust_age")).not.toBeInTheDocument();
108
+ expect(screen.queryByText("order_total")).not.toBeInTheDocument();
109
+ });
110
+ });
111
+
112
+ describe("ColumnExplorerPanel header counts", () => {
113
+ it("uses rendered-subset total when a clipped column is hidden", () => {
114
+ // Dataset has 100 columns server-side; only 3 are rendered into the
115
+ // TanStack table (the clipped subset). Hiding one of the rendered columns
116
+ // must report "2 visible (1 hidden)", not "99 visible (1 hidden)".
117
+ renderPanel({ totalColumns: 100, initiallyHidden: ["cust_age"] });
118
+ expect(screen.getByText(/2 visible/)).toBeInTheDocument();
119
+ expect(screen.getByText(/\(1 hidden\)/)).toBeInTheDocument();
120
+ expect(screen.queryByText(/99 visible/)).not.toBeInTheDocument();
121
+ });
122
+
123
+ it("uses dataset-wide total when no column is hidden", () => {
124
+ renderPanel({ totalColumns: 100 });
125
+ expect(screen.getByText(/100 columns/)).toBeInTheDocument();
126
+ expect(screen.queryByText(/hidden/)).not.toBeInTheDocument();
127
+ });
128
+ });
@@ -5,7 +5,7 @@ import type {
5
5
  RowSelectionState,
6
6
  SortingState,
7
7
  } from "@tanstack/react-table";
8
- import { render, screen, within } from "@testing-library/react";
8
+ import { fireEvent, render, screen, within } from "@testing-library/react";
9
9
  import { describe, expect, it, vi } from "vitest";
10
10
  import { TooltipProvider } from "@/components/ui/tooltip";
11
11
  import { DataTable } from "../data-table";
@@ -251,3 +251,54 @@ describe("DataTable", () => {
251
251
  expect(within(updatedRows[3]).getByText("pending")).toBeTruthy();
252
252
  });
253
253
  });
254
+
255
+ describe("DataTable — all-hidden banner", () => {
256
+ interface Row {
257
+ a: number;
258
+ b: number;
259
+ }
260
+
261
+ const columns: ColumnDef<Row>[] = [
262
+ { accessorKey: "a", header: "A" },
263
+ { accessorKey: "b", header: "B" },
264
+ ];
265
+ const data: Row[] = [{ a: 1, b: 2 }];
266
+
267
+ const renderWithVisibility = (hiddenColumns: string[]) =>
268
+ render(
269
+ <TooltipProvider>
270
+ <DataTable
271
+ data={data}
272
+ columns={columns}
273
+ selection={null}
274
+ totalRows={1}
275
+ totalColumns={2}
276
+ pagination={false}
277
+ hiddenColumns={hiddenColumns}
278
+ />
279
+ </TooltipProvider>,
280
+ );
281
+
282
+ it("renders banner when every user column is hidden", () => {
283
+ renderWithVisibility(["a", "b"]);
284
+ expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
285
+ expect(screen.getByText(/Unhide all/i)).toBeInTheDocument();
286
+ });
287
+
288
+ it("does not render the banner when at least one column is visible", () => {
289
+ renderWithVisibility(["a"]);
290
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
291
+ });
292
+
293
+ it("does not render the banner when no columns are hidden", () => {
294
+ renderWithVisibility([]);
295
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
296
+ });
297
+
298
+ it("'Unhide all' restores columns hidden via the Python kwarg", () => {
299
+ renderWithVisibility(["a", "b"]);
300
+ expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
301
+ fireEvent.click(screen.getByText(/Unhide all/i));
302
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
303
+ });
304
+ });
@@ -1,7 +1,35 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { SortingState } from "@tanstack/react-table";
3
+ import type {
4
+ Column,
5
+ SortDirection,
6
+ SortingState,
7
+ Table,
8
+ } from "@tanstack/react-table";
9
+ import { fireEvent, render, screen } from "@testing-library/react";
4
10
  import { describe, expect, it, vi } from "vitest";
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuTrigger,
15
+ } from "@/components/ui/dropdown-menu";
16
+ import {
17
+ ColumnPinning,
18
+ ColumnWrapping,
19
+ CopyColumn,
20
+ DataType,
21
+ FormatOptions,
22
+ HideColumn,
23
+ Sorts,
24
+ } from "../header-items";
25
+
26
+ const renderInMenu = (node: React.ReactNode) =>
27
+ render(
28
+ <DropdownMenu open={true}>
29
+ <DropdownMenuTrigger />
30
+ <DropdownMenuContent>{node}</DropdownMenuContent>
31
+ </DropdownMenu>,
32
+ );
5
33
 
6
34
  describe("multi-column sorting logic", () => {
7
35
  // Extract the core sorting logic to test in isolation
@@ -146,3 +174,231 @@ describe("multi-column sorting logic", () => {
146
174
  // After removal, dept should move from priority 3 to priority 2
147
175
  });
148
176
  });
177
+
178
+ describe("HideColumn", () => {
179
+ const makeColumn = ({
180
+ canHide = true,
181
+ toggleVisibility = vi.fn(),
182
+ }: {
183
+ canHide?: boolean;
184
+ toggleVisibility?: (value?: boolean) => void;
185
+ } = {}) =>
186
+ ({
187
+ getCanHide: () => canHide,
188
+ toggleVisibility,
189
+ }) as unknown as Column<unknown, unknown>;
190
+
191
+ it("renders 'Hide column' when canHide is true", () => {
192
+ renderInMenu(<HideColumn column={makeColumn()} />);
193
+ expect(screen.getByText("Hide column")).toBeInTheDocument();
194
+ });
195
+
196
+ it("returns null when getCanHide is false", () => {
197
+ renderInMenu(<HideColumn column={makeColumn({ canHide: false })} />);
198
+ expect(screen.queryByText("Hide column")).toBeNull();
199
+ });
200
+
201
+ it("calls toggleVisibility(false) on click", () => {
202
+ const toggleVisibility = vi.fn();
203
+ renderInMenu(<HideColumn column={makeColumn({ toggleVisibility })} />);
204
+ fireEvent.click(screen.getByText("Hide column"));
205
+ expect(toggleVisibility).toHaveBeenCalledWith(false);
206
+ });
207
+ });
208
+
209
+ describe("DataType", () => {
210
+ const makeColumn = (dtype?: string) =>
211
+ ({
212
+ columnDef: { meta: dtype === undefined ? {} : { dtype } },
213
+ }) as unknown as Column<unknown, unknown>;
214
+
215
+ it("renders the dtype label when present", () => {
216
+ renderInMenu(<DataType column={makeColumn("int64")} />);
217
+ expect(screen.getByText("int64")).toBeInTheDocument();
218
+ });
219
+
220
+ it("returns null when dtype is absent", () => {
221
+ renderInMenu(<DataType column={makeColumn()} />);
222
+ expect(screen.queryByText("int64")).toBeNull();
223
+ });
224
+ });
225
+
226
+ describe("Sorts", () => {
227
+ const makeColumn = ({
228
+ canSort = true,
229
+ sorted = false,
230
+ sortIndex = 0,
231
+ }: {
232
+ canSort?: boolean;
233
+ sorted?: false | SortDirection;
234
+ sortIndex?: number;
235
+ } = {}) =>
236
+ ({
237
+ getCanSort: () => canSort,
238
+ getIsSorted: () => sorted,
239
+ getSortIndex: () => sortIndex,
240
+ clearSorting: vi.fn(),
241
+ toggleSorting: vi.fn(),
242
+ }) as unknown as Column<unknown, unknown>;
243
+
244
+ const makeTable = (sorting: SortingState) =>
245
+ ({
246
+ getState: () => ({ sorting }),
247
+ resetSorting: vi.fn(),
248
+ }) as unknown as Table<unknown>;
249
+
250
+ it("returns null when the column cannot sort", () => {
251
+ renderInMenu(<Sorts column={makeColumn({ canSort: false })} />);
252
+ expect(screen.queryByText("Asc")).toBeNull();
253
+ });
254
+
255
+ it("renders Asc and Desc items", () => {
256
+ renderInMenu(<Sorts column={makeColumn()} />);
257
+ expect(screen.getByText("Asc")).toBeInTheDocument();
258
+ expect(screen.getByText("Desc")).toBeInTheDocument();
259
+ });
260
+
261
+ it("offers single-column 'Clear sort' when sorted without multi-sort", () => {
262
+ renderInMenu(<Sorts column={makeColumn({ sorted: "asc" })} />);
263
+ expect(screen.getByText("Clear sort")).toBeInTheDocument();
264
+ });
265
+
266
+ it("offers 'Clear all sorts' when the table has multiple sorts", () => {
267
+ renderInMenu(
268
+ <Sorts
269
+ column={makeColumn({ sorted: "asc" })}
270
+ table={makeTable([
271
+ { id: "a", desc: false },
272
+ { id: "b", desc: true },
273
+ ])}
274
+ />,
275
+ );
276
+ expect(screen.getByText("Clear all sorts")).toBeInTheDocument();
277
+ });
278
+ });
279
+
280
+ describe("CopyColumn", () => {
281
+ const makeColumn = ({
282
+ canCopy = true,
283
+ id = "name",
284
+ }: {
285
+ canCopy?: boolean;
286
+ id?: string;
287
+ } = {}) =>
288
+ ({
289
+ id,
290
+ getCanCopy: () => canCopy,
291
+ }) as unknown as Column<unknown, unknown>;
292
+
293
+ it("renders 'Copy column name' when copyable", () => {
294
+ renderInMenu(<CopyColumn column={makeColumn()} />);
295
+ expect(screen.getByText("Copy column name")).toBeInTheDocument();
296
+ });
297
+
298
+ it("returns null when the column cannot be copied", () => {
299
+ renderInMenu(<CopyColumn column={makeColumn({ canCopy: false })} />);
300
+ expect(screen.queryByText("Copy column name")).toBeNull();
301
+ });
302
+ });
303
+
304
+ describe("ColumnPinning", () => {
305
+ const makeColumn = ({
306
+ canPin = true,
307
+ pinned = false,
308
+ }: {
309
+ canPin?: boolean;
310
+ pinned?: false | "left" | "right";
311
+ } = {}) =>
312
+ ({
313
+ getCanPin: () => canPin,
314
+ getIsPinned: () => pinned,
315
+ pin: vi.fn(),
316
+ }) as unknown as Column<unknown, unknown>;
317
+
318
+ it("returns null when the column cannot be pinned", () => {
319
+ renderInMenu(<ColumnPinning column={makeColumn({ canPin: false })} />);
320
+ expect(screen.queryByText("Freeze left")).toBeNull();
321
+ });
322
+
323
+ it("offers freeze options when unpinned", () => {
324
+ renderInMenu(<ColumnPinning column={makeColumn()} />);
325
+ expect(screen.getByText("Freeze left")).toBeInTheDocument();
326
+ expect(screen.getByText("Freeze right")).toBeInTheDocument();
327
+ });
328
+
329
+ it("offers 'Unfreeze' when pinned", () => {
330
+ renderInMenu(<ColumnPinning column={makeColumn({ pinned: "left" })} />);
331
+ expect(screen.getByText("Unfreeze")).toBeInTheDocument();
332
+ });
333
+ });
334
+
335
+ describe("ColumnWrapping", () => {
336
+ const makeColumn = ({
337
+ canWrap = true,
338
+ wrapping = "nowrap",
339
+ }: {
340
+ canWrap?: boolean;
341
+ wrapping?: "wrap" | "nowrap";
342
+ } = {}) =>
343
+ ({
344
+ getCanWrap: () => canWrap,
345
+ getColumnWrapping: () => wrapping,
346
+ toggleColumnWrapping: vi.fn(),
347
+ }) as unknown as Column<unknown, unknown>;
348
+
349
+ it("returns null when the column cannot wrap", () => {
350
+ renderInMenu(<ColumnWrapping column={makeColumn({ canWrap: false })} />);
351
+ expect(screen.queryByText("Wrap text")).toBeNull();
352
+ });
353
+
354
+ it("offers 'Wrap text' when not wrapping", () => {
355
+ renderInMenu(<ColumnWrapping column={makeColumn()} />);
356
+ expect(screen.getByText("Wrap text")).toBeInTheDocument();
357
+ });
358
+
359
+ it("offers 'No wrap text' when wrapping", () => {
360
+ renderInMenu(<ColumnWrapping column={makeColumn({ wrapping: "wrap" })} />);
361
+ expect(screen.getByText("No wrap text")).toBeInTheDocument();
362
+ });
363
+ });
364
+
365
+ describe("FormatOptions", () => {
366
+ const makeColumn = ({
367
+ dataType = "number",
368
+ canFormat = true,
369
+ }: {
370
+ dataType?: string;
371
+ canFormat?: boolean;
372
+ } = {}) =>
373
+ ({
374
+ columnDef: { meta: { dataType } },
375
+ getCanFormat: () => canFormat,
376
+ getColumnFormatting: () => undefined,
377
+ setColumnFormatting: vi.fn(),
378
+ }) as unknown as Column<unknown, unknown>;
379
+
380
+ it("renders the 'Format' submenu trigger for formattable columns", () => {
381
+ renderInMenu(<FormatOptions column={makeColumn()} locale="en-US" />);
382
+ expect(screen.getByText("Format")).toBeInTheDocument();
383
+ });
384
+
385
+ it("returns null when the column cannot be formatted", () => {
386
+ renderInMenu(
387
+ <FormatOptions
388
+ column={makeColumn({ canFormat: false })}
389
+ locale="en-US"
390
+ />,
391
+ );
392
+ expect(screen.queryByText("Format")).toBeNull();
393
+ });
394
+
395
+ it("returns null when the data type has no format options", () => {
396
+ renderInMenu(
397
+ <FormatOptions
398
+ column={makeColumn({ dataType: "unknown" })}
399
+ locale="en-US"
400
+ />,
401
+ );
402
+ expect(screen.queryByText("Format")).toBeNull();
403
+ });
404
+ });
@@ -0,0 +1,42 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { useColumnVisibility } from "../hooks/use-column-visibility";
6
+
7
+ describe("useColumnVisibility", () => {
8
+ it("should initialize with correct default values", () => {
9
+ const { result } = renderHook(() => useColumnVisibility());
10
+ expect(result.current.columnVisibility).toEqual({});
11
+ });
12
+
13
+ it("should seed hidden columns as { name: false }", () => {
14
+ const { result } = renderHook(() => useColumnVisibility(["a", "b"]));
15
+ expect(result.current.columnVisibility).toEqual({ a: false, b: false });
16
+ });
17
+
18
+ it("should treat empty hidden list as a no-op", () => {
19
+ const { result } = renderHook(() => useColumnVisibility([]));
20
+ expect(result.current.columnVisibility).toEqual({});
21
+ });
22
+
23
+ it("should update visibility state via setter", () => {
24
+ const { result } = renderHook(() => useColumnVisibility(["a"]));
25
+
26
+ act(() => {
27
+ result.current.setColumnVisibility({ a: true, b: false });
28
+ });
29
+
30
+ expect(result.current.columnVisibility).toEqual({ a: true, b: false });
31
+ });
32
+
33
+ it("should handle functional updates", () => {
34
+ const { result } = renderHook(() => useColumnVisibility(["a"]));
35
+
36
+ act(() => {
37
+ result.current.setColumnVisibility((prev) => ({ ...prev, c: false }));
38
+ });
39
+
40
+ expect(result.current.columnVisibility).toEqual({ a: false, c: false });
41
+ });
42
+ });