/* Copyright 2026 Marimo. All rights reserved. */
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SentinelCell, WhitespaceMarkers } from "../sentinel-cell";
import type { CellValueSentinel } from "../types";
function renderSentinel(sentinel: CellValueSentinel) {
const { container } = render();
return container.querySelector("span")!;
}
function renderMarkers(value: string) {
return render();
}
describe("SentinelCell", () => {
it("renders null as None", () => {
const span = renderSentinel({ type: "null", value: null });
expect(span.textContent).toBe("None");
expect(span.getAttribute("aria-label")).toBe("None");
expect(span.className).toContain("italic");
expect(span.className).toContain("bg-muted");
});
it("renders empty string as ", () => {
const span = renderSentinel({ type: "empty-string", value: "" });
expect(span.textContent).toBe("");
expect(span.getAttribute("aria-label")).toBe("empty string");
});
it("renders single space", () => {
const span = renderSentinel({ type: "whitespace", value: " " });
expect(span.textContent).toBe("\u2423");
expect(span.getAttribute("aria-label")).toBe("1 space");
});
it("renders multiple spaces", () => {
const span = renderSentinel({ type: "whitespace", value: " " });
expect(span.textContent).toBe("\u2423\u2423\u2423");
expect(span.getAttribute("aria-label")).toBe("3 spaces");
});
it("renders tab", () => {
const span = renderSentinel({ type: "whitespace", value: "\t" });
expect(span.textContent).toBe("\\t");
expect(span.getAttribute("aria-label")).toBe("1 tab");
});
it("renders newline", () => {
const span = renderSentinel({ type: "whitespace", value: "\n" });
expect(span.textContent).toBe("\\n");
expect(span.getAttribute("aria-label")).toBe("1 newline");
});
it("renders mixed whitespace", () => {
const span = renderSentinel({ type: "whitespace", value: "\t \n" });
expect(span.textContent).toBe("\\t\u2423\\n");
expect(span.getAttribute("aria-label")).toBe("1 tab, 1 space, 1 newline");
});
it("renders NaN", () => {
const span = renderSentinel({ type: "nan", value: Number.NaN });
expect(span.textContent).toBe("NaN");
});
it("renders inf", () => {
const span = renderSentinel({ type: "positive-infinity", value: Infinity });
expect(span.textContent).toBe("inf");
expect(span.getAttribute("title")).toBe("Infinity");
});
it("renders -inf", () => {
const span = renderSentinel({
type: "negative-infinity",
value: -Infinity,
});
expect(span.textContent).toBe("-inf");
expect(span.getAttribute("title")).toBe("-Infinity");
});
it("renders NaT", () => {
const span = renderSentinel({ type: "nat", value: "NaT" });
expect(span.textContent).toBe("NaT");
expect(span.getAttribute("title")).toBe("NaT (Not a Time)");
});
});
describe("WhitespaceMarkers", () => {
it("renders nothing for empty string", () => {
const { container } = renderMarkers("");
expect(container.firstChild).toBeNull();
});
it("renders a single space as open box", () => {
const { container } = renderMarkers(" ");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\u2423");
expect(outer.getAttribute("aria-label")).toBe("1 space");
});
it("renders multiple spaces as multiple open boxes", () => {
const { container } = renderMarkers(" ");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\u2423\u2423\u2423");
expect(outer.getAttribute("aria-label")).toBe("3 spaces");
});
it("renders tab, newline, CR with escape labels", () => {
const { container } = renderMarkers("\t\n\r");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\\t\\n\\r");
});
it("renders each char in its own span for CSS spacing", () => {
const { container } = renderMarkers(" ");
const outer = container.querySelector("span")!;
// Outer wrapper + three inner spans (one per char)
expect(outer.querySelectorAll("span")).toHaveLength(3);
});
it("renders unknown whitespace (NBSP) as \\uXXXX escape", () => {
const { container } = renderMarkers("\u00a0");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\\u00a0");
});
it("renders BOM as \\ufeff", () => {
const { container } = renderMarkers("\ufeff");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\\ufeff");
});
it("renders en space and em space as escapes", () => {
const { container } = renderMarkers("\u2002\u2003");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\\u2002\\u2003");
});
it("mixes known glyphs and unknown escapes correctly", () => {
const { container } = renderMarkers(" \t\u00a0");
const outer = container.querySelector("span")!;
expect(outer.textContent).toBe("\u2423\\t\\u00a0");
});
it("describes mixed whitespace in aria-label", () => {
const { container } = renderMarkers(" \t\n");
const outer = container.querySelector("span")!;
expect(outer.getAttribute("aria-label")).toBe("1 space, 1 tab, 1 newline");
});
it("describes unknown whitespace as 'unicode whitespace'", () => {
const { container } = renderMarkers("\u00a0");
const outer = container.querySelector("span")!;
expect(outer.getAttribute("aria-label")).toBe("1 unicode whitespace");
});
it("pluralizes unknown whitespace in aria-label", () => {
const { container } = renderMarkers("\u00a0\u00a0\u2002");
const outer = container.querySelector("span")!;
expect(outer.getAttribute("aria-label")).toBe("3 unicode whitespaces");
});
it("mixes known and unknown whitespace labels", () => {
const { container } = renderMarkers(" \u00a0\t");
const outer = container.querySelector("span")!;
expect(outer.getAttribute("aria-label")).toBe(
"1 space, 1 unicode whitespace, 1 tab",
);
});
});