/* Copyright 2026 Marimo. All rights reserved. */
import { describe, expect, it } from "vitest";
import { SELECT_COLUMN_ID } from "../../types";
import {
countDataCellsInSelection,
getCellsBetween,
getCellValues,
getNumericValuesFromSelectedCells,
} from "../utils";
import {
createMockCell,
createMockColumn,
createMockRow,
createMockTable,
createSelectedCell,
} from "./test-utils";
describe("getCellValues", () => {
it("should return empty string for empty selection", () => {
const mockTable = createMockTable([], []);
const result = getCellValues(mockTable, new Set());
expect(result.text).toBe("");
expect(result.html).toBeUndefined();
});
it("should ignore select checkbox in tables", () => {
const cell = createMockCell(`row_${SELECT_COLUMN_ID}`, "test");
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set());
expect(result.text).toBe("");
expect(result.html).toBeUndefined();
});
it("should return single cell value", () => {
const cell = createMockCell("0_0", "test");
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_0"]));
expect(result.text).toBe("test");
expect(result.html).toBeUndefined();
});
it("should return multiple cells from same row separated by tabs", () => {
const cell1 = createMockCell("0_0", "value1");
const cell2 = createMockCell("0_1", "value2");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_0", "0_1"]));
expect(result.text).toBe("value1\tvalue2");
expect(result.html).toBeUndefined();
});
it("should return multiple rows separated by newlines", () => {
const cell1 = createMockCell("0_0", "row1");
const cell2 = createMockCell("1_0", "row2");
const row1 = createMockRow("0", [cell1]);
const row2 = createMockRow("1", [cell2]);
const table = createMockTable([row1, row2], []);
const result = getCellValues(table, new Set(["0_0", "1_0"]));
expect(result.text).toBe("row1\nrow2");
expect(result.html).toBeUndefined();
});
it("should handle missing rows gracefully", () => {
const cell = createMockCell("0_0", "test");
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
// Row "999" doesn't exist and is skipped
const result = getCellValues(table, new Set(["0_0", "999_0"]));
expect(result.text).toBe("test");
});
it("should include undefined for missing columns on existing rows", () => {
const cell1 = createMockCell("0_0", "test1");
const cell2 = createMockCell("0_1", "test2");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
// Column "999" doesn't exist but row.getValue() returns undefined
const result = getCellValues(table, new Set(["0_0", "0_1", "0_999"]));
expect(result.text).toBe("test1\ttest2\tundefined");
});
it("should handle complex data types", () => {
const cell1 = createMockCell("0_0", { name: "test" });
const cell2 = createMockCell("0_1", null);
const cell3 = createMockCell("0_2", undefined);
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_0", "0_1", "0_2"]));
expect(result.text).toBe('{"name":"test"}\tnull\tundefined');
expect(result.html).toBeUndefined();
});
it("should use raw values when rawData is available", () => {
const mimeBundle = {
_serialized_mime_bundle: {
mimetype: "text/html",
data: "formatted_42",
},
};
const cell1 = createMockCell("0_a", mimeBundle);
const cell2 = createMockCell("0_b", "displayed_b");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], [], {
rawData: [{ a: 42, b: "raw_b" }],
});
const result = getCellValues(table, new Set(["0_a", "0_b"]));
expect(result.text).toBe("42\traw_b");
expect(result.html).toBe(
"
",
);
});
it("should fall back to displayed value when rawData is not set", () => {
const cell = createMockCell("0_name", "displayed");
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_name"]));
expect(result.text).toBe("displayed");
expect(result.html).toBeUndefined();
});
it("should return html table when cells contain mime bundles", () => {
const mimeBundle = {
_serialized_mime_bundle: {
mimetype: "text/html",
data: 'link',
},
};
const cell1 = createMockCell("0_url", mimeBundle);
const cell2 = createMockCell("0_name", "plain text");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_url", "0_name"]));
expect(result.text).toBe("link\tplain text");
expect(result.html).toBe(
'',
);
});
it("should escape html entities in non-mime cells within html table", () => {
const mimeBundle = {
_serialized_mime_bundle: {
mimetype: "text/html",
data: "bold",
},
};
const cell1 = createMockCell("0_html", mimeBundle);
const cell2 = createMockCell("0_text", "a < b & c > d");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getCellValues(table, new Set(["0_html", "0_text"]));
expect(result.html).toBe(
"",
);
});
});
describe("getCellsBetween", () => {
it("should return empty array when start row is not found", () => {
const cell = createMockCell("0_0", "test");
const rows = [createMockRow("0", [cell])];
const table = createMockTable(rows, []);
const cellStart = createSelectedCell("999", "0"); // non existent row
const cellEnd = createSelectedCell("0", "0");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual([]);
});
it("should return empty array when end row is not found", () => {
const cell = createMockCell("0_0", "test");
const rows = [createMockRow("0", [cell])];
const table = createMockTable(rows, []);
const cellStart = createSelectedCell("0", "0");
const cellEnd = createSelectedCell("999", "0"); // non existent row
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual([]);
});
it("should return single cell when start and end are the same", () => {
const cell = createMockCell("0_0", "test");
const rows = [createMockRow("0", [cell])];
const columns = [createMockColumn("0")];
const table = createMockTable(rows, columns);
const cellStart = createSelectedCell("0", "0");
const cellEnd = createSelectedCell("0", "0");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual(["0_0"]);
});
it("should return cells in a single row range", () => {
const cell1 = createMockCell("0_0", "test1");
const cell2 = createMockCell("0_1", "test2");
const cell3 = createMockCell("0_2", "test3");
const cell4 = createMockCell("0_3", "test4");
const cell5 = createMockCell("0_4", "test5");
const rows = [createMockRow("0", [cell1, cell2, cell3, cell4, cell5])];
const columns = [
createMockColumn("0"),
createMockColumn("1"),
createMockColumn("2"),
createMockColumn("3"),
createMockColumn("4"),
];
const table = createMockTable(rows, columns);
const startCell = createSelectedCell("0", "1");
const endCell = createSelectedCell("0", "3");
const result = getCellsBetween(table, startCell, endCell);
expect(result).toEqual(["0_1", "0_2", "0_3"]);
});
it("should return cells in a single column range", () => {
const rows = [
createMockRow("0", [
createMockCell("0_0", "test1"),
createMockCell("0_1", "test2"),
createMockCell("0_2", "test3"),
]),
createMockRow("1", [
createMockCell("1_0", "test4"),
createMockCell("1_1", "test5"),
createMockCell("1_2", "test6"),
]),
createMockRow("2", [
createMockCell("2_0", "test7"),
createMockCell("2_1", "test8"),
createMockCell("2_2", "test9"),
]),
];
const columns = [
createMockColumn("0"),
createMockColumn("1"),
createMockColumn("2"),
];
const table = createMockTable(rows, columns);
const cellStart = createSelectedCell("0", "1");
const cellEnd = createSelectedCell("2", "1");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual(["0_1", "1_1", "2_1"]);
});
it("should return cells in a rectangular range", () => {
const rows = [
createMockRow("0", [
createMockCell("0_0", "test1"),
createMockCell("0_1", "test2"),
createMockCell("0_2", "test3"),
]),
createMockRow("1", [
createMockCell("1_0", "test4"),
createMockCell("1_1", "test5"),
createMockCell("1_2", "test6"),
]),
createMockRow("2", [
createMockCell("2_0", "test7"),
createMockCell("2_1", "test8"),
createMockCell("2_2", "test9"),
]),
];
const columns = [
createMockColumn("0"),
createMockColumn("1"),
createMockColumn("2"),
];
const table = createMockTable(rows, columns);
const cellStart = createSelectedCell("0", "1");
const cellEnd = createSelectedCell("2", "2");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual(["0_1", "0_2", "1_1", "1_2", "2_1", "2_2"]);
});
it("should work when end is before start (reverse selection)", () => {
const rows = [
createMockRow("0", [
createMockCell("0_0", "test1"),
createMockCell("0_1", "test2"),
createMockCell("0_2", "test3"),
]),
createMockRow("1", [
createMockCell("1_0", "test4"),
createMockCell("1_1", "test5"),
createMockCell("1_2", "test6"),
]),
];
const columns = [
createMockColumn("0"),
createMockColumn("1"),
createMockColumn("2"),
];
const table = createMockTable(rows, columns);
const cellStart = createSelectedCell("1", "2");
const cellEnd = createSelectedCell("0", "0");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual(["0_0", "0_1", "0_2", "1_0", "1_1", "1_2"]);
});
it("should handle missing cells gracefully", () => {
const rows = [
createMockRow("0", [
createMockCell("0_0", "test1"),
createMockCell("0_1", "test2"),
createMockCell("0_2", "test3"),
]),
];
const columns = [
createMockColumn("0"),
createMockColumn("1"),
createMockColumn("2"),
];
const table = createMockTable(rows, columns);
const cellStart = createSelectedCell("0", "999");
const cellEnd = createSelectedCell("0", "0");
const result = getCellsBetween(table, cellStart, cellEnd);
expect(result).toEqual([]);
});
});
describe("getNumericValuesFromSelectedCells", () => {
it("should return empty array for empty selection", () => {
const mockTable = createMockTable([], []);
const result = getNumericValuesFromSelectedCells(mockTable, new Set());
expect(result).toEqual([]);
});
it("should ignore select checkbox in tables", () => {
const cell = createMockCell(`row_${SELECT_COLUMN_ID}`, 10);
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set([`row_${SELECT_COLUMN_ID}`]),
);
expect(result).toEqual([]);
});
it("should extract numeric values from number cells", () => {
const cell1 = createMockCell("0_0", 10);
const cell2 = createMockCell("0_1", 20);
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1"]),
);
expect(result).toEqual([10, 20]);
});
it("should parse string numbers to numeric values", () => {
const cell1 = createMockCell("0_0", "42");
const cell2 = createMockCell("0_1", "3.14");
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1"]),
);
expect(result).toEqual([42, 3.14]);
});
it("should skip non-numeric values", () => {
const cell1 = createMockCell("0_0", 10);
const cell2 = createMockCell("0_1", "abc");
const cell3 = createMockCell("0_2", undefined);
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "0_2"]),
);
expect(result).toEqual([10]);
});
it("should skip NaN and Infinity", () => {
const cell1 = createMockCell("0_0", 5);
const cell2 = createMockCell("0_1", Number.NaN);
const cell3 = createMockCell("0_2", Infinity);
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "0_2"]),
);
expect(result).toEqual([5]);
});
it("should skip booleans (not treat as 1 or 0)", () => {
const cell1 = createMockCell("0_0", 10);
const cell2 = createMockCell("0_1", true);
const cell3 = createMockCell("0_2", false);
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "0_2"]),
);
expect(result).toEqual([10]);
});
it("should skip null and empty string (not treat as 0)", () => {
const cell1 = createMockCell("0_0", 10);
const cell2 = createMockCell("0_1", null);
const cell3 = createMockCell("0_2", "");
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "0_2"]),
);
expect(result).toEqual([10]);
});
it("should include string '0' as numeric zero", () => {
const cell1 = createMockCell("0_0", "0");
const cell2 = createMockCell("0_1", 0);
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1"]),
);
expect(result).toEqual([0, 0]);
});
it("should skip -Infinity", () => {
const cell1 = createMockCell("0_0", 5);
const cell2 = createMockCell("0_1", -Infinity);
const row = createMockRow("0", [cell1, cell2]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1"]),
);
expect(result).toEqual([5]);
});
it("should skip objects", () => {
const cell1 = createMockCell("0_0", 5);
const cell2 = createMockCell("0_1", { x: 1 });
const cell3 = createMockCell("0_2", [1, 2]);
const row = createMockRow("0", [cell1, cell2, cell3]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "0_2"]),
);
expect(result).toEqual([5]);
});
it("should handle missing cells gracefully", () => {
const cell = createMockCell("0_0", 100);
const row = createMockRow("0", [cell]);
const table = createMockTable([row], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_999", "999_0"]),
);
expect(result).toEqual([100]);
});
it("should return multiple numeric cells across rows", () => {
const row1 = createMockRow("0", [
createMockCell("0_0", 1),
createMockCell("0_1", 2),
]);
const row2 = createMockRow("1", [
createMockCell("1_0", 3),
createMockCell("1_1", 4),
]);
const table = createMockTable([row1, row2], []);
const result = getNumericValuesFromSelectedCells(
table,
new Set(["0_0", "0_1", "1_0", "1_1"]),
);
expect(result).toEqual([1, 2, 3, 4]);
});
});
describe("countDataCellsInSelection", () => {
it("should return 0 for empty selection", () => {
expect(countDataCellsInSelection(new Set())).toBe(0);
});
it("should count only non-checkbox cells", () => {
expect(countDataCellsInSelection(new Set(["0_0", "0_1", "1_0"]))).toBe(3);
});
it("should exclude select checkbox column cells", () => {
const selectCellId = `0_${SELECT_COLUMN_ID}`;
expect(
countDataCellsInSelection(new Set([selectCellId, "0_0", "0_1"])),
).toBe(2);
});
it("should return 0 when only checkbox cells are selected", () => {
const selectCellId1 = `0_${SELECT_COLUMN_ID}`;
const selectCellId2 = `1_${SELECT_COLUMN_ID}`;
expect(
countDataCellsInSelection(new Set([selectCellId1, selectCellId2])),
).toBe(0);
});
});