From da2a308d31eb1865f841249dd97de199773fc793 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sat, 12 Apr 2025 12:21:28 -0700 Subject: [PATCH] test(data-table): add unit tests (#2144) --- tests/DataTable.test.svelte | 342 -------- tests/DataTable/DataTable.test.svelte | 107 +++ tests/DataTable/DataTable.test.ts | 775 ++++++++++++++++++ ...DataTableBatchSelectionToolbar.test.svelte | 45 +- .../DataTableBatchSelectionToolbar.test.ts | 168 ++++ tests/DataTable/DuplicateDataTables.test.ts | 73 ++ tests/DataTableAppendColumns.test.svelte | 38 - tests/DataTableBatchSelection.test.svelte | 24 - tests/DataTableNestedHeaders.test.svelte | 30 - 9 files changed, 1150 insertions(+), 452 deletions(-) delete mode 100644 tests/DataTable.test.svelte create mode 100644 tests/DataTable/DataTable.test.svelte create mode 100644 tests/DataTable/DataTable.test.ts rename tests/{ => DataTable}/DataTableBatchSelectionToolbar.test.svelte (57%) create mode 100644 tests/DataTable/DataTableBatchSelectionToolbar.test.ts create mode 100644 tests/DataTable/DuplicateDataTables.test.ts delete mode 100644 tests/DataTableAppendColumns.test.svelte delete mode 100644 tests/DataTableBatchSelection.test.svelte delete mode 100644 tests/DataTableNestedHeaders.test.svelte diff --git a/tests/DataTable.test.svelte b/tests/DataTable.test.svelte deleted file mode 100644 index 38ae3165..00000000 --- a/tests/DataTable.test.svelte +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - {#if header.key === "port"} - {header.value} - (network) - {:else}{header.value}{/if} - - - {#if cell.key === "rule" && cell.value === "Round robin"} - - {cell.value} - - - {:else}{cell.value}{/if} - - - - - - - - - { - return row.name.includes(value); - }} - /> - - Restart all - - API documentation - - Stop all - - - - - - - - - - - - Restart all - - API documentation - - Stop all - - - - - - - - - - - - - - - - - cost + " €" }, - { - key: "expireDate", - value: "Expire date", - display: (date) => new Date(date).toLocaleString(), - sort, - }, - ]} - rows={[ - { - id: 0, - name: "Load Balancer 3", - protocol: "HTTP", - port: 3000, - cost: 100, - expireDate: "2020-10-21", - }, - { - id: "b", - name: "Load Balancer 1", - protocol: "HTTP", - port: 443, - cost: 200, - expireDate: "2020-09-10", - }, - { - id: "c", - name: "Load Balancer 2", - protocol: "HTTP", - port: 80, - cost: 150, - expireDate: "2020-11-24", - }, - { - id: "d", - name: "Load Balancer 6", - protocol: "HTTP", - port: 3000, - cost: 250, - expireDate: "2020-12-01", - }, - { - id: "e", - name: "Load Balancer 4", - protocol: "HTTP", - port: 443, - cost: 550, - expireDate: "2021-03-21", - }, - { - id: "f", - name: "Load Balancer 5", - protocol: "HTTP", - port: 80, - cost: 400, - expireDate: "2020-11-14", - }, - ]} -/> - - -
-
-      {JSON.stringify(row, null, 2)}
-    
-
-
- - -
-
-      {JSON.stringify(row, null, 2)}
-    
-
-
- - - - - - - - - - - - - - - - - - { - return value + " Protocol"; - }, - }, - { - key: "port", - value: "Port", - display: (value, row) => { - console.log(row.port); - return value + " €"; - }, - sort: (a, b) => { - if (a > b) return 1; - return 0; - }, - }, - { - key: "rule", - value: "Rule", - }, - ]} - sortKey="name" - on:click:row={(e) => { - const detail = e.detail; - detail.name; - detail.port; - }} - on:click:cell={(e) => { - const detail = e.detail; - switch (detail.key) { - case "name": - detail.value; - break; - } - }} - on:click={(e) => { - e.detail.cell; - e.detail.row?.name; - }} - on:click:row--expand={(e) => { - const detail = e.detail; - detail.row.id; - detail.row.name; - }} -/> diff --git a/tests/DataTable/DataTable.test.svelte b/tests/DataTable/DataTable.test.svelte new file mode 100644 index 00000000..7f0dd3b0 --- /dev/null +++ b/tests/DataTable/DataTable.test.svelte @@ -0,0 +1,107 @@ + + + { + console.log("click", e.detail); + }} + on:click:header={(e) => { + console.log("click:header", e.detail); + }} + on:click:row={(e) => { + console.log("click:row", e.detail); + }} + on:click:cell={(e) => { + console.log("click:cell", e.detail); + }} + on:mouseenter:row={(e) => { + console.log("mouseenter:row", e.detail); + }} + on:mouseleave:row={(e) => { + console.log("mouseleave:row", e.detail); + }} +> + + diff --git a/tests/DataTable/DataTable.test.ts b/tests/DataTable/DataTable.test.ts new file mode 100644 index 00000000..7ab91561 --- /dev/null +++ b/tests/DataTable/DataTable.test.ts @@ -0,0 +1,775 @@ +import { render, screen } from "@testing-library/svelte"; +import { tick } from "svelte"; +import { user } from "../setup-tests"; +import DataTable from "./DataTable.test.svelte"; + +describe("DataTable", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const headers = [ + { key: "name", value: "Name" }, + { key: "protocol", value: "Protocol" }, + { key: "port", value: "Port" }, + { key: "rule", value: "Rule" }, + ] as const; + + const rows = [ + { + id: "a", + name: "Load Balancer 3", + protocol: "HTTP", + port: 3000, + rule: "Round robin", + }, + { + id: "b", + name: "Load Balancer 1", + protocol: "HTTP", + port: 443, + rule: "Round robin", + }, + { + id: "c", + name: "Load Balancer 2", + protocol: "HTTP", + port: 80, + rule: "DNS delegation", + }, + ]; + + // Basic rendering and structure tests + it("renders with default props", async () => { + const { container } = render(DataTable); + // Check if table headers are rendered + headers.forEach((header) => { + expect(screen.getByText(header.value)).toBeInTheDocument(); + }); + + // Check if table has correct structure + const table = container.querySelector("table"); + expect(table).toBeInTheDocument(); + expect(table).toHaveClass("bx--data-table"); + + // Check if table has correct number of rows + const tableRows = container.querySelectorAll("tbody tr"); + expect(tableRows).toHaveLength(3); + + // Check if all rows contain the expected data + rows.forEach((row) => { + const rowElement = screen.getByText(row.name).closest("tr"); + expect(rowElement).toBeInTheDocument(); + + if (rowElement) { + const cells = rowElement.querySelectorAll("td"); + expect(cells.length).toBe(4); + + const protocolCell = Array.from(cells).find( + (cell) => cell.textContent?.trim() === row.protocol, + ); + expect(protocolCell).toBeInTheDocument(); + + const portCell = Array.from(cells).find( + (cell) => cell.textContent?.trim() === row.port.toString(), + ); + expect(portCell).toBeInTheDocument(); + + const ruleCell = Array.from(cells).find( + (cell) => cell.textContent?.trim() === row.rule, + ); + expect(ruleCell).toBeInTheDocument(); + } + }); + }); + + it("renders with title and description", () => { + render(DataTable, { + props: { + title: "Test Table", + description: "Test Description", + headers, + rows, + }, + }); + + expect( + screen.getByRole("heading", { name: "Test Table" }), + ).toBeInTheDocument(); + expect(screen.getByText("Test Description")).toBeInTheDocument(); + }); + + it("handles empty table state", () => { + const { container } = render(DataTable, { + props: { + headers, + rows: [], + }, + }); + + const tableBody = container.querySelector("tbody"); + assert(tableBody); + expect(tableBody.children.length).toBe(0); + }); + + it("handles table with only one column", () => { + const singleColumnHeaders = [{ key: "name", value: "Name" }]; + + const { container } = render(DataTable, { + props: { + headers: singleColumnHeaders, + rows: rows.map(({ id, name }) => ({ id, name })), + }, + }); + + const columns = container.querySelectorAll("th"); + expect(columns.length).toBe(1); + expect(columns[0]).toHaveTextContent("Name"); + }); + + it("handles table with very long content", () => { + const longContentRows = [ + { + id: "a", + name: "A very long name that should be truncated or wrapped in the table cell", + protocol: "HTTP", + port: 3000, + rule: "Round robin", + }, + ]; + + const { container } = render(DataTable, { + props: { + headers, + rows: longContentRows, + }, + }); + + const longCell = container.querySelector("td"); + assert(longCell); + expect(longCell).toBeInTheDocument(); + }); + + // Sorting tests + it("handles sorting functionality", async () => { + const { container } = render(DataTable, { + props: { + sortable: true, + headers, + rows, + }, + }); + + // Get all header cells + const headerCells = container.querySelectorAll("th"); + expect(headerCells.length).toBe(4); + + // Test sorting by name (ascending) + const nameHeader = screen.getByText("Name"); + await user.click(nameHeader); + + // Verify rows are sorted by name ascending + const rowsAfterNameSort = container.querySelectorAll("tbody tr"); + const firstRowName = rowsAfterNameSort[0].querySelector("td"); + expect(firstRowName).toHaveTextContent("Load Balancer 1"); + + // Test sorting by name (descending) + await user.click(nameHeader); + const rowsAfterNameDescSort = container.querySelectorAll("tbody tr"); + const firstRowNameDesc = rowsAfterNameDescSort[0].querySelector("td"); + expect(firstRowNameDesc).toHaveTextContent("Load Balancer 3"); + + // Test sorting by port (ascending) + const portHeader = screen.getByText("Port"); + await user.click(portHeader); + + // Verify rows are sorted by port ascending + const rowsAfterPortSort = container.querySelectorAll("tbody tr"); + const firstRowPort = rowsAfterPortSort[0].querySelectorAll("td")[2]; + expect(firstRowPort).toHaveTextContent("80"); + + // Test sorting by port (descending) + await user.click(portHeader); + const rowsAfterPortDescSort = container.querySelectorAll("tbody tr"); + const firstRowPortDesc = rowsAfterPortDescSort[0].querySelectorAll("td")[2]; + expect(firstRowPortDesc).toHaveTextContent("3000"); + }); + + it("handles sorting with custom display and sort methods", async () => { + const customHeaders = [ + { key: "name", value: "Name" }, + { key: "cost", value: "Cost", display: (cost: number) => cost + " €" }, + { + key: "expireDate", + value: "Expire date", + display: (date: string) => new Date(date).toLocaleString(), + sort: (a: string, b: string) => + new Date(a).getTime() - new Date(b).getTime(), + }, + ]; + + const customRows = [ + { + id: "a", + name: "Load Balancer 1", + cost: 100, + expireDate: "2024-01-01", + }, + { + id: "b", + name: "Load Balancer 2", + cost: 200, + expireDate: "2023-12-31", + }, + { + id: "c", + name: "Load Balancer 3", + cost: 150, + expireDate: "2024-02-01", + }, + ]; + + const { container } = render(DataTable, { + props: { + sortable: true, + headers: customHeaders, + rows: customRows, + }, + }); + + // Verify custom display formatting + const costCells = container.querySelectorAll("td:nth-child(2)"); + expect(costCells[0]).toHaveTextContent("100 €"); + expect(costCells[1]).toHaveTextContent("200 €"); + + // Test sorting by expireDate + const dateHeader = screen.getByText("Expire date"); + await user.click(dateHeader); + + // Verify rows are sorted by date ascending + const rowsAfterDateSort = container.querySelectorAll("tbody tr"); + expect(rowsAfterDateSort[0].querySelector("td")).toHaveTextContent( + "Load Balancer 2", + ); + expect(rowsAfterDateSort[2].querySelector("td")).toHaveTextContent( + "Load Balancer 3", + ); + }); + + it("handles sorting with nested object values", async () => { + const nestedHeaders = [ + { key: "name", value: "Name" }, + { key: "network.protocol", value: "Protocol" }, + { key: "network.port", value: "Port" }, + ] as const; + + const nestedRows = [ + { + id: "a", + name: "Load Balancer 1", + network: { protocol: "HTTP", port: 3000 }, + }, + { + id: "b", + name: "Load Balancer 2", + network: { protocol: "HTTPS", port: 443 }, + }, + { + id: "c", + name: "Load Balancer 3", + network: { protocol: "HTTP", port: 80 }, + }, + ]; + + const { container } = render(DataTable, { + props: { + sortable: true, + headers: nestedHeaders, + rows: nestedRows, + }, + }); + + // Test sorting by nested port value + const portHeader = screen.getByText("Port"); + await user.click(portHeader); + + // Verify rows are sorted by port ascending + const rowsAfterPortSort = container.querySelectorAll("tbody tr"); + expect(rowsAfterPortSort[0].querySelectorAll("td")[2]).toHaveTextContent( + "80", + ); + expect(rowsAfterPortSort[1].querySelectorAll("td")[2]).toHaveTextContent( + "443", + ); + expect(rowsAfterPortSort[2].querySelectorAll("td")[2]).toHaveTextContent( + "3000", + ); + }); + + it("handles disabled sorting on specific columns", async () => { + const customHeaders = [ + { key: "name", value: "Name" }, + { key: "protocol", value: "Protocol", sort: false as const }, + { key: "port", value: "Port" }, + ]; + + const { container } = render(DataTable, { + props: { + sortable: true, + headers: customHeaders, + rows, + }, + }); + + const protocolHeader = screen.getByText("Protocol"); + await user.click(protocolHeader); + + // Verify no sorting occurred + const firstRow = container.querySelector("tbody tr:first-child"); + assert(firstRow); + expect(firstRow.querySelector("td:first-child")).toHaveTextContent( + "Load Balancer 3", + ); + }); + + it("handles table with numeric sorting", async () => { + const numericRows = [ + { id: "a", value: 10 }, + { id: "b", value: 2 }, + { id: "c", value: 20 }, + ]; + + const numericHeaders = [{ key: "value", value: "Value" }]; + + const { container } = render(DataTable, { + props: { + sortable: true, + headers: numericHeaders, + rows: numericRows, + }, + }); + + const valueHeader = screen.getByText("Value"); + await user.click(valueHeader); + + const sortedCells = container.querySelectorAll("td"); + expect(sortedCells[0]).toHaveTextContent("2"); + expect(sortedCells[1]).toHaveTextContent("10"); + expect(sortedCells[2]).toHaveTextContent("20"); + }); + + // Selection tests + it("handles selectable rows", async () => { + const { container } = render(DataTable, { + props: { + selectable: true, + headers, + rows, + }, + }); + + // Verify checkboxes are present in each row + const checkboxes = container.querySelectorAll("input[type='checkbox']"); + expect(checkboxes.length).toBe(3); + + // Select first row + await user.click(checkboxes[0]); + + // Verify row is selected + const selectedRow = container.querySelector(".bx--data-table--selected"); + expect(selectedRow).toBeInTheDocument(); + }); + + it("handles batch selection", async () => { + const { container } = render(DataTable, { + props: { + batchSelection: true, + headers, + rows, + }, + }); + + // Verify batch selection checkbox is present + const batchCheckbox = container.querySelector( + ".bx--table-column-checkbox input[type='checkbox']", + ); + assert(batchCheckbox); + + // Click batch selection checkbox + await user.click(batchCheckbox); + + // Verify all rows are selected + const selectedRows = container.querySelectorAll( + ".bx--data-table--selected", + ); + expect(selectedRows.length).toBe(3); + + // Click batch selection checkbox again + await user.click(batchCheckbox); + + // Verify no rows are selected + const unselectedRows = container.querySelectorAll( + ".bx--data-table--selected", + ); + expect(unselectedRows.length).toBe(0); + }); + + it("handles radio selection", async () => { + const { container } = render(DataTable, { + props: { + selectable: true, + radio: true, + headers, + rows, + }, + }); + + const radioButtons = container.querySelectorAll("input[type='radio']"); + expect(radioButtons.length).toBe(3); + + await user.click(radioButtons[0]); + const selectedRow = container.querySelector(".bx--data-table--selected"); + expect(selectedRow).toBeInTheDocument(); + }); + + it("handles non-selectable and non-expandable rows", async () => { + const { container } = render(DataTable, { + props: { + selectable: true, + expandable: true, + nonSelectableRowIds: ["b"], + nonExpandableRowIds: ["c"], + headers, + rows, + }, + }); + + // Verify non-selectable row doesn't have a checkbox + const nonSelectableRow = container.querySelector("tr[data-row='b']"); + const nonSelectableCheckbox = nonSelectableRow?.querySelector( + "input[type='checkbox']", + ); + expect(nonSelectableCheckbox).not.toBeInTheDocument(); + + // Verify non-expandable row doesn't have an expand button + const nonExpandableRow = container.querySelector("tr[data-row='c']"); + const nonExpandableButton = nonExpandableRow?.querySelector( + ".bx--table-expand__button", + ); + expect(nonExpandableButton).not.toBeInTheDocument(); + }); + + // Expandable rows tests + it("handles expandable rows", async () => { + const { container } = render(DataTable, { + props: { + expandable: true, + headers, + rows, + }, + }); + + // Verify expand button is present in each row + const expandButtons = container.querySelectorAll( + ".bx--table-expand__button", + ); + expect(expandButtons.length).toBe(3); + + // Click expand button on first row + await user.click(expandButtons[0]); + + const expandedRow = container.querySelector(".bx--expandable-row"); + expect(expandedRow).toBeInTheDocument(); + expect(expandedRow).toHaveClass("bx--expandable-row bx--parent-row"); + }); + + it("handles batch expansion", async () => { + const { container } = render(DataTable, { + props: { + expandable: true, + batchExpansion: true, + headers, + rows, + }, + }); + + expect( + container.querySelectorAll(".bx--child-row-inner-container"), + ).toHaveLength(0); + + const expandAllButton = screen.getByLabelText("Expand all rows"); + + await user.click(expandAllButton); + expect( + container.querySelectorAll(".bx--child-row-inner-container"), + ).toHaveLength(3); + + await user.click(expandAllButton); + expect( + container.querySelectorAll(".bx--child-row-inner-container"), + ).toHaveLength(0); + }); + + // Styling and layout tests + it("applies zebra stripe styling", async () => { + const { container } = render(DataTable, { + props: { + zebra: true, + headers, + rows, + }, + }); + + // Verify zebra stripe classes are applied + const table = container.querySelector("table"); + expect(table).toHaveClass("bx--data-table--zebra"); + }); + + it("applies different size variants", async () => { + type Size = "compact" | "short" | "medium" | "tall"; + const sizeMappings: Record = { + compact: "bx--data-table--compact", + short: "bx--data-table--short", + medium: "bx--data-table--md", + tall: "bx--data-table--tall", + }; + + for (const [size, expectedClass] of Object.entries(sizeMappings)) { + const { container } = render(DataTable, { + props: { + size: size as Size, + headers, + rows, + }, + }); + + // Verify size class is applied + const table = container.querySelector("table"); + expect(table).toHaveClass(expectedClass); + } + }); + + it("applies custom column widths", async () => { + const customHeaders = [ + { key: "name", value: "Name", width: "200px" }, + { key: "protocol", value: "Protocol", minWidth: "100px" }, + { key: "port", value: "Port" }, + { key: "rule", value: "Rule" }, + ] as const; + + const { container } = render(DataTable, { + props: { + headers: customHeaders, + rows, + }, + }); + + // Verify table has fixed layout + const table = container.querySelector("table"); + expect(table).toHaveStyle({ "table-layout": "fixed" }); + + // Verify name column has correct width + const nameHeader = container.querySelector("th:first-child"); + expect(nameHeader).toHaveStyle({ width: "200px" }); + + // Verify protocol column has correct min-width + const protocolHeader = container.querySelector("th:nth-child(2)"); + expect(protocolHeader).toHaveStyle({ "min-width": "100px" }); + }); + + it("applies sticky header", async () => { + const { container } = render(DataTable, { + props: { + stickyHeader: true, + headers, + rows, + }, + }); + + const table = container.querySelector("table"); + expect(table).toHaveClass("bx--data-table--sticky-header"); + }); + + it("handles static width", () => { + const { container } = render(DataTable, { + props: { + useStaticWidth: true, + headers, + rows, + }, + }); + + const tableContainer = container.querySelector(".bx--data-table-container"); + expect(tableContainer).toHaveClass("bx--data-table-container--static"); + + const table = container.querySelector("table"); + expect(table).toHaveClass("bx--data-table--static"); + }); + + // Custom cell display tests + it("handles custom cell display", () => { + const customHeaders = [ + { key: "name", value: "Name" }, + { + key: "port", + value: "Port", + display: (value: number) => `Port ${value}`, + }, + ]; + + const { container } = render(DataTable, { + props: { + headers: customHeaders, + rows, + }, + }); + + const portCells = container.querySelectorAll("td:nth-child(2)"); + expect(portCells[0]).toHaveTextContent("Port 3000"); + expect(portCells[1]).toHaveTextContent("Port 443"); + expect(portCells[2]).toHaveTextContent("Port 80"); + }); + + it("handles empty columns for custom content", () => { + const emptyColumnHeaders = [ + { key: "name", value: "Name" }, + { key: "protocol", value: "Protocol" }, + { key: "port", value: "Port" }, + { key: "rule", value: "Rule" }, + { key: "actions", value: "", empty: true }, + ] as const; + + const { container } = render(DataTable, { + props: { + headers: emptyColumnHeaders, + rows, + }, + }); + + // Verify empty column header exists and has no text content + const headerCells = screen.getAllByRole("columnheader"); + expect(headerCells.length).toBe(5); // 4 regular headers + 1 empty column + expect(headerCells[4]).toHaveTextContent(""); + + // Verify empty column cells exist in each row + const tableRows = container.querySelectorAll("tbody tr"); + tableRows.forEach((row) => { + const cells = row.querySelectorAll("td"); + expect(cells.length).toBe(5); + }); + }); + + // Pagination tests + it("handles pagination", async () => { + const paginatedRows = Array.from({ length: 15 }).map((_, i) => ({ + id: `row-${i}`, + name: `Load Balancer ${i + 1}`, + protocol: "HTTP", + port: 3000 + i, + rule: i % 2 ? "Round robin" : "DNS delegation", + })); + + const { container, component } = render(DataTable, { + props: { + headers, + rows: paginatedRows, + pageSize: 5, + page: 1, + }, + }); + + // Verify only 5 rows are displayed on first page + const firstPageRows = container.querySelectorAll("tbody tr"); + expect(firstPageRows.length).toBe(5); + expect(firstPageRows[0].querySelector("td")).toHaveTextContent( + "Load Balancer 1", + ); + expect(firstPageRows[4].querySelector("td")).toHaveTextContent( + "Load Balancer 5", + ); + + // Update page to 2 + component.$set({ page: 2 }); + await tick(); + + // Verify 5 rows are displayed on second page + const secondPageRows = container.querySelectorAll("tbody tr"); + expect(secondPageRows.length).toBe(5); + expect(secondPageRows[0].querySelector("td")).toHaveTextContent( + "Load Balancer 6", + ); + expect(secondPageRows[4].querySelector("td")).toHaveTextContent( + "Load Balancer 10", + ); + + // Update page to 3 + component.$set({ page: 3 }); + await tick(); + + // Verify remaining rows are displayed on third page + const thirdPageRows = container.querySelectorAll("tbody tr"); + expect(thirdPageRows.length).toBe(5); + expect(thirdPageRows[0].querySelector("td")).toHaveTextContent( + "Load Balancer 11", + ); + expect(thirdPageRows[4].querySelector("td")).toHaveTextContent( + "Load Balancer 15", + ); + }); + + // Event handling tests + it("emits proper events", async () => { + const consoleLog = vi.spyOn(console, "log"); + + const { container } = render(DataTable, { + props: { + headers, + rows, + sortable: true, + }, + }); + + // Click header + const nameHeader = screen.getByText("Name"); + await user.click(nameHeader); + + // Click row + const firstRow = container.querySelector("tbody tr"); + assert(firstRow); + await user.click(firstRow); + + // Click cell + const firstCell = container.querySelector("td"); + assert(firstCell); + await user.click(firstCell); + + // Verify events were logged + expect(consoleLog).toHaveBeenCalledWith("click:header", expect.any(Object)); + expect(consoleLog).toHaveBeenCalledWith("click:row", expect.any(Object)); + expect(consoleLog).toHaveBeenCalledWith("click:cell", expect.any(Object)); + }); + + it("handles row hover events", async () => { + const consoleLog = vi.spyOn(console, "log"); + const { container } = render(DataTable, { + props: { + headers, + rows, + }, + }); + + const firstRow = container.querySelector("tbody tr"); + assert(firstRow); + await user.hover(firstRow); + await user.unhover(firstRow); + + expect(consoleLog).toHaveBeenCalledWith( + "mouseenter:row", + expect.any(Object), + ); + expect(consoleLog).toHaveBeenCalledWith( + "mouseleave:row", + expect.any(Object), + ); + }); +}); diff --git a/tests/DataTableBatchSelectionToolbar.test.svelte b/tests/DataTable/DataTableBatchSelectionToolbar.test.svelte similarity index 57% rename from tests/DataTableBatchSelectionToolbar.test.svelte rename to tests/DataTable/DataTableBatchSelectionToolbar.test.svelte index b5c5e70e..fe7df1e8 100644 --- a/tests/DataTableBatchSelectionToolbar.test.svelte +++ b/tests/DataTable/DataTableBatchSelectionToolbar.test.svelte @@ -3,13 +3,9 @@ DataTable, Toolbar, ToolbarContent, - ToolbarSearch, - ToolbarMenu, - ToolbarMenuItem, ToolbarBatchActions, Button, } from "carbon-components-svelte"; - import Save from "carbon-icons-svelte/lib/Save.svelte"; const headers = [ { key: "name", value: "Name" }, @@ -26,25 +22,38 @@ { id: "f", name: "Load Balancer 5", port: 80, rule: "DNS delegation" }, ]; - let selectedRowIds = [rows[0].id, rows[1].id]; - - $: console.log("selectedRowIds", selectedRowIds); + export let selectedRowIds: string[] = []; + export let active: boolean | undefined = undefined; + export let controlled = false; - + - - + { + if (!controlled) { + selectedRowIds = []; + } + }} + > + + - - - Restart all - - API documentation - - Stop all - diff --git a/tests/DataTable/DataTableBatchSelectionToolbar.test.ts b/tests/DataTable/DataTableBatchSelectionToolbar.test.ts new file mode 100644 index 00000000..90d9120e --- /dev/null +++ b/tests/DataTable/DataTableBatchSelectionToolbar.test.ts @@ -0,0 +1,168 @@ +import { render, screen } from "@testing-library/svelte"; +import { tick } from "svelte"; +import { user } from "../setup-tests"; +import DataTableBatchSelectionToolbar from "./DataTableBatchSelectionToolbar.test.svelte"; + +describe("DataTableBatchSelectionToolbar", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders toolbar when rows are selected", async () => { + const { container } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + }, + }); + + // Verify toolbar is visible + const toolbar = container.querySelector(".bx--batch-actions"); + expect(toolbar).toBeInTheDocument(); + expect(toolbar).toHaveClass("bx--batch-actions--active"); + + // Verify selected count is displayed + expect(screen.getByText("2 items selected")).toBeInTheDocument(); + }); + + it("hides toolbar when no rows are selected", () => { + const { container } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: [], + }, + }); + + // Verify toolbar is not visible + const toolbar = container.querySelector(".bx--batch-actions"); + expect(toolbar).not.toHaveClass("bx--batch-actions--active"); + }); + + it("handles cancel action", async () => { + const { component } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + }, + }); + + // Click cancel button + const cancelButton = screen.getByText("Cancel"); + await user.click(cancelButton); + + // Verify selected rows are cleared + expect(component.$$.ctx[component.$$.props["selectedRowIds"]]).toEqual([]); + }); + + it("handles custom batch actions", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + }, + }); + + // Click custom action button + const actionButton = screen.getByText("Delete"); + await user.click(actionButton); + + // Verify action was triggered + expect(consoleLog).toHaveBeenCalledWith("delete", ["a", "b"]); + }); + + it("handles controlled active state", async () => { + const { container, component } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + active: false, + }, + }); + + // Verify toolbar is not active despite selected rows + const toolbar = container.querySelector(".bx--batch-actions"); + expect(toolbar).not.toHaveClass("bx--batch-actions--active"); + + // Update active state + component.$set({ active: true }); + await tick(); + + // Verify toolbar is now active + expect(toolbar).toHaveClass("bx--batch-actions--active"); + }); + + it("prevents default cancel behavior when controlled", async () => { + const { component } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + active: true, + controlled: true, + }, + }); + + // Click cancel button + const cancelButton = screen.getByText("Cancel"); + await user.click(cancelButton); + + // Verify selected rows are not cleared + expect(component.$$.ctx[component.$$.props["selectedRowIds"]]).toEqual([ + "a", + "b", + ]); + }); + + it("handles multiple batch actions", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + }, + }); + + // Click first action button + const deleteButton = screen.getByText("Delete"); + await user.click(deleteButton); + expect(consoleLog).toHaveBeenCalledWith("delete", ["a", "b"]); + + // Click second action button + const restartButton = screen.getByText("Restart"); + await user.click(restartButton); + expect(consoleLog).toHaveBeenCalledWith("restart", ["a", "b"]); + }); + + it("updates selected count when selection changes", async () => { + const { component } = render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a"], + }, + }); + + // Verify initial count + expect(screen.getByText("1 item selected")).toBeInTheDocument(); + + // Update selection + component.$set({ selectedRowIds: ["a", "b", "c"] }); + await tick(); + + // Verify updated count + expect(screen.getByText("3 items selected")).toBeInTheDocument(); + }); + + it("handles keyboard navigation", async () => { + render(DataTableBatchSelectionToolbar, { + props: { + selectedRowIds: ["a", "b"], + }, + }); + + // Focus cancel button + const cancelButton = screen.getByText("Cancel"); + cancelButton.focus(); + + // Press tab to move to next action + await user.keyboard("{Tab}"); + expect(screen.getByText("Create balancer")).toHaveFocus(); + + // Press tab again to move to next action + await user.keyboard("{Tab}"); + expect( + screen.getByRole("checkbox", { name: "Select all rows" }), + ).toHaveFocus(); + }); +}); diff --git a/tests/DataTable/DuplicateDataTables.test.ts b/tests/DataTable/DuplicateDataTables.test.ts new file mode 100644 index 00000000..af921813 --- /dev/null +++ b/tests/DataTable/DuplicateDataTables.test.ts @@ -0,0 +1,73 @@ +import { fireEvent, render } from "@testing-library/svelte"; +import { expect, test } from "vitest"; +import DuplicateDataTables from "./DuplicateDataTables.test.svelte"; + +describe("DuplicateDataTables", () => { + test("should allow independent radio selection in duplicate tables", async () => { + const { container } = render(DuplicateDataTables); + + // Get all radio tables + const radioTables = container.querySelectorAll( + 'input[type="radio"][name="radio-select"]', + ); + expect(radioTables).toHaveLength(4); // 2 rows * 2 tables + + // Select first row in first table + await fireEvent.click(radioTables[0]); + expect(radioTables[0]).toBeChecked(); + expect(radioTables[1]).not.toBeChecked(); + expect(radioTables[2]).not.toBeChecked(); + expect(radioTables[3]).not.toBeChecked(); + + // Select second row in second table + await fireEvent.click(radioTables[3]); + expect(radioTables[0]).not.toBeChecked(); + expect(radioTables[1]).not.toBeChecked(); + expect(radioTables[2]).not.toBeChecked(); + expect(radioTables[3]).toBeChecked(); + }); + + test("should allow independent checkbox selection in duplicate tables", async () => { + const { container } = render(DuplicateDataTables); + + // Get all checkbox tables + const checkboxTables = container.querySelectorAll( + 'input[type="checkbox"][name="checkbox-select"]', + ); + expect(checkboxTables).toHaveLength(4); // 2 rows * 2 tables + + // Select first row in first table + await fireEvent.click(checkboxTables[0]); + expect(checkboxTables[0]).toBeChecked(); + expect(checkboxTables[1]).not.toBeChecked(); + expect(checkboxTables[2]).not.toBeChecked(); + expect(checkboxTables[3]).not.toBeChecked(); + + // Select second row in second table + await fireEvent.click(checkboxTables[3]); + expect(checkboxTables[0]).toBeChecked(); + expect(checkboxTables[1]).not.toBeChecked(); + expect(checkboxTables[2]).not.toBeChecked(); + expect(checkboxTables[3]).toBeChecked(); + }); + + test("should maintain separate select-all checkboxes for each table", async () => { + const { container } = render(DuplicateDataTables); + + // Get all select-all checkboxes + const selectAllCheckboxes = container.querySelectorAll( + 'input[type="checkbox"][value="all"]', + ); + expect(selectAllCheckboxes).toHaveLength(2); // One per table + + // Select all in first table + await fireEvent.click(selectAllCheckboxes[0]); + expect(selectAllCheckboxes[0]).toBeChecked(); + expect(selectAllCheckboxes[1]).not.toBeChecked(); + + // Select all in second table + await fireEvent.click(selectAllCheckboxes[1]); + expect(selectAllCheckboxes[0]).toBeChecked(); + expect(selectAllCheckboxes[1]).toBeChecked(); + }); +}); diff --git a/tests/DataTableAppendColumns.test.svelte b/tests/DataTableAppendColumns.test.svelte deleted file mode 100644 index 98c5ecd8..00000000 --- a/tests/DataTableAppendColumns.test.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - - {#if cell.key === "overflow"} - - - - - - {:else}{cell.value}{/if} - - diff --git a/tests/DataTableBatchSelection.test.svelte b/tests/DataTableBatchSelection.test.svelte deleted file mode 100644 index 30f40a0c..00000000 --- a/tests/DataTableBatchSelection.test.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/tests/DataTableNestedHeaders.test.svelte b/tests/DataTableNestedHeaders.test.svelte deleted file mode 100644 index 88d53467..00000000 --- a/tests/DataTableNestedHeaders.test.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - {cell.key === "rule.name"} - {#if cell.key === "name"} - {row.name} {row.id} - {:else} - {cell.value} - {/if} - -