mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-14 18:01:06 +00:00
test(data-table): add unit tests (#2144)
This commit is contained in:
parent
c57c0efb73
commit
da2a308d31
9 changed files with 1150 additions and 452 deletions
|
@ -1,342 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
DataTable,
|
|
||||||
DataTableSkeleton,
|
|
||||||
Toolbar,
|
|
||||||
ToolbarContent,
|
|
||||||
ToolbarSearch,
|
|
||||||
ToolbarMenu,
|
|
||||||
ToolbarMenuItem,
|
|
||||||
Button,
|
|
||||||
Link,
|
|
||||||
} from "carbon-components-svelte";
|
|
||||||
import Launch from "carbon-icons-svelte/lib/Launch.svelte";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{ key: "name", value: "Name" },
|
|
||||||
{ key: "protocol", value: "Protocol", width: "400px", minWidth: "40%" },
|
|
||||||
{ key: "port", value: "Port" },
|
|
||||||
{ key: "rule", value: "Rule", sort: false },
|
|
||||||
] 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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "d",
|
|
||||||
name: "Load Balancer 6",
|
|
||||||
protocol: "HTTP",
|
|
||||||
port: 3000,
|
|
||||||
rule: "Round robin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "e",
|
|
||||||
name: "Load Balancer 4",
|
|
||||||
protocol: "HTTP",
|
|
||||||
port: 443,
|
|
||||||
rule: "Round robin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "f",
|
|
||||||
name: "Load Balancer 5",
|
|
||||||
protocol: "HTTP",
|
|
||||||
port: 80,
|
|
||||||
rule: "DNS delegation",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function sort(a: any, b: any) {
|
|
||||||
if (new Date(a) > new Date(b)) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let filteredRowIds: ComponentProps<ToolbarSearch>["filteredRowIds"] = [];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
{headers}
|
|
||||||
{rows}
|
|
||||||
style=""
|
|
||||||
sortKey="name"
|
|
||||||
sortDirection="descending"
|
|
||||||
class="class"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable {headers} {rows}>
|
|
||||||
<span slot="cell-header" let:header>
|
|
||||||
{#if header.key === "port"}
|
|
||||||
{header.value}
|
|
||||||
(network)
|
|
||||||
{:else}{header.value}{/if}
|
|
||||||
</span>
|
|
||||||
<span slot="cell" let:cell>
|
|
||||||
{#if cell.key === "rule" && cell.value === "Round robin"}
|
|
||||||
<Link
|
|
||||||
inline
|
|
||||||
href="https://en.wikipedia.org/wiki/Round-robin_DNS"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{cell.value}
|
|
||||||
<Launch />
|
|
||||||
</Link>
|
|
||||||
{:else}{cell.value}{/if}
|
|
||||||
</span>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
title="Load balancers"
|
|
||||||
description="Your organization's active load balancers."
|
|
||||||
{headers}
|
|
||||||
{rows}
|
|
||||||
useStaticWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
title="Load balancers"
|
|
||||||
description="Your organization's active load balancers."
|
|
||||||
{headers}
|
|
||||||
{rows}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
|
||||||
<ToolbarContent>
|
|
||||||
<ToolbarSearch
|
|
||||||
bind:filteredRowIds
|
|
||||||
shouldFilterRows={(row, value) => {
|
|
||||||
return row.name.includes(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ToolbarMenu>
|
|
||||||
<ToolbarMenuItem primaryFocus>Restart all</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem href="https://cloud.ibm.com/docs/loadbalancer-service">
|
|
||||||
API documentation
|
|
||||||
</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem danger>Stop all</ToolbarMenuItem>
|
|
||||||
</ToolbarMenu>
|
|
||||||
<Button>Create balancer</Button>
|
|
||||||
</ToolbarContent>
|
|
||||||
</Toolbar>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
size="short"
|
|
||||||
title="Load balancers"
|
|
||||||
description="Your organization's active load balancers."
|
|
||||||
{headers}
|
|
||||||
{rows}
|
|
||||||
>
|
|
||||||
<Toolbar size="sm">
|
|
||||||
<ToolbarContent>
|
|
||||||
<ToolbarSearch />
|
|
||||||
<ToolbarMenu>
|
|
||||||
<ToolbarMenuItem primaryFocus>Restart all</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem href="https://cloud.ibm.com/docs/loadbalancer-service">
|
|
||||||
API documentation
|
|
||||||
</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem danger>Stop all</ToolbarMenuItem>
|
|
||||||
</ToolbarMenu>
|
|
||||||
<Button>Create balancer</Button>
|
|
||||||
</ToolbarContent>
|
|
||||||
</Toolbar>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<DataTable zebra {headers} {rows} />
|
|
||||||
|
|
||||||
<DataTable size="tall" {headers} {rows} />
|
|
||||||
|
|
||||||
<DataTable size="short" {headers} {rows} />
|
|
||||||
|
|
||||||
<DataTable size="compact" {headers} {rows} />
|
|
||||||
|
|
||||||
<DataTable sortable {headers} {rows} />
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
sortable
|
|
||||||
title="Load balancers"
|
|
||||||
description="Your organization's active load balancers."
|
|
||||||
headers={[
|
|
||||||
{ key: "name", value: "Name" },
|
|
||||||
{ key: "protocol", value: "Protocol" },
|
|
||||||
{ key: "port", value: "Port" },
|
|
||||||
{ key: "cost", value: "Cost", display: (cost) => 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",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable expandable nonExpandableRowIds={["a", "b"]} {headers} {rows}>
|
|
||||||
<div slot="expanded-row" let:row>
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(row, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<DataTable batchExpansion {headers} {rows}>
|
|
||||||
<div slot="expanded-row" let:row>
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(row, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<DataTableSkeleton />
|
|
||||||
|
|
||||||
<DataTableSkeleton headers={["Name", "Protocol", "Port", "Rule"]} rows={10} />
|
|
||||||
|
|
||||||
<DataTableSkeleton
|
|
||||||
headers={[
|
|
||||||
{ value: "Name" },
|
|
||||||
{ value: "Protocol" },
|
|
||||||
{ value: "Port" },
|
|
||||||
{ value: "Rule" },
|
|
||||||
{ empty: true },
|
|
||||||
]}
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTableSkeleton {headers} rows={10} />
|
|
||||||
|
|
||||||
<DataTableSkeleton showHeader={false} showToolbar={false} />
|
|
||||||
|
|
||||||
<DataTableSkeleton showHeader={false} showToolbar={false} size="tall" />
|
|
||||||
|
|
||||||
<DataTableSkeleton showHeader={false} showToolbar={false} size="short" />
|
|
||||||
|
|
||||||
<DataTableSkeleton showHeader={false} showToolbar={false} size="compact" />
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
name: "Load Balancer 3",
|
|
||||||
protocol: "HTTP",
|
|
||||||
port: 3000,
|
|
||||||
rule: "Round robin",
|
|
||||||
id: "-",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
headers={[
|
|
||||||
{
|
|
||||||
key: "name",
|
|
||||||
value: "Name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "protocol",
|
|
||||||
value: "Protocol",
|
|
||||||
display: (value) => {
|
|
||||||
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;
|
|
||||||
}}
|
|
||||||
/>
|
|
107
tests/DataTable/DataTable.test.svelte
Normal file
107
tests/DataTable/DataTable.test.svelte
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DataTable } from "carbon-components-svelte";
|
||||||
|
|
||||||
|
type BaseRow = {
|
||||||
|
id: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataTableHeader = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
width?: string;
|
||||||
|
minWidth?: string;
|
||||||
|
display?: (value: any) => string;
|
||||||
|
sort?: false | ((a: any, b: any) => number);
|
||||||
|
};
|
||||||
|
|
||||||
|
export let headers: readonly DataTableHeader[] = [
|
||||||
|
{ key: "name", value: "Name" },
|
||||||
|
{ key: "protocol", value: "Protocol" },
|
||||||
|
{ key: "port", value: "Port" },
|
||||||
|
{ key: "rule", value: "Rule" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export let rows: readonly BaseRow[] = [
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export let title = "";
|
||||||
|
export let description = "";
|
||||||
|
export let size: "compact" | "short" | "medium" | "tall" | undefined =
|
||||||
|
undefined;
|
||||||
|
export let zebra = false;
|
||||||
|
export let sortable = false;
|
||||||
|
export let stickyHeader = false;
|
||||||
|
export let useStaticWidth = false;
|
||||||
|
export let expandable = false;
|
||||||
|
export let batchExpansion = false;
|
||||||
|
export let selectable = false;
|
||||||
|
export let radio = false;
|
||||||
|
export let batchSelection = false;
|
||||||
|
export let nonSelectableRowIds: string[] = [];
|
||||||
|
export let nonExpandableRowIds: string[] = [];
|
||||||
|
export let pageSize = 0;
|
||||||
|
export let page = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
{headers}
|
||||||
|
{rows}
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
{size}
|
||||||
|
{zebra}
|
||||||
|
{sortable}
|
||||||
|
{stickyHeader}
|
||||||
|
{useStaticWidth}
|
||||||
|
{expandable}
|
||||||
|
{batchExpansion}
|
||||||
|
{selectable}
|
||||||
|
{radio}
|
||||||
|
{batchSelection}
|
||||||
|
{nonSelectableRowIds}
|
||||||
|
{nonExpandableRowIds}
|
||||||
|
{pageSize}
|
||||||
|
{page}
|
||||||
|
on:click={(e) => {
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DataTable>
|
775
tests/DataTable/DataTable.test.ts
Normal file
775
tests/DataTable/DataTable.test.ts
Normal file
|
@ -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<Size, string> = {
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,13 +3,9 @@
|
||||||
DataTable,
|
DataTable,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarContent,
|
ToolbarContent,
|
||||||
ToolbarSearch,
|
|
||||||
ToolbarMenu,
|
|
||||||
ToolbarMenuItem,
|
|
||||||
ToolbarBatchActions,
|
ToolbarBatchActions,
|
||||||
Button,
|
Button,
|
||||||
} from "carbon-components-svelte";
|
} from "carbon-components-svelte";
|
||||||
import Save from "carbon-icons-svelte/lib/Save.svelte";
|
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{ key: "name", value: "Name" },
|
{ key: "name", value: "Name" },
|
||||||
|
@ -26,25 +22,38 @@
|
||||||
{ id: "f", name: "Load Balancer 5", port: 80, rule: "DNS delegation" },
|
{ id: "f", name: "Load Balancer 5", port: 80, rule: "DNS delegation" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let selectedRowIds = [rows[0].id, rows[1].id];
|
export let selectedRowIds: string[] = [];
|
||||||
|
export let active: boolean | undefined = undefined;
|
||||||
$: console.log("selectedRowIds", selectedRowIds);
|
export let controlled = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DataTable batchSelection bind:selectedRowIds {headers} {rows}>
|
<DataTable batchSelection {headers} {rows} {selectedRowIds}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarBatchActions>
|
<ToolbarBatchActions
|
||||||
<Button icon={Save}>Save</Button>
|
{active}
|
||||||
|
on:cancel={() => {
|
||||||
|
if (!controlled) {
|
||||||
|
selectedRowIds = [];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
kind="danger"
|
||||||
|
on:click={() => {
|
||||||
|
console.log("delete", selectedRowIds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
console.log("restart", selectedRowIds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
</ToolbarBatchActions>
|
</ToolbarBatchActions>
|
||||||
<ToolbarContent>
|
<ToolbarContent>
|
||||||
<ToolbarSearch />
|
|
||||||
<ToolbarMenu>
|
|
||||||
<ToolbarMenuItem primaryFocus>Restart all</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem href="https://cloud.ibm.com/docs/loadbalancer-service">
|
|
||||||
API documentation
|
|
||||||
</ToolbarMenuItem>
|
|
||||||
<ToolbarMenuItem hasDivider danger>Stop all</ToolbarMenuItem>
|
|
||||||
</ToolbarMenu>
|
|
||||||
<Button>Create balancer</Button>
|
<Button>Create balancer</Button>
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
168
tests/DataTable/DataTableBatchSelectionToolbar.test.ts
Normal file
168
tests/DataTable/DataTableBatchSelectionToolbar.test.ts
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
73
tests/DataTable/DuplicateDataTables.test.ts
Normal file
73
tests/DataTable/DuplicateDataTables.test.ts
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,38 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
DataTable,
|
|
||||||
OverflowMenu,
|
|
||||||
OverflowMenuItem,
|
|
||||||
} from "carbon-components-svelte";
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{ key: "name", value: "Name" },
|
|
||||||
{ key: "port", value: "Port" },
|
|
||||||
{ key: "rule", value: "Rule" },
|
|
||||||
{ key: "overflow", empty: true },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{ id: "a", name: "Load Balancer 3", port: 3000, rule: "Round robin" },
|
|
||||||
{ id: "b", name: "Load Balancer 1", port: 443, rule: "Round robin" },
|
|
||||||
{ id: "c", name: "Load Balancer 2", port: 80, rule: "DNS delegation" },
|
|
||||||
{ id: "d", name: "Load Balancer 6", port: 3000, rule: "Round robin" },
|
|
||||||
{ id: "e", name: "Load Balancer 4", port: 443, rule: "Round robin" },
|
|
||||||
{ id: "f", name: "Load Balancer 5", port: 80, rule: "DNS delegation" },
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DataTable sortable {headers} {rows}>
|
|
||||||
<span slot="cell" let:cell>
|
|
||||||
{#if cell.key === "overflow"}
|
|
||||||
<OverflowMenu flipped>
|
|
||||||
<OverflowMenuItem text="Restart" />
|
|
||||||
<OverflowMenuItem
|
|
||||||
href="https://cloud.ibm.com/docs/loadbalancer-service"
|
|
||||||
text="API documentation"
|
|
||||||
/>
|
|
||||||
<OverflowMenuItem danger text="Stop" />
|
|
||||||
</OverflowMenu>
|
|
||||||
{:else}{cell.value}{/if}
|
|
||||||
</span>
|
|
||||||
</DataTable>
|
|
|
@ -1,24 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DataTable } from "carbon-components-svelte";
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{ key: "name", value: "Name" },
|
|
||||||
{ key: "port", value: "Port" },
|
|
||||||
{ key: "rule", value: "Rule" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{ id: "a", name: "Load Balancer 3", port: 3000, rule: "Round robin" },
|
|
||||||
{ id: "b", name: "Load Balancer 1", port: 443, rule: "Round robin" },
|
|
||||||
{ id: "c", name: "Load Balancer 2", port: 80, rule: "DNS delegation" },
|
|
||||||
{ id: "d", name: "Load Balancer 6", port: 3000, rule: "Round robin" },
|
|
||||||
{ id: "e", name: "Load Balancer 4", port: 443, rule: "Round robin" },
|
|
||||||
{ id: "f", name: "Load Balancer 5", port: 80, rule: "DNS delegation" },
|
|
||||||
];
|
|
||||||
|
|
||||||
let selectedRowIds = [rows[0].id, rows[1].id];
|
|
||||||
|
|
||||||
$: console.log("selectedRowIds", selectedRowIds);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DataTable batchSelection bind:selectedRowIds {headers} {rows} />
|
|
|
@ -1,30 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DataTable } from "../types";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
headers={[
|
|
||||||
{ key: "name", value: "Name" },
|
|
||||||
{ key: "protocol", value: "Protocol", width: "400px", minWidth: "40%" },
|
|
||||||
{ key: "port", value: "Port" },
|
|
||||||
{ key: "rule.name", value: "Rule", sort: false },
|
|
||||||
]}
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
id: "a",
|
|
||||||
name: "Load Balancer 3",
|
|
||||||
protocol: "HTTP",
|
|
||||||
port: 3000,
|
|
||||||
"rule.name": "Round robin",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<span slot="cell" let:cell let:row>
|
|
||||||
{cell.key === "rule.name"}
|
|
||||||
{#if cell.key === "name"}
|
|
||||||
{row.name} {row.id}
|
|
||||||
{:else}
|
|
||||||
{cell.value}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</DataTable>
|
|
Loading…
Add table
Add a link
Reference in a new issue