/* Copyright 2026 Marimo. All rights reserved. */ import type { Column } from "@tanstack/react-table"; import { fireEvent, render, waitFor } from "@testing-library/react"; import { I18nProvider } from "react-aria"; import { beforeAll, describe, expect, it, test, vi } from "vitest"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Logger } from "@/utils/Logger"; import { parseContent } from "@/utils/url-parser"; import { generateColumns, inferFieldTypes, LocaleNumber, renderCellValue, } from "../columns"; import { getMimeValues, isMimeValue, MimeCell } from "../mime-cell"; import type { FieldTypesWithExternalType } from "../types"; import { uniformSample } from "../uniformSample"; import { UrlDetector } from "../url-detector"; test("uniformSample", () => { const items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; expect(uniformSample(items, 2)).toMatchInlineSnapshot(` [ "A", "J", ] `); expect(uniformSample(items, 4)).toMatchInlineSnapshot(` [ "A", "C", "F", "J", ] `); expect(uniformSample(items, 100)).toBe(items); }); test("UrlDetector renders URLs as hyperlinks", () => { const text = "Check this link: https://example.com"; const { container } = render(); const link = container.querySelector("a"); expect(link).toBeTruthy(); expect(link?.href).toBe("https://example.com/"); }); test("inferFieldTypes", () => { const data = [ { a: 1, b: "foo", c: null, d: { mime: "text/csv" }, e: [1, 2, 3], f: true, g: false, h: new Date(), }, ]; const fieldTypes = inferFieldTypes(data); expect(fieldTypes).toMatchInlineSnapshot(` [ [ "a", [ "number", "number", ], ], [ "b", [ "string", "string", ], ], [ "c", [ "unknown", "object", ], ], [ "d", [ "unknown", "object", ], ], [ "e", [ "unknown", "object", ], ], [ "f", [ "boolean", "boolean", ], ], [ "g", [ "boolean", "boolean", ], ], [ "h", [ "datetime", "datetime", ], ], ] `); }); test("inferFieldTypes with nulls", () => { const data = [{ a: 1, b: null }]; const fieldTypes = inferFieldTypes(data); expect(fieldTypes).toMatchInlineSnapshot(` [ [ "a", [ "number", "number", ], ], [ "b", [ "unknown", "object", ], ], ] `); }); test("inferFieldTypes with mimetypes", () => { const data = [{ a: { mime: "text/csv" }, b: { mime: "image/png" } }]; const fieldTypes = inferFieldTypes(data); expect(fieldTypes).toMatchInlineSnapshot(` [ [ "a", [ "unknown", "object", ], ], [ "b", [ "unknown", "object", ], ], ] `); }); describe("generateColumns", () => { const fieldTypes: FieldTypesWithExternalType = [ ["name", ["string", "text"]], ["age", ["number", "integer"]], ]; it("should generate columns with row headers", () => { const loggerWarn = vi.spyOn(Logger, "warn").mockImplementation(() => {}); // "name" intentionally appears in both inputs; the row header takes precedence. const columns = generateColumns({ rowHeaders: [["name", ["string", "text"]]], selection: null, fieldTypes, }); expect(columns).toHaveLength(2); expect(columns.map((column) => column.id)).toEqual(["name", "age"]); expect(columns[0].id).toBe("name"); expect(columns[0].meta?.rowHeader).toBe(true); expect(columns[0].enableSorting).toBe(true); expect(loggerWarn).toHaveBeenCalledWith( "Skipping field type for row header column name", ); loggerWarn.mockRestore(); }); it("should generate columns with nameless row headers", () => { const columns = generateColumns({ rowHeaders: [["", ["string", "text"]]], selection: null, fieldTypes, }); expect(columns).toHaveLength(3); expect(columns[0].id).toMatchInlineSnapshot(`"__m_column__0"`); expect(columns[0].meta?.rowHeader).toBe(true); expect(columns[0].enableSorting).toBe(false); }); it("should include selection column for multi selection", () => { const columns = generateColumns({ rowHeaders: [], selection: "multi", fieldTypes, }); expect(columns[0].id).toBe("__select__"); expect(columns[0].enableSorting).toBe(false); }); it("should generate columns with correct meta data", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, }); expect(columns.length).toBe(2); expect(columns[0].meta?.dataType).toBe("string"); expect(columns[1].meta?.dataType).toBe("number"); }); it("should auto right-align numeric columns", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, }); // "age" is a number column — should auto right-align // oxlint-disable-next-line typescript/no-explicit-any const cell = (columns[1].cell as any)({ column: { columnDef: columns[1], }, renderValue: () => 25, getValue: () => 25, }); expect(cell?.props.className).toContain("text-right"); // "name" is a string column — should remain left-aligned // oxlint-disable-next-line typescript/no-explicit-any const nameCell = (columns[0].cell as any)({ column: { columnDef: columns[0], }, renderValue: () => "John", getValue: () => "John", }); expect(nameCell?.props.className).not.toContain("text-right"); }); it("should respect explicit textJustifyColumns over auto alignment", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, textJustifyColumns: { age: "left" }, }); // "age" is numeric but explicitly set to left // oxlint-disable-next-line typescript/no-explicit-any const cell = (columns[1].cell as any)({ column: { columnDef: columns[1], }, renderValue: () => 25, getValue: () => 25, }); expect(cell?.props.className).not.toContain("text-right"); }); it("should set minFractionDigits from fractionDigitsByColumn", () => { const numericFieldTypes: FieldTypesWithExternalType = [ ["price", ["number", "float64"]], ["count", ["integer", "int64"]], ]; const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes: numericFieldTypes, fractionDigitsByColumn: { price: 2 }, }); // price has 2 fraction digits expect(columns[0].meta?.minFractionDigits).toBe(2); // count not in fractionDigitsByColumn expect(columns[1].meta?.minFractionDigits).toBeUndefined(); }); it("should handle text justification and wrapping", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, textJustifyColumns: { name: "center" }, wrappedColumns: ["age"], }); // Assuming getCellStyleClass is a function that returns a class name // oxlint-disable-next-line typescript/no-explicit-any const cell = (columns[0].cell as any)({ column: { columnDef: columns[0], }, renderValue: () => "John", getValue: () => "John", }); expect(cell?.props.className).toContain("center"); }); it("should align column headers to match textJustifyColumns", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, textJustifyColumns: { name: "right", age: "center" }, }); const mockColumn = (col: (typeof columns)[number]) => ({ id: col.id, getCanSort: () => true, getCanFilter: () => false, getIsSorted: () => false, getSortIndex: () => -1, getFilterValue: () => undefined, columnDef: { meta: col.meta }, }); // Right-justified column: outer summary wrapper aligns to end, and the // header row uses flex-row-reverse so the title sits at the right edge. const { container: rightContainer } = render( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[0].header as any)({ column: mockColumn(columns[0]) })} , ); expect( rightContainer.querySelector("[data-testid='data-table-sort-button']"), ).toBeTruthy(); expect( rightContainer.querySelector( "[data-testid='data-table-column-menu-button']", ), ).toBeTruthy(); expect(rightContainer.firstElementChild?.className).toContain("items-end"); expect(rightContainer.querySelector(".flex-row-reverse")).toBeTruthy(); // Center-justified column: outer summary wrapper centers; header row // keeps natural order. const { container: centerContainer } = render( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[1].header as any)({ column: mockColumn(columns[1]) })} , ); expect( centerContainer.querySelector("[data-testid='data-table-sort-button']"), ).toBeTruthy(); expect( centerContainer.querySelector( "[data-testid='data-table-column-menu-button']", ), ).toBeTruthy(); expect(centerContainer.firstElementChild?.className).toContain( "items-center", ); expect(centerContainer.querySelector(".flex-row-reverse")).toBeNull(); }); it("should not auto-align numeric column headers without explicit override", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, }); const mockColumn = (col: (typeof columns)[number]) => ({ id: col.id, getCanSort: () => true, getCanFilter: () => false, getIsSorted: () => false, getSortIndex: () => -1, getFilterValue: () => undefined, columnDef: { meta: col.meta }, }); // "age" is numeric: cells auto right-align, but the header stays // left-aligned unless the user explicitly opts in via text_justify_columns. const { container } = render( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[1].header as any)({ column: mockColumn(columns[1]) })} , ); expect(container.firstElementChild?.className).not.toContain("items-end"); expect(container.querySelector(".flex-row-reverse")).toBeNull(); }); it("should cycle sort button through asc, desc, and clear on clicks", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, }); const toggleSorting = vi.fn(); const clearSorting = vi.fn(); let sortDirection: false | "asc" | "desc" = false; const mockColumn = (col: (typeof columns)[number]) => ({ id: col.id, getCanSort: () => true, getCanFilter: () => false, getIsSorted: () => sortDirection, getSortIndex: () => -1, getFilterValue: () => undefined, toggleSorting, clearSorting, columnDef: { meta: col.meta }, }); const mock = mockColumn(columns[0]); const { container, rerender } = render( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[0].header as any)({ column: mock })} , ); const sortButton = container.querySelector( "[data-testid='data-table-sort-button']", ); expect(sortButton).toBeTruthy(); // first click unsorted > asc fireEvent.click(sortButton!); expect(toggleSorting).toHaveBeenCalledWith(false, true); // Simulate asc state and re-render sortDirection = "asc"; rerender( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[0].header as any)({ column: mock })} , ); // second click asc >dsc fireEvent.click(sortButton!); expect(toggleSorting).toHaveBeenCalledWith(true, true); // Simulate desc state and re-render sortDirection = "desc"; rerender( {/* oxlint-disable-next-line typescript/no-explicit-any */} {(columns[0].header as any)({ column: mock })} , ); // third click back to unsorted fireEvent.click(sortButton!); expect(clearSorting).toHaveBeenCalled(); }); it("should not include index column if it exists", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes: [...fieldTypes, ["_marimo_row_id", ["string", "text"]]], }); expect(columns).toHaveLength(2); expect(columns[0].id).toBe("name"); expect(columns[1].id).toBe("age"); }); it("should render header with tooltip when headerTooltip is provided", () => { const columns = generateColumns({ rowHeaders: [], selection: null, fieldTypes, headerTooltip: { name: "Custom Name Tooltip" }, }); // Get the header function for the first column const headerFunction = columns[0].header; expect(headerFunction).toBeTypeOf("function"); const mockColumn = { id: "name", getCanSort: () => false, getCanFilter: () => false, columnDef: { meta: { dtype: "string", dataType: "string", }, }, }; const { container } = render( {/* @ts-expect-error: mock column and header function */} {headerFunction({ column: mockColumn })} , ); expect(container.textContent).toContain("name"); // The tooltip functionality is tested by verifying that the header renders correctly // when headerTooltip is provided. expect(container.firstChild).toBeTruthy(); }); }); describe("MimeCell", () => { it("renders with correct mime data", () => { const value = { mimetype: "text/plain", data: "Hello World" }; const { container } = render(); expect(container.textContent).toContain("Hello World"); }); }); describe("isMimeValue", () => { it("should return true for valid MimeValue objects", () => { const value = { mimetype: "text/plain", data: "test data" }; expect(isMimeValue(value)).toBe(true); }); it("should return false for null", () => { expect(isMimeValue(null)).toBe(false); }); it("should return false for primitive values", () => { expect(isMimeValue("string")).toBe(false); expect(isMimeValue(123)).toBe(false); expect(isMimeValue(true)).toBe(false); }); it("should return false for objects missing required properties", () => { expect(isMimeValue({})).toBe(false); expect(isMimeValue({ mimetype: "text/plain" })).toBe(false); expect(isMimeValue({ data: "test data" })).toBe(false); }); }); describe("getMimeValues", () => { it("should return array with single MimeValue when input is a MimeValue", () => { const value = { mimetype: "text/plain", data: "test data" }; expect(getMimeValues(value)).toEqual([value]); }); it("should return array with MimeValue when input has serialized_mime_bundle", () => { const mimeValue = { mimetype: "text/plain", data: "test data" }; const value = { serialized_mime_bundle: mimeValue }; expect(getMimeValues(value)).toEqual([mimeValue]); }); it("should return array with MimeValue when input has _serialized_mime_bundle", () => { const mimeValue = { mimetype: "text/plain", data: "test data" }; const value = { _serialized_mime_bundle: mimeValue }; expect(getMimeValues(value)).toEqual([mimeValue]); }); it("should return array of MimeValues when input is an array of MimeValues", () => { const values = [ { mimetype: "text/plain", data: "test data 1" }, { mimetype: "text/html", data: "

test data 2

" }, ]; expect(getMimeValues(values)).toEqual(values); }); it("should return undefined for null input", () => { expect(getMimeValues(null)).toBeUndefined(); }); it("should return undefined for primitive values", () => { expect(getMimeValues("string")).toBeUndefined(); expect(getMimeValues(123)).toBeUndefined(); expect(getMimeValues(true)).toBeUndefined(); }); it("should return undefined for objects that don't match any pattern", () => { expect(getMimeValues({})).toBeUndefined(); expect(getMimeValues({ random: "property" })).toBeUndefined(); }); it("should return undefined for invalid serialized_mime_bundle", () => { expect( getMimeValues({ serialized_mime_bundle: "not a mime value" }), ).toBeUndefined(); }); it("should return undefined for invalid _serialized_mime_bundle", () => { expect( getMimeValues({ _serialized_mime_bundle: "not a mime value" }), ).toBeUndefined(); }); it("should return undefined for array with non-MimeValue items", () => { const values = [ { mimetype: "text/plain", data: "test data" }, "not a mime value", ]; expect(getMimeValues(values)).toBeUndefined(); }); }); describe("LocaleNumber", () => { it("should format numbers correctly for en-US locale", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1,234,567.89"`); }); it("should format numbers correctly for de-DE locale", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1.234.567,89"`); }); it("should format integers correctly", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1,000"`); }); it("should format zero correctly", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"0"`); }); it("should format negative numbers correctly", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"-1,234.56"`); }); it("should format small decimal numbers correctly", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"0.123456789"`); }); it("should format large numbers correctly", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"999,999,999.99"`); }); it("should format numbers correctly for fr-FR locale", () => { const { container } = render( , ); // oxlint-disable-next-line no-irregular-whitespace expect(container.textContent).toMatchInlineSnapshot(`"1 234 567,89"`); }); it("should format numbers correctly for ja-JP locale", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1,234,567.89"`); }); it("should respect maximumFractionDigits based on locale", () => { // Test with a number that has many decimal places const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1.1234567890123457"`); }); it("should handle very large numbers", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot( `"123,456,789,012,345.67"`, ); }); it("should handle Infinity", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"∞"`); }); it("should handle negative Infinity", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"-∞"`); }); it("should handle NaN", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"NaN"`); }); it("should handle numbers with scientific notation", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"10,000,000,000"`); }); it("should pad decimals with minFractionDigits", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"42.00"`); }); it("should pad to minFractionDigits for numbers with fewer decimals", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1,234.500"`); }); it("should not truncate decimals beyond minFractionDigits", () => { const { container } = render( , ); expect(container.textContent).toMatchInlineSnapshot(`"1.23456"`); }); }); describe("renderCellValue with string + edge whitespace", () => { const createMockStringColumn = (dtype = "object") => ({ id: "desc", columnDef: { meta: { dataType: "string" as const, dtype, }, }, getColumnFormatting: () => undefined, getColumnWrapping: () => undefined, applyColumnFormatting: (value: unknown) => value, }) as unknown as Column; const renderWithProviders = (node: React.ReactNode) => render( {node} , ); it("closes a long-string popover when its shadow-DOM trigger is clicked again", async () => { const value = "This string is long enough to render in a cell popover."; const result = renderCellValue({ column: createMockStringColumn(), renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); const host = document.createElement("div"); document.body.append(host); try { const shadowRoot = host.attachShadow({ mode: "open" }); const { unmount } = render( {result} , { container: shadowRoot as unknown as HTMLElement }, ); try { const trigger = shadowRoot.querySelector("button[data-state]"); const getCopyButton = () => document.querySelector('[aria-label="Copy to clipboard"]') ?? shadowRoot.querySelector('[aria-label="Copy to clipboard"]'); expect(trigger).not.toBeNull(); fireEvent.pointerDown(trigger!); fireEvent.click(trigger!); await waitFor(() => expect(getCopyButton()).not.toBeNull()); fireEvent.pointerDown(trigger!); fireEvent.click(trigger!); expect(getCopyButton()).toBeNull(); } finally { unmount(); } } finally { host.remove(); } }); it("renders pandas timedelta NaT as a sentinel", () => { const mockColumn = createMockStringColumn("timedelta64[us]"); const result = renderCellValue({ column: mockColumn, renderValue: () => "NaT", getValue: () => "NaT", selectCell: undefined, cellStyles: "", }); const { container } = renderWithProviders(result); expect(container.querySelector('[aria-label="Not a Time"]')).not.toBeNull(); }); it("renders edge whitespace markers and still detects the URL in the middle", () => { const mockColumn = createMockStringColumn(); const value = " https://example.com "; const result = renderCellValue({ column: mockColumn, renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); const { container } = renderWithProviders(result); // URL detection runs on the middle, so the anchor is still rendered. const link = container.querySelector("a"); expect(link).toBeTruthy(); expect(link?.href).toBe("https://example.com/"); // The link text is exactly the URL — no leading/trailing whitespace // leaked into the anchor. expect(link?.textContent).toBe("https://example.com"); // Both edge-whitespace marker containers are present and render // visible glyphs (U+2423 "open box" for regular spaces). const markerSpans = container.querySelectorAll( "span[aria-label$='space'], span[aria-label$='spaces']", ); expect(markerSpans.length).toBeGreaterThanOrEqual(2); expect(container.textContent?.includes("\u2423")).toBe(true); }); it("does not split URLs on whitespace padding (regression)", () => { const mockColumn = createMockStringColumn(); // Trailing whitespace would previously be consumed by the URL regex // (\S+). We render the middle only through parseContent to avoid that. const value = "go here: https://example.com/path "; const result = renderCellValue({ column: mockColumn, renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); const { container } = renderWithProviders(result); const link = container.querySelector("a"); expect(link).toBeTruthy(); // href is URL-normalized by the browser — should not include the // trailing spaces as part of the URL path. expect(link?.href).toBe("https://example.com/path"); expect(link?.textContent?.trimEnd()).toBe("https://example.com/path"); }); it("renders no marker span when the string has no edge whitespace", () => { const mockColumn = createMockStringColumn(); const value = "https://example.com"; const result = renderCellValue({ column: mockColumn, renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); const { container } = renderWithProviders(result); // No marker glyph leaked through. expect(container.textContent?.includes("\u2423")).toBe(false); // And no WhitespaceMarkers wrapper was rendered at all. The component // returns null for empty strings, and always sets an aria-label // generated by `describeWhitespace` when it does render (e.g. // "1 space", "2 spaces", "1 tab", "1 unicode whitespace"). // Matching the "space"/"spaces" suffix is enough here because the // test value contains no whitespace, so no marker of any kind should // appear. const markerSpans = container.querySelectorAll( "span[aria-label$='space'], span[aria-label$='spaces']", ); expect(markerSpans.length).toBe(0); }); }); describe("renderCellValue with JSON document strings", () => { beforeAll(() => { // JsonViewer reads the color scheme through matchMedia, which jsdom // does not implement. Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); }); const createMockStringColumn = () => ({ id: "json_data", columnDef: { meta: { dataType: "string" as const, dtype: "str", }, }, getColumnFormatting: () => undefined, getColumnWrapping: () => undefined, applyColumnFormatting: (value: unknown) => value, }) as unknown as Column; const renderCell = (value: string) => { const result = renderCellValue({ column: createMockStringColumn(), renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); return render( {result} , ); }; it("opens the JSON viewer for a str cell that holds a JSON object", () => { const value = '{"vendor":"ACME","attachments":[{"name":"inv.pdf","url":"https://example.com/p/AAA"}]}'; const { container } = renderCell(value); const trigger = container.querySelector("button"); expect(trigger).toBeTruthy(); fireEvent.click(trigger!); // Popover content mounts in a portal, so query the document. expect(document.querySelector(".marimo-json-output")).toBeTruthy(); expect(document.body.textContent).toContain('"vendor":"ACME"'); }); it("opens the JSON viewer for a str cell that holds a JSON array", () => { const value = '[{"name":"inv.pdf","url":"https://example.com/p/AAA"}]'; const { container } = renderCell(value); const trigger = container.querySelector("button"); expect(trigger).toBeTruthy(); fireEvent.click(trigger!); expect(document.querySelector(".marimo-json-output")).toBeTruthy(); expect(document.body.textContent).toContain('"name":"inv.pdf"'); }); it("opens the JSON viewer when the JSON document has edge whitespace", () => { const { container } = renderCell(' {"a": 1} '); const trigger = container.querySelector("button"); expect(trigger).toBeTruthy(); fireEvent.click(trigger!); expect(document.querySelector(".marimo-json-output")).toBeTruthy(); }); it("keeps a Python repr as inline text", () => { const value = "{'vendor': 'ACME'}"; const { container } = renderCell(value); expect(container.querySelector("button")).toBeNull(); expect(container.textContent).toBe(value); }); it("keeps a truncated JSON document as inline text", () => { const value = '{"vendor":"ACME","attachments":'; const { container } = renderCell(value); expect(container.querySelector("button")).toBeNull(); expect(container.textContent).toBe(value); }); it("keeps JSON scalar strings as inline text", () => { const value = '"hello"'; const { container } = renderCell(value); expect(container.querySelector("button")).toBeNull(); expect(container.textContent).toBe(value); }); it("keeps the text popout for long strings that are not JSON", () => { const value = "This long plain sentence exceeds the fifty character popout threshold."; const { container } = renderCell(value); const trigger = container.querySelector("button"); expect(trigger).toBeTruthy(); fireEvent.click(trigger!); expect(document.querySelector(".marimo-json-output")).toBeNull(); expect(document.body.textContent).toContain(value); }); }); describe("renderCellValue with boolean values", () => { const createMockColumn = () => ({ id: "active", columnDef: { meta: { dataType: "boolean" as const, dtype: "bool", }, }, getColumnFormatting: () => undefined, getColumnWrapping: () => undefined, applyColumnFormatting: (value: unknown) => value, }) as unknown as Column; it("should render true as True", () => { const mockColumn = createMockColumn(); const result = renderCellValue({ column: mockColumn, renderValue: () => true, getValue: () => true, selectCell: undefined, cellStyles: "", }); const { container } = render(result); expect(container.textContent).toBe("True"); }); it("should render false as False", () => { const mockColumn = createMockColumn(); const result = renderCellValue({ column: mockColumn, renderValue: () => false, getValue: () => false, selectCell: undefined, cellStyles: "", }); const { container } = render(result); expect(container.textContent).toBe("False"); }); }); describe("renderCellValue with datetime values", () => { const createMockColumn = () => ({ id: "created", columnDef: { meta: { dataType: "datetime" as const, dtype: "datetime64[ns]", }, }, getColumnFormatting: () => undefined, getColumnWrapping: () => undefined, applyColumnFormatting: (value: unknown) => value, }) as unknown as Column; it("should handle null, empty string, and 'null' string datetime values without throwing RangeError", () => { const mockColumn = createMockColumn(); // Test null, empty string, and "null" string (as they might come from SQL) const nullishValues = [null, "", "null"]; nullishValues.forEach((value) => { const result = renderCellValue({ column: mockColumn, renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); expect(result).toBeDefined(); // Should not throw RangeError when rendering expect(() => { render( {result} , ); }).not.toThrow(); }); }); it("should handle invalid date strings without throwing RangeError", () => { const mockColumn = createMockColumn(); const invalidDates = [ "invalid-date", "2024-13-45", // Invalid month/day "not-a-date", "2024-06-14 12:34:20.665332", ]; invalidDates.forEach((invalidDate) => { const result = renderCellValue({ column: mockColumn, renderValue: () => invalidDate, getValue: () => invalidDate, selectCell: undefined, cellStyles: "", }); expect(result).toBeDefined(); // Should not throw RangeError when rendering expect(() => { render( {result} , ); }).not.toThrow(); }); }); it("should still render valid datetime strings correctly", () => { const mockColumn = createMockColumn(); const validDates = [ "2024-06-14T12:34:20Z", "2024-06-14 12:34:20", "2024-06-14", ]; validDates.forEach((validDate) => { const result = renderCellValue({ column: mockColumn, renderValue: () => validDate, getValue: () => validDate, selectCell: undefined, cellStyles: "", }); expect(result).toBeDefined(); // Should render as a date component, not as plain string expect(result).not.toBeNull(); // Should not throw when rendering expect(() => { render( {result} , ); }).not.toThrow(); }); }); it("should handle invalid Date instances without throwing RangeError", () => { const mockColumn = createMockColumn(); const invalidDate = new Date("invalid"); const result = renderCellValue({ column: mockColumn, renderValue: () => invalidDate, getValue: () => invalidDate, selectCell: undefined, cellStyles: "", }); expect(result).toBeDefined(); // Should not throw RangeError when rendering expect(() => { render( {result} , ); }).not.toThrow(); }); it("should handle mixed valid and null datetime values in a column", () => { const mockColumn = createMockColumn(); const values = [ "2024-06-14T12:34:20Z", // Valid null, "2024-06-15T12:34:20Z", // Valid "", "2024-06-16T12:34:20Z", // Valid ]; values.forEach((value) => { const result = renderCellValue({ column: mockColumn, renderValue: () => value, getValue: () => value, selectCell: undefined, cellStyles: "", }); expect(result).toBeDefined(); // Should not throw RangeError when rendering expect(() => { render( {result} , ); }).not.toThrow(); }); }); }); test("column_widths sets fixed size and meta.width", () => { const cols = generateColumns({ rowHeaders: [], selection: null, fieldTypes: [ ["a", ["number", "int64"]], ["b", ["string", "str"]], ], columnWidths: { a: 120 }, }); const a = cols.find((c) => c.id === "a"); const b = cols.find((c) => c.id === "b"); expect(a?.size).toBe(120); expect(a?.minSize).toBe(120); expect(a?.maxSize).toBe(120); expect(a?.meta?.width).toBe(120); expect(b?.size).toBeUndefined(); expect(b?.meta?.width).toBeUndefined(); });