vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
1,067 lines (810 loc) • 32.1 kB
text/typescript
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import { nextTick } from "vue";
import UTable from "../UTable.vue";
import UTableRow from "../UTableRow.vue";
import UEmpty from "../../ui.container-empty/UEmpty.vue";
import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
import UDivider from "../../ui.container-divider/UDivider.vue";
import {
LoaderProgressSymbol,
createLoaderProgress,
} from "../../ui.loader-progress/useLoaderProgress";
import type { Column, Row, Props, ColumnObject } from "../types";
describe("UTable.vue", () => {
const defaultColumns: ColumnObject[] = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "role", label: "Role" },
];
const defaultRows: Row[] = [
{
id: "1",
name: "John Doe",
email: "john@example.com",
role: "Admin",
},
{
id: "2",
name: "Jane Smith",
email: "jane@example.com",
role: "User",
},
{
id: "3",
name: "Bob Johnson",
email: "bob@example.com",
role: "Manager",
},
];
const rowsWithDates: Row[] = [
{
id: "1",
name: "John Doe",
email: "john@example.com",
role: "Admin",
rowDate: "2023-01-01",
},
{
id: "2",
name: "Jane Smith",
email: "jane@example.com",
role: "User",
rowDate: "2023-01-02",
},
];
function getDefaultProps(overrides = {}): Props {
return {
columns: defaultColumns,
rows: defaultRows,
...overrides,
};
}
function getGlobalOptions() {
return {
provide: {
[LoaderProgressSymbol]: createLoaderProgress(),
},
};
}
function mountUTable(props: Props, options = {}) {
return mount(UTable, {
props,
global: getGlobalOptions(),
...options,
});
}
describe("Props", () => {
it("Columns – renders table headers correctly", () => {
const component = mountUTable(getDefaultProps());
defaultColumns.forEach((column) => {
expect(component.text()).toContain(column.label);
});
});
it("Columns – handles string columns", () => {
const stringColumns: Column[] = ["name", "email", "role"];
const component = mountUTable(getDefaultProps({ columns: stringColumns }));
stringColumns.forEach((column) => {
expect(component.text()).toContain(column);
});
});
it("Columns – applies column thClass to header cells", () => {
const columnsWithClasses: ColumnObject[] = [
{ key: "name", label: "Name", thClass: "name-header-class" },
{ key: "email", label: "Email", thClass: "email-header-class" },
{ key: "role", label: "Role" },
];
const component = mountUTable(getDefaultProps({ columns: columnsWithClasses }));
const headerCells = component.findAll("th");
expect(headerCells[0].attributes("class")).toContain(columnsWithClasses[0].thClass);
expect(headerCells[1].attributes("class")).toContain(columnsWithClasses[1].thClass);
});
it("Columns – hides columns when isShown is false", async () => {
const columnsWithHidden: ColumnObject[] = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email", isShown: false },
{ key: "role", label: "Role" },
];
const component = mountUTable(getDefaultProps({ columns: columnsWithHidden }));
const theadElement = component.get("thead");
expect(theadElement.text()).toContain(columnsWithHidden[0].label);
expect(theadElement.text()).not.toContain(columnsWithHidden[1].label);
expect(theadElement.text()).toContain(columnsWithHidden[2].label);
});
it("Rows – renders all table rows", () => {
const component = mountUTable(getDefaultProps());
const tableRows = component.findAllComponents(UTableRow);
expect(tableRows).toHaveLength(defaultRows.length);
});
it("Rows – renders empty state when no rows provided", () => {
const component = mountUTable(getDefaultProps({ rows: [] }));
const emptyComponent = component.findComponent(UEmpty);
expect(emptyComponent.exists()).toBe(true);
});
it("Selectable – renders select all checkbox when selectable is true", () => {
const component = mountUTable(getDefaultProps({ selectable: true }));
const selectAllCheckbox = component.get("thead").find("input");
expect(selectAllCheckbox.exists()).toBe(true);
});
it("Selectable – does not render select all checkbox when selectable is false", () => {
const component = mountUTable(getDefaultProps({ selectable: false }));
const selectAllCheckbox = component.get("thead").find("input");
expect(selectAllCheckbox.exists()).toBe(false);
});
it("Selected Rows – displays selection counter", async () => {
const selectedRows = [defaultRows[0], defaultRows[1]];
const component = mountUTable(getDefaultProps({ selectable: true, selectedRows }));
const theadElement = component.get("thead");
expect(theadElement.text()).toContain(selectedRows.length.toString());
});
it("Selected Rows – passes is checked state to row", async () => {
const selectedRows = [defaultRows[0], defaultRows[1]];
const component = mountUTable(getDefaultProps({ selectable: true, selectedRows }));
const tableRows = component.findAllComponents(UTableRow);
tableRows.forEach((row) => {
const currentRowId = row.props("row").id;
const isChecked = !!selectedRows.find((selectedRow) => selectedRow.id === currentRowId);
expect(row.props("isChecked")).toBe(isChecked);
});
});
it("Expanded Rows – passes is expanded state to row", async () => {
const expandedRows = [defaultRows[0].id, defaultRows[2].id];
const component = mountUTable(getDefaultProps({ expandedRows }));
const tableRows = component.findAllComponents(UTableRow);
tableRows.forEach((row) => {
const currentRowId = row.props("row").id;
expect(row.props("isExpanded")).toBe(expandedRows.includes(currentRowId));
});
});
it("Loading – shows loader when loading is true", () => {
const component = mountUTable(getDefaultProps({ loading: true }));
const loader = component.findComponent(ULoaderProgress);
expect(loader.exists()).toBe(true);
expect(loader.props("loading")).toBe(true);
});
it("Loading – hides loader when loading is false", () => {
const component = mountUTable(getDefaultProps({ loading: false }));
const loader = component.findComponent(ULoaderProgress);
expect(loader.props("loading")).toBe(false);
});
it("Data Test – applies correct data-test attributes", () => {
const dataTest = "test-table";
const component = mountUTable(getDefaultProps({ dataTest }));
expect(component.attributes("data-test")).toBe(dataTest);
});
it("Empty Cell Label – passes empty cell label to table rows", () => {
const emptyCellLabel = "No data available";
const component = mountUTable(getDefaultProps({ emptyCellLabel }));
const tableRow = component.getComponent(UTableRow);
expect(tableRow.props("emptyCellLabel")).toBe(emptyCellLabel);
});
it("Date Divider – renders date dividers when dateDivider is true", () => {
const component = mountUTable(
getDefaultProps({
rows: rowsWithDates,
dateDivider: true,
}),
);
const dividers = component.findAllComponents(UDivider);
expect(dividers.length).toBeGreaterThan(0);
});
it("Date Divider – renders custom date dividers", () => {
const customDividers = [
{ date: "2023-01-01", label: "New Year" },
{ date: "2023-01-02", label: "Day Two" },
];
const component = mountUTable(
getDefaultProps({
rows: rowsWithDates,
dateDivider: customDividers,
}),
);
expect(component.text()).not.toContain("New Year"); // should not render above first item
expect(component.text()).toContain("Day Two");
});
it("Compact – applies compact classes from config", () => {
const compactClasses = "px-4 py-3";
const component = mountUTable(
getDefaultProps({
compact: true,
selectable: true,
selectedRows: [defaultRows[0]],
}),
{
slots: {
footer: "Footer content",
},
},
);
const headerCells = component.findAll("th");
headerCells.forEach((cell) => {
expect(cell.attributes("class")).toContain(compactClasses);
});
});
it("Virtual Scroll – is disabled by default", () => {
const component = mountUTable(getDefaultProps());
expect(component.vm.$props.virtualScroll).toBe(false);
});
it("Virtual Scroll – enables virtual scrolling when virtualScroll is true", () => {
const component = mountUTable(
getDefaultProps({
virtualScroll: true,
}),
);
expect(component.vm.$props.virtualScroll).toBe(true);
// Check that the table wrapper has virtual scroll classes
const tableWrapper = component.find("table").element.parentElement;
expect(tableWrapper?.className).toContain("overflow-y-auto");
});
it("Virtual Scroll – renders spacer rows when virtualScroll is enabled", async () => {
const manyRows = Array.from({ length: 100 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: true,
rowHeight: 40,
}),
);
await nextTick();
const allRows = component.findAll("tbody tr");
const spacerRows = allRows.filter((row) => {
const td = row.find("td");
const hasTableRow = row.findComponent(UTableRow).exists();
return (
td.exists() &&
td.attributes("colspan") &&
td.attributes("style")?.includes("height") &&
!hasTableRow
);
});
// Should have at least one spacer row (top or bottom)
expect(spacerRows.length).toBeGreaterThanOrEqual(1);
});
it("Row Height – uses default rowHeight value", () => {
const component = mountUTable(getDefaultProps());
expect(component.vm.$props.rowHeight).toBe(40);
});
it("Row Height – accepts custom rowHeight value", () => {
const customRowHeight = 60;
const component = mountUTable(
getDefaultProps({
rowHeight: customRowHeight,
}),
);
expect(component.vm.$props.rowHeight).toBe(customRowHeight);
});
it("Buffer Size – uses default bufferSize value", () => {
const component = mountUTable(getDefaultProps());
expect(component.vm.$props.bufferSize).toBe(10);
});
it("Buffer Size – accepts custom bufferSize value", () => {
const customBufferSize = 20;
const component = mountUTable(
getDefaultProps({
bufferSize: customBufferSize,
}),
);
expect(component.vm.$props.bufferSize).toBe(customBufferSize);
});
it("Virtual Scroll – renders only visible rows plus buffer", async () => {
const manyRows = Array.from({ length: 100 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: true,
rowHeight: 40,
scrollHeight: "400px",
bufferSize: 5,
}),
);
await nextTick();
const tableRows = component.findAllComponents(UTableRow);
// Should render less than total rows (only visible + buffer)
expect(tableRows.length).toBeLessThan(manyRows.length);
expect(tableRows.length).toBeGreaterThan(0);
});
it("Virtual Scroll – renders all rows when virtualScroll is false", () => {
const manyRows = Array.from({ length: 50 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: false,
}),
);
const tableRows = component.findAllComponents(UTableRow);
expect(tableRows.length).toBe(manyRows.length);
});
it("Scroll Height – accepts scrollHeight prop", () => {
const scrollHeight = "500px";
const component = mountUTable(
getDefaultProps({
virtualScroll: true,
scrollHeight,
}),
);
expect(component.vm.$props.scrollHeight).toBe(scrollHeight);
});
it("Search – uses default empty search value", () => {
const component = mountUTable(getDefaultProps());
expect(component.vm.$props.search).toBe("");
});
it("Search – accepts search string", () => {
const searchQuery = "john";
const component = mountUTable(
getDefaultProps({
search: searchQuery,
}),
);
expect(component.vm.$props.search).toBe(searchQuery);
});
it("Search – emits search event with total matches count", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "doe" });
await nextTick();
expect(component.emitted("search")).toBeTruthy();
expect(component.emitted("search")![0][0]).toBe(1);
});
it("Search – emits search event with zero when no matches found", async () => {
const component = mountUTable(getDefaultProps());
// First set a search that has matches
await component.setProps({ search: "doe" });
await nextTick();
// Then set a search with no matches
await component.setProps({ search: "nonexistent" });
await nextTick();
const emittedEvents = component.emitted("search");
expect(emittedEvents).toBeTruthy();
expect(emittedEvents![emittedEvents!.length - 1][0]).toBe(0);
});
it("Search – finds multiple matches across different rows", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "example" });
await nextTick();
const emittedEvents = component.emitted("search");
expect(emittedEvents).toBeTruthy();
expect(emittedEvents![0][0]).toBe(3); // All 3 rows have "example.com" in email
});
it("Search – is case insensitive", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "DOE" });
await nextTick();
const emittedEvents = component.emitted("search");
expect(emittedEvents).toBeTruthy();
expect(emittedEvents![0][0]).toBe(1);
// Clear search first to trigger a new event
await component.setProps({ search: "" });
await nextTick();
await component.setProps({ search: "doe" });
await nextTick();
expect(emittedEvents![emittedEvents!.length - 1][0]).toBe(1);
});
it("Search – finds partial matches", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "Jo" });
await nextTick();
expect(component.emitted("search")![0][0]).toBeGreaterThan(0);
});
it("Search Match – uses default searchMatch value", () => {
const component = mountUTable(getDefaultProps());
expect(component.vm.$props.searchMatch).toBe(-1);
});
it("Search Match – accepts searchMatch index", () => {
const searchMatchIndex = 2;
const component = mountUTable(
getDefaultProps({
search: "example",
searchMatch: searchMatchIndex,
}),
);
expect(component.vm.$props.searchMatch).toBe(searchMatchIndex);
});
it("Search Match – passes search props to table rows", async () => {
const searchQuery = "john";
const component = mountUTable(
getDefaultProps({
search: searchQuery,
}),
);
await nextTick();
const tableRow = component.getComponent(UTableRow);
expect(tableRow.props("search")).toBe(searchQuery);
});
});
describe("Slots", () => {
it("Header Counter Slot – renders custom header counter content", () => {
const customHeaderCounterContent = "Custom Header Counter";
const component = mountUTable(
getDefaultProps({
selectable: true,
selectedRows: [defaultRows[0]],
}),
{
slots: {
"header-counter": customHeaderCounterContent,
},
},
);
expect(component.text()).toContain(customHeaderCounterContent);
});
it("Header Slot – renders custom header content", () => {
const customHeaderContent = "Custom Name Header";
const component = mountUTable(getDefaultProps(), {
slots: {
"header-name": customHeaderContent,
},
});
expect(component.text()).toContain(customHeaderContent);
});
it("Header Slot – exposes column and index to slot", () => {
const component = mountUTable(getDefaultProps(), {
slots: {
"header-name": "Column: {{ params.column?.label }}, Index: {{ params.index }}",
},
});
expect(component.text()).toContain("Column: Name");
expect(component.text()).toContain("Index: 0");
});
it("Cell Slot – renders custom cell content", () => {
const customCellContent = "Custom Cell Content";
const component = mountUTable(getDefaultProps(), {
slots: {
"cell-name": customCellContent,
},
});
expect(component.text()).toContain(customCellContent);
});
it("Cell Slot – exposes value, row, index, and cellIndex to slot", () => {
const component = mountUTable(getDefaultProps(), {
slots: {
"cell-name": `
Value: {{ params.value }}, Row: {{ params.row.id }}
Index: {{ params.index }}, Cell: {{ params.cellIndex }}
`,
},
});
expect(component.text()).toContain("Value: John Doe");
expect(component.text()).toContain("Row: 1");
expect(component.text()).toContain("Index: 0");
expect(component.text()).toContain("Cell: 0");
});
it("Expand Slot – renders custom expand content", () => {
const nestedRows: Row[] = [
{
id: "1",
name: "Parent Row",
email: "parent@example.com",
role: "Admin",
row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
},
];
const customExpandContent = "Custom Expand Button";
const component = mountUTable(getDefaultProps({ rows: nestedRows }), {
slots: {
expand: customExpandContent,
},
});
expect(component.text()).toContain(customExpandContent);
});
it("Before First Row Slot – renders content before first row", () => {
const customContent = "Before First Row Content";
const component = mountUTable(getDefaultProps(), {
slots: {
"before-first-row": customContent,
},
});
expect(component.text()).toContain(customContent);
});
it("Header Actions Slot – renders header actions when rows are selected", async () => {
const customActions = "Custom Actions";
const component = mountUTable(
getDefaultProps({
selectable: true,
selectedRows: [defaultRows[0]],
}),
{
slots: {
"header-actions": customActions,
},
},
);
await nextTick();
expect(component.text()).toContain(customActions);
});
it("Footer Slot – renders custom footer content", () => {
const customFooterContent = "Custom Footer";
const component = mountUTable(getDefaultProps(), {
slots: {
footer: customFooterContent,
},
});
expect(component.text()).toContain(customFooterContent);
});
it("Nested Row Slot – renders custom nested row content", async () => {
const nestedRows: Row[] = [
{
id: "1",
name: "Parent Row",
email: "parent@example.com",
role: "Admin",
row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
},
];
const customNestedContent = "Custom Nested Content";
const component = mountUTable(
getDefaultProps({
rows: nestedRows,
expandedRows: ["1"],
}),
{
slots: {
"nested-row": customNestedContent,
},
},
);
await nextTick();
expect(component.text()).toContain(customNestedContent);
});
});
describe("Events", () => {
it("Click Row – emits clickRow event when row is clicked", async () => {
const component = mountUTable(getDefaultProps());
const firstRow = component.find("tbody tr");
await firstRow.trigger("click");
expect(component.emitted("clickRow")).toBeTruthy();
expect(component.emitted("clickRow")![0][0]).toMatchObject(defaultRows[0]);
});
it("Double Click Row – emits doubleClickRow event when row is double-clicked", async () => {
const component = mountUTable(getDefaultProps());
const firstRow = component.find("tbody tr");
await firstRow.trigger("dblclick");
expect(component.emitted("doubleClickRow")).toBeTruthy();
expect(component.emitted("doubleClickRow")![0][0]).toMatchObject(defaultRows[0]);
});
it("Click Cell – emits clickCell event when cell is clicked", async () => {
const component = mountUTable(getDefaultProps());
const firstCell = component.find("tbody tr td");
await firstCell.trigger("click");
expect(component.emitted("clickCell")).toBeTruthy();
const emittedData = component.emitted("clickCell")![0];
expect(emittedData[0]).toBe(defaultRows[0].name);
expect(emittedData[1]).toMatchObject(defaultRows[0]);
expect(emittedData[2]).toBe("name");
});
it("Toggle Row Checkbox – emits update:selectedRows when row checkbox is clicked", async () => {
const component = mountUTable(getDefaultProps({ selectable: true }));
const checkboxCell = component.find("tbody tr td[data-checkbox-id]");
await checkboxCell.trigger("click");
expect(component.emitted("update:selectedRows")).toBeTruthy();
const emittedRows = component.emitted("update:selectedRows")![0][0] as Row[];
expect(emittedRows).toHaveLength(1);
expect(emittedRows[0]).toMatchObject(defaultRows[0]);
});
it("Toggle Expand – emits row-expand and update:expandedRows when expand icon is clicked", async () => {
const expandableRow: Row = {
id: "1",
name: "Parent Row",
email: "parent@example.com",
role: "Admin",
row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
};
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
const expandIcon = component.find("[data-row-toggle-icon='1']");
await expandIcon.trigger("click");
expect(component.emitted("row-expand")).toBeTruthy();
expect(component.emitted("row-expand")![0][0]).toMatchObject(expandableRow);
expect(component.emitted("update:expandedRows")).toBeTruthy();
expect(component.emitted("update:expandedRows")![0][0]).toEqual(["1"]);
});
it("Toggle Expand – emits row-collapse and update:expandedRows when collapse icon is clicked", async () => {
const expandableRow: Row = {
id: "1",
name: "Parent Row",
email: "parent@example.com",
role: "Admin",
row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
};
const component = mountUTable(
getDefaultProps({
rows: [expandableRow],
expandedRows: ["1"],
}),
);
const collapseIcon = component.find("[data-row-toggle-icon='1']");
await collapseIcon.trigger("click");
expect(component.emitted("row-collapse")).toBeTruthy();
expect(component.emitted("row-collapse")![0][0]).toMatchObject(expandableRow);
expect(component.emitted("update:expandedRows")).toBeTruthy();
expect(component.emitted("update:expandedRows")![0][0]).toEqual([]);
});
it("Multiple Row Selection – emits update:selectedRows with all selected rows", async () => {
const component = mountUTable(getDefaultProps({ selectable: true }));
// Select first row
let tableRows = component.findAll("tbody tr");
await tableRows[0].find("td[data-checkbox-id]").trigger("click");
await nextTick();
// Update props with the first selected row
const firstEmit = component.emitted("update:selectedRows")![0][0] as Row[];
await component.setProps({ selectedRows: firstEmit });
await nextTick();
// Re-query the DOM after props update
tableRows = component.findAll("tbody tr");
// Select second row
await tableRows[1].find("td[data-checkbox-id]").trigger("click");
await nextTick();
const emittedEvents = component.emitted("update:selectedRows");
expect(emittedEvents).toBeTruthy();
const emittedRows = emittedEvents![emittedEvents!.length - 1][0] as Row[];
expect(emittedRows).toHaveLength(2);
expect(emittedRows[0]).toMatchObject(defaultRows[0]);
expect(emittedRows[1]).toMatchObject(defaultRows[1]);
});
it("Nested Row Expansion – emits update:expandedRows for nested rows", async () => {
const nestedRows: Row[] = [
{
id: "1",
name: "Parent",
email: "parent@example.com",
role: "Admin",
row: [
{
id: "1-1",
name: "Child 1",
email: "child1@example.com",
role: "User",
row: [
{ id: "1-1-1", name: "Grandchild", email: "grandchild@example.com", role: "Guest" },
],
},
],
},
];
const component = mountUTable(getDefaultProps({ rows: nestedRows }));
const expandIcon = component.find("[data-row-toggle-icon='1']");
await expandIcon.trigger("click");
expect(component.emitted("update:expandedRows")).toBeTruthy();
expect(component.emitted("update:expandedRows")![0][0]).toEqual(["1"]);
});
it("Expand Icon – prevents row click events when expand icon is clicked", async () => {
const expandableRow: Row = {
id: "1",
name: "Parent Row",
email: "parent@example.com",
role: "Admin",
row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
};
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
const expandIcon = component.find("[data-row-toggle-icon='1']");
await expandIcon.trigger("click");
await expandIcon.trigger("dblclick");
// Row click events should not be triggered when expand icon is clicked
expect(component.emitted("clickRow")).toBeFalsy();
expect(component.emitted("doubleClickRow")).toBeFalsy();
});
it("Search Event – emits when search prop changes", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "doe" });
await nextTick();
expect(component.emitted("search")).toBeTruthy();
expect(component.emitted("search")![0][0]).toBe(1);
});
it("Search Event – emits updated count when search changes", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "doe" });
await nextTick();
const initialCount = component.emitted("search")![0][0];
await component.setProps({ search: "example" });
await nextTick();
const updatedCount = component.emitted("search")![1][0];
expect(updatedCount).toBeGreaterThan(initialCount as number);
});
it("Search Event – emits zero when search is cleared", async () => {
const component = mountUTable(getDefaultProps());
await component.setProps({ search: "doe" });
await nextTick();
await component.setProps({ search: "" });
await nextTick();
const emittedEvents = component.emitted("search");
expect(emittedEvents![emittedEvents!.length - 1][0]).toBe(0);
});
it("Search Event – counts all occurrences in a single cell", async () => {
const rowsWithDuplicates: Row[] = [
{
id: "1",
name: "Test Test Test",
email: "test@example.com",
role: "User",
},
];
const component = mountUTable(getDefaultProps({ rows: rowsWithDuplicates }));
await component.setProps({ search: "test" });
await nextTick();
expect(component.emitted("search")![0][0]).toBe(4);
});
});
describe("Virtual Scroll with Search", () => {
it("Virtual Scroll with Search – renders only visible rows with search", async () => {
const manyRows = Array.from({ length: 100 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: i % 2 === 0 ? "Admin" : "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: true,
rowHeight: 40,
scrollHeight: "400px",
}),
);
await component.setProps({ search: "Admin" });
await nextTick();
const tableRows = component.findAllComponents(UTableRow);
expect(tableRows.length).toBeLessThan(manyRows.length);
expect(component.emitted("search")).toBeTruthy();
});
it("Virtual Scroll with Search – passes search match columns to rows", async () => {
const component = mountUTable(
getDefaultProps({
virtualScroll: true,
search: "john",
}),
);
await nextTick();
const tableRow = component.getComponent(UTableRow);
expect(tableRow.props("search")).toBe("john");
expect(tableRow.props("searchMatchColumns")).toBeDefined();
});
it("Virtual Scroll with Search – handles searchMatch prop", async () => {
const manyRows = Array.from({ length: 50 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: true,
search: "user",
searchMatch: 0,
}),
);
await nextTick();
expect(component.vm.$props.searchMatch).toBe(0);
});
it("Virtual Scroll with Search – emits search event with virtual scroll enabled", async () => {
const manyRows = Array.from({ length: 100 }, (_, i) => ({
id: String(i + 1),
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: "User",
}));
const component = mountUTable(
getDefaultProps({
rows: manyRows,
virtualScroll: true,
}),
);
await component.setProps({ search: "user" });
await nextTick();
expect(component.emitted("search")).toBeTruthy();
expect(component.emitted("search")![0][0]).toBeGreaterThan(0);
});
});
});