Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

1022 lines
28 KiB
TypeScript

import { test, describe, afterEach, expect } from "vitest";
import {
cleanup,
render,
fireEvent,
waitFor,
within
} from "@self/tootils/render";
import { tick } from "svelte";
import Dataframe from "./Index.svelte";
const default_props = {
value: {
data: [
["Alice", "30", "Engineer"],
["Bob", "25", "Designer"],
["Carol", "35", "Manager"]
],
headers: ["Name", "Age", "Role"],
metadata: null
},
col_count: [3, "fixed"] as [number, "fixed" | "dynamic"],
row_count: [3, "fixed"] as [number, "fixed" | "dynamic"],
editable: true,
datatype: "str" as const,
root: "",
static_columns: [] as (string | number)[],
latex_delimiters: [] as { left: string; right: string; display: boolean }[],
line_breaks: true,
wrap: false,
column_widths: [] as string[],
show_row_numbers: false,
show_search: "none" as "none" | "search" | "filter",
pinned_columns: 0,
fullscreen: false,
max_height: 500
};
function get_cell(container: HTMLElement, row: number, col: number) {
return container.querySelector(
`[data-row='${row}'][data-col='${col}']`
) as HTMLElement | null;
}
function get_header_cells(container: HTMLElement) {
return container.querySelectorAll("th.header-cell");
}
function get_rows(container: HTMLElement) {
return container.querySelectorAll(".virtual-row");
}
function get_table_wrap(container: HTMLElement) {
return container.querySelector(".table-wrap") as HTMLElement;
}
async function wait(ms = 50) {
await new Promise((r) => setTimeout(r, ms));
await tick();
await tick();
}
describe("Dataframe rendering", () => {
afterEach(() => cleanup());
test("renders provided headers", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const headers = get_header_cells(container);
const header_texts = Array.from(headers).map(
(h) => h.textContent?.trim() ?? ""
);
expect(header_texts).toContain("Name");
expect(header_texts).toContain("Age");
expect(header_texts).toContain("Role");
});
test("renders provided cell values", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const alice_cell = get_cell(container, 0, 0);
expect(alice_cell?.textContent).toContain("Alice");
const bob_cell = get_cell(container, 1, 0);
expect(bob_cell?.textContent).toContain("Bob");
});
test("renders correct number of rows", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const rows = get_rows(container);
expect(rows.length).toBe(3);
});
test("renders label when show_label is true", async () => {
const { container } = await render(Dataframe, {
...default_props,
label: "Test Table",
show_label: true
});
await wait();
expect(container.textContent).toContain("Test Table");
});
test("renders row numbers when show_row_numbers is true", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_row_numbers: true
});
await wait();
const row_number_cells = container.querySelectorAll(".row-number-cell");
expect(row_number_cells.length).toBeGreaterThan(0);
});
test("renders search input when show_search is 'search'", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_search: "search"
});
await wait();
const search_input = container.querySelector(
"input.search-input"
) as HTMLInputElement;
expect(search_input).not.toBeNull();
expect(search_input.placeholder).toBe("Search...");
});
test("renders filter input when show_search is 'filter'", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_search: "filter"
});
await wait();
const filter_input = container.querySelector(
"input.search-input"
) as HTMLInputElement;
expect(filter_input).not.toBeNull();
expect(filter_input.placeholder).toBe("Filter...");
});
});
describe("Cell editing", () => {
afterEach(() => cleanup());
test("double-click enters edit mode (shows textarea)", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
);
expect(textarea).not.toBeNull();
});
test("Escape key cancels edit", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Escape" });
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
);
expect(textarea).toBeNull();
});
test("non-editable dataframe prevents editing on double-click", async () => {
const { container } = await render(Dataframe, {
...default_props,
interactive: false,
editable: false
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
);
expect(textarea).toBeNull();
});
test("static columns cannot be edited", async () => {
const { container } = await render(Dataframe, {
...default_props,
static_columns: [0]
});
await wait();
const static_cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(static_cell);
await tick();
await fireEvent.dblClick(static_cell);
await wait();
// static cell should not have an editable textarea
const textarea = static_cell.querySelector("textarea");
expect(textarea).toBeNull();
});
});
describe("Cell selection", () => {
afterEach(() => cleanup());
test("click selects a cell", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 1, 1)!;
await fireEvent.mouseDown(cell);
await wait();
// cell should have selection styling
expect(cell.className).toContain("cell-selected");
});
test("arrow keys move selection", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "ArrowRight" });
await wait();
const next_cell = get_cell(container, 0, 1)!;
expect(next_cell.className).toContain("cell-selected");
});
test("Tab moves to next cell", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Tab" });
await wait();
const next_cell = get_cell(container, 0, 1)!;
expect(next_cell.className).toContain("cell-selected");
});
test("shift+click selects a range", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const first_cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(first_cell);
await wait();
const last_cell = get_cell(container, 1, 1)!;
await fireEvent.mouseDown(last_cell, { shiftKey: true });
await wait();
// all 4 cells in the 2x2 range should be selected
expect(get_cell(container, 0, 0)!.className).toContain("cell-selected");
expect(get_cell(container, 0, 1)!.className).toContain("cell-selected");
expect(get_cell(container, 1, 0)!.className).toContain("cell-selected");
expect(get_cell(container, 1, 1)!.className).toContain("cell-selected");
});
// Regression: pressing Ctrl between mousedown and mouseup must not call
// handle_cell_click a second time via the click event. If it did, the
// Ctrl+click path would toggle the already-selected cell back off,
// producing conflicting state updates that froze the interface.
test("pressing Ctrl between mousedown and mouseup keeps cell selected", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
// mousedown (no ctrl) selects the cell
await fireEvent.mouseDown(cell);
await wait();
expect(cell.className).toContain("cell-selected");
// The browser fires a click event when the user releases the mouse button.
// If the user pressed Ctrl before releasing, that click carries ctrlKey:true.
// A second call to handle_cell_click with ctrlKey would toggle the cell
// back off (deselect it). Verify that the click event has no effect.
await fireEvent.click(cell, { ctrlKey: true });
await wait();
expect(cell.className).toContain("cell-selected");
});
});
describe("Header overflow", () => {
let wrapper: HTMLDivElement;
afterEach(async () => {
cleanup();
wrapper?.remove();
});
// Headers must expand to fit their text (no ellipsis truncation).
// When headers are wider than the scroll container, later columns are
// hidden by the container's overflow — not by cutting off the text.
test("long headers are not truncated in the DOM and the 3rd header is hidden by scroll overflow", async () => {
// Narrow container makes overflow predictable regardless of viewport size.
wrapper = document.createElement("div");
wrapper.style.width = "300px";
document.body.appendChild(wrapper);
// Capital W is one of the widest glyphs — 60 of them easily exceeds 300 px.
const long_header = "W".repeat(60);
const { getAllByRole } = await render(
Dataframe,
{
...default_props,
value: {
data: [["a", "b", "c"]],
headers: [long_header + " A", long_header + " B", long_header + " C"],
metadata: null
},
col_count: [3, "fixed"] as [number, "fixed" | "dynamic"],
row_count: [1, "fixed"] as [number, "fixed" | "dynamic"]
},
{ container: wrapper }
);
await wait();
const headers = getAllByRole("columnheader");
expect(headers).toHaveLength(3);
// Full text must be present in the DOM — no in-cell truncation.
expect(headers[2].textContent?.trim()).toContain(long_header + " C");
// The 3rd header's left edge must be beyond the scroll container's right
// edge, meaning it is completely outside the visible area.
const viewport = wrapper.querySelector(
".virtual-table-viewport"
) as HTMLElement;
const viewport_rect = viewport.getBoundingClientRect();
const third_rect = headers[2].getBoundingClientRect();
expect(third_rect.left).toBeGreaterThanOrEqual(viewport_rect.right);
});
});
describe("Sorting", () => {
afterEach(() => cleanup());
test("sort ascending via header menu", async () => {
const { container } = await render(Dataframe, {
...default_props,
col_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
});
await wait();
// Click on the "Age" header — find by content containing "Age"
const headers = get_header_cells(container);
const age_header = Array.from(headers).find((h) =>
h.textContent?.includes("Age")
) as HTMLElement;
expect(age_header).toBeTruthy();
// Click the menu button on the header
const menu_btn = age_header.querySelector(
".cell-menu-button"
) as HTMLElement;
expect(menu_btn).toBeTruthy();
await fireEvent.click(menu_btn);
await wait();
// Click "Sort ascending" in the menu
const sort_asc_btn = Array.from(
document.querySelectorAll('[role="menuitem"]')
).find((el) => el.textContent?.includes("sort_ascending")) as HTMLElement;
expect(sort_asc_btn).toBeTruthy();
await fireEvent.click(sort_asc_btn);
await wait();
// After sorting by Age ascending, Bob (25) should be first visible row
const first_row = container.querySelector(".virtual-row");
const first_name_cell = first_row?.querySelector("[data-col='0']");
expect(first_name_cell?.textContent).toContain("Bob");
});
});
describe("Global search/filter", () => {
afterEach(() => cleanup());
test("search filters visible rows", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_search: "search"
});
await wait();
const search_input = container.querySelector(
"input.search-input"
) as HTMLInputElement;
expect(search_input).not.toBeNull();
// Type a search query
search_input.focus();
await fireEvent.input(search_input, { target: { value: "Alice" } });
await wait(100);
const rows = get_rows(container);
expect(rows.length).toBe(1);
const first_cell = get_cell(container, 0, 0);
expect(first_cell?.textContent).toContain("Alice");
});
test("clearing search restores all rows", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_search: "search"
});
await wait();
const search_input = container.querySelector(
"input.search-input"
) as HTMLInputElement;
// Search then clear
await fireEvent.input(search_input, { target: { value: "Alice" } });
await wait(100);
expect(get_rows(container).length).toBe(1);
await fireEvent.input(search_input, { target: { value: "" } });
await wait(100);
expect(get_rows(container).length).toBe(3);
});
test("filter mode shows apply button when query present", async () => {
const { container } = await render(Dataframe, {
...default_props,
show_search: "filter"
});
await wait();
const filter_input = container.querySelector(
"input.search-input"
) as HTMLInputElement;
await fireEvent.input(filter_input, { target: { value: "Alice" } });
await wait();
const apply_btn = container.querySelector(
"button[aria-label='Apply filter and update dataframe values']"
);
expect(apply_btn).not.toBeNull();
});
});
describe("Add/remove rows and columns", () => {
afterEach(() => cleanup());
const dynamic_props = {
...default_props,
col_count: [3, "dynamic"] as [number, "fixed" | "dynamic"],
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
};
test("add row button appends a new row", async () => {
const { container } = await render(Dataframe, dynamic_props);
await wait();
const initial_rows = get_rows(container).length;
// The empty row button should be present for dynamic row_count
const add_btn = container.querySelector(".empty-row-button") as HTMLElement;
if (add_btn) {
await fireEvent.click(add_btn);
await wait();
expect(get_rows(container).length).toBe(initial_rows + 1);
}
});
// Cell menu add row tests: The CellMenu renders outside the table-wrap parent,
// so the document click handler (handle_click_outside) unmounts it before
// the menu button's onclick fires in synthetic event dispatch. These
// interactions are covered by E2E tests in dataframe_events.spec.ts.
test.todo("add row above via cell menu");
test.todo("add row below via cell menu");
test("delete row via cell menu", async () => {
const { container } = await render(Dataframe, dynamic_props);
await wait();
const cell = get_cell(container, 1, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const menu_btn = cell.querySelector(".cell-menu-button") as HTMLElement;
expect(menu_btn).toBeTruthy();
await fireEvent.click(menu_btn);
await wait();
const delete_btn = document.querySelector(
'[aria-label="Delete row"]'
) as HTMLElement;
expect(delete_btn).toBeTruthy();
await fireEvent.click(delete_btn);
await wait();
expect(get_rows(container).length).toBe(2);
});
test("add column via header menu", async () => {
const { container } = await render(Dataframe, dynamic_props);
await wait();
const initial_headers = get_header_cells(container).length;
// Click a header to select it and show menu
const headers = get_header_cells(container);
const header = headers[0] as HTMLElement;
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
if (menu_btn) {
await fireEvent.click(menu_btn);
await wait();
const add_col_btn = document.querySelector(
'[aria-label="Add column to the right"]'
) as HTMLElement;
if (add_col_btn) {
await fireEvent.click(add_col_btn);
await wait();
expect(get_header_cells(container).length).toBe(initial_headers + 1);
}
}
});
test("delete column via header menu", async () => {
const { container } = await render(Dataframe, dynamic_props);
await wait();
const initial_headers = get_header_cells(container).length;
const headers = get_header_cells(container);
const header = headers[0] as HTMLElement;
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
if (menu_btn) {
await fireEvent.click(menu_btn);
await wait();
const delete_col_btn = document.querySelector(
'[aria-label="Delete column"]'
) as HTMLElement;
if (delete_col_btn) {
await fireEvent.click(delete_col_btn);
await wait();
expect(get_header_cells(container).length).toBe(initial_headers - 1);
}
}
});
});
describe("Cell menu visibility on static cells", () => {
afterEach(() => cleanup());
test("context menu does not open on a static column cell", async () => {
const { container } = await render(Dataframe, {
...default_props,
static_columns: [0]
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.contextMenu(cell);
await wait();
expect(within(document.body).queryByRole("menu")).toBeNull();
});
test("context menu does not open when interactive is false", async () => {
const { container } = await render(Dataframe, {
...default_props,
interactive: false,
editable: false
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.contextMenu(cell);
await wait();
expect(within(document.body).queryByRole("menu")).toBeNull();
});
test("context menu does open on an editable, non-static cell", async () => {
const { container } = await render(Dataframe, {
...default_props,
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.contextMenu(cell);
await wait();
expect(within(document.body).queryByRole("menu")).not.toBeNull();
});
test("cell menu button does not render on a static column cell", async () => {
const { container } = await render(Dataframe, {
...default_props,
static_columns: [0]
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
expect(cell.querySelector(".cell-menu-button")).toBeNull();
});
});
describe("Delete/clear cells", () => {
afterEach(() => cleanup());
test("Delete key clears selected cell value", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
// Press Escape first to ensure we're in selection mode, not edit mode
await fireEvent.keyDown(table_wrap, { key: "Escape" });
await fireEvent.keyDown(table_wrap, { key: "Delete" });
await wait();
// Cell should be empty
const cell_text = get_cell(container, 0, 0)?.textContent?.trim();
expect(cell_text === "" || cell_text === "⋮").toBe(true);
});
test("Backspace key clears selected cell value", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 1)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Escape" });
await fireEvent.keyDown(table_wrap, { key: "Backspace" });
await wait();
const cell_text = get_cell(container, 0, 1)?.textContent?.trim();
expect(cell_text === "" || cell_text === "⋮").toBe(true);
});
test("Delete key does not clear static column cells", async () => {
const { container } = await render(Dataframe, {
...default_props,
static_columns: [0]
});
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Escape" });
await fireEvent.keyDown(table_wrap, { key: "Delete" });
await wait();
// Static cell should still have its value
expect(get_cell(container, 0, 0)?.textContent).toContain("Alice");
});
});
describe("Events", () => {
afterEach(() => cleanup());
test("change: emitted when value changes from outside via set_data", async () => {
const { listen, set_data } = await render(Dataframe, default_props);
await wait();
const change = listen("change");
await set_data({
value: {
data: [
["X", "Y", "Z"],
["1", "2", "3"],
["4", "5", "6"]
],
headers: ["Name", "Age", "Role"],
metadata: null
}
});
await wait();
expect(change).toHaveBeenCalled();
});
test("select: emitted when cell is clicked", async () => {
const { container, listen } = await render(Dataframe, default_props);
await wait();
const select = listen("select");
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
expect(select).toHaveBeenCalled();
});
test("change: emitted when cell is edited", async () => {
const { container, listen } = await render(Dataframe, default_props);
await wait();
const change = listen("change");
// Double-click to enter edit mode
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
) as HTMLTextAreaElement;
if (textarea) {
textarea.value = "NewValue";
await fireEvent.input(textarea);
await tick();
// Press Enter to confirm
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Enter" });
await wait();
expect(change).toHaveBeenCalled();
}
});
test("input: emitted on cell edit", async () => {
const { container, listen } = await render(Dataframe, default_props);
await wait();
const input = listen("input");
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
) as HTMLTextAreaElement;
if (textarea) {
textarea.value = "Changed";
await fireEvent.input(textarea);
await tick();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Enter" });
await wait();
expect(input).toHaveBeenCalled();
}
});
test("edit: emitted with old and new value on cell edit", async () => {
const { container, listen } = await render(Dataframe, default_props);
await wait();
const edit = listen("edit");
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await tick();
await fireEvent.dblClick(cell);
await wait();
const textarea = container.querySelector(
"textarea[aria-label='Edit cell']"
) as HTMLTextAreaElement;
if (textarea) {
textarea.value = "Zara";
await fireEvent.input(textarea);
await tick();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "Enter" });
await wait();
expect(edit).toHaveBeenCalled();
const call_args = edit.mock.calls[0][0];
expect(call_args.value).toBe("Zara");
expect(call_args.previous_value).toBe("Alice");
}
});
});
describe("Copy", () => {
afterEach(() => cleanup());
test("Cmd+C copies selected cell to clipboard", async () => {
const { container } = await render(Dataframe, default_props);
await wait();
const cell = get_cell(container, 0, 0)!;
await fireEvent.mouseDown(cell);
await wait();
const table_wrap = get_table_wrap(container)!;
await fireEvent.keyDown(table_wrap, { key: "c", metaKey: true });
// handle_copy() is async but called without await in the keydown handler,
// so the clipboard write may not be complete immediately. Poll until it settles.
await waitFor(async () => {
expect(await navigator.clipboard.readText()).toBe("Alice");
});
});
});
describe("Boolean column select-all header checkbox", () => {
afterEach(() => cleanup());
const bool_props = {
...default_props,
value: {
data: [
["Alice", true, false],
["Bob", false, false],
["Carol", true, false]
],
headers: ["Name", "Active", "Admin"],
metadata: null
},
datatype: ["str", "bool", "bool"] as any,
col_count: [3, "fixed"] as [number, "fixed" | "dynamic"]
};
function get_header(container: HTMLElement, col_idx: number): HTMLElement {
return container.querySelector<HTMLElement>(
`th[data-heading='${col_idx}']`
)!;
}
function get_header_checkbox(
container: HTMLElement,
col_idx: number
): HTMLInputElement {
return within(get_header(container, col_idx)).getByTestId(
"checkbox"
) as HTMLInputElement;
}
test("renders checkbox in bool column header (interactive mode)", async () => {
const { container } = await render(Dataframe, bool_props);
await wait();
expect(get_header_checkbox(container, 1)).toBeInTheDocument();
expect(get_header_checkbox(container, 2)).toBeInTheDocument();
expect(
within(get_header(container, 0)).queryByTestId("checkbox")
).not.toBeInTheDocument();
});
test("does not render checkbox when not interactive", async () => {
const { container } = await render(Dataframe, {
...bool_props,
interactive: false
});
await wait();
expect(
within(get_header(container, 1)).queryByTestId("checkbox")
).not.toBeInTheDocument();
});
test("header checkbox reflects indeterminate state for mixed column", async () => {
const { container } = await render(Dataframe, bool_props);
await wait();
const active = get_header_checkbox(container, 1);
expect(active.indeterminate).toBe(true);
expect(active.checked).toBe(false);
});
test("header checkbox is checked when all column values are true", async () => {
const { container } = await render(Dataframe, {
...bool_props,
value: {
data: [
["Alice", true],
["Bob", true]
],
headers: ["Name", "Active"],
metadata: null
},
datatype: ["str", "bool"] as any,
col_count: [2, "fixed"] as [number, "fixed" | "dynamic"]
});
await wait();
const active = get_header_checkbox(container, 1);
expect(active.checked).toBe(true);
expect(active.indeterminate).toBe(false);
});
test("clicking indeterminate header checkbox sets all column cells to true", async () => {
const { container, listen } = await render(Dataframe, bool_props);
await wait();
const change = listen("change");
await fireEvent.click(get_header_checkbox(container, 1));
await wait();
const data = change.mock.calls.at(-1)?.[0].data;
expect(data).toEqual([
["Alice", true, false],
["Bob", true, false],
["Carol", true, false]
]);
const updated = get_header_checkbox(container, 1);
expect(updated.checked).toBe(true);
expect(updated.indeterminate).toBe(false);
});
test("clicking fully-checked header checkbox sets all column cells to false", async () => {
const { container, listen } = await render(Dataframe, {
...bool_props,
value: {
data: [
["Alice", true, false],
["Bob", true, false]
],
headers: ["Name", "Active", "Admin"],
metadata: null
}
});
await wait();
const change = listen("change");
await fireEvent.click(get_header_checkbox(container, 1));
await wait();
const data = change.mock.calls.at(-1)?.[0].data;
expect(data).toEqual([
["Alice", false, false],
["Bob", false, false]
]);
});
test("toggling one bool header does not affect cells in other bool columns", async () => {
const { container, listen } = await render(Dataframe, {
...bool_props,
value: {
data: [
["Alice", false, true],
["Bob", false, true],
["Carol", false, true]
],
headers: ["Name", "Active", "Admin"],
metadata: null
}
});
await wait();
const change = listen("change");
await fireEvent.click(get_header_checkbox(container, 1));
await wait();
const data = change.mock.calls.at(-1)?.[0].data;
expect(data).toEqual([
["Alice", true, true],
["Bob", true, true],
["Carol", true, true]
]);
const admin = get_header_checkbox(container, 2);
expect(admin.checked).toBe(true);
expect(admin.indeterminate).toBe(false);
});
});