@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
936 lines (806 loc) • 32.1 kB
text/typescript
import { describe, test, expect, afterEach, vi, beforeEach } from "vitest";
import { enableAutoUnmount, mount } from "@vue/test-utils";
import { nextTick } from "vue";
import type { TableColumn } from "../types";
import OTable from "@/components/table/Table.vue";
describe("OTable tests", () => {
enableAutoUnmount(afterEach);
const data = [
{
id: 1,
first_name: "Jesse",
last_name: "Simmons",
date: "2016-10-15 13:43:27",
gender: "Male",
},
{
id: 2,
first_name: "John",
last_name: "Jacobs",
date: "2016-12-15 06:00:53",
gender: "Male",
},
{
id: 3,
first_name: "Tina",
last_name: "Gilbert",
date: "2016-04-26 06:26:28",
gender: "Female",
},
{
id: 4,
first_name: "Clarence",
last_name: "Flores",
date: "2016-04-10 10:28:46",
gender: "Male",
},
{
id: 5,
first_name: "Anne",
last_name: "Lee",
date: "2016-12-06 14:38:38",
gender: "Female",
},
{
id: 6,
first_name: "Bonita",
last_name: "Cortez",
date: "2016-03-03 10:28:46",
gender: "Female",
},
{
id: 7,
first_name: "Randolf",
last_name: "Mayor",
date: "2016-07-04 14:38:39",
gender: "Male",
},
];
const columns: TableColumn<(typeof data)[number]>[] = [
{
field: "id",
label: "ID",
width: "40",
numeric: true,
sortable: true,
},
{
field: "first_name",
label: "First Name",
sortable: true,
},
{
field: "last_name",
label: "Last Name",
sortable: true,
},
{
field: "date",
label: "Date",
position: "centered",
sortable: true,
},
{
field: "gender",
label: "Gender",
sortable: true,
},
];
test("render correctly", async () => {
const wrapper = mount(OTable, {
props: { data, columns },
});
await nextTick(); // await child component rendering
expect(!!wrapper.vm).toBeTruthy();
expect(wrapper.exists()).toBeTruthy();
expect(wrapper.attributes("data-oruga")).toBe("table");
expect(wrapper.html()).toMatchSnapshot();
});
test("holds columns", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "default", width: "100px" },
{ label: "pecent", width: "50%" },
{ label: "fixed_num", width: 100 },
{ label: "fixed_str", width: "100" },
],
},
});
await nextTick();
const headers = wrapper.findAll("th");
expect(headers).toHaveLength(4);
const cols = headers.filter((th) => th.find("span").exists());
expect(cols).toHaveLength(4);
expect(cols[0].attributes("style")).toBe(
"width: 100px; min-width: 100px;",
);
expect(cols[1].attributes("style")).toBe("width: 50%; min-width: 50%;");
expect(cols[2].attributes("style")).toBe(
"width: 100px; min-width: 100px;",
);
expect(cols[3].attributes("style")).toBe(
"width: 100px; min-width: 100px;",
);
});
test("displays all data", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
},
});
await nextTick();
const bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(data.length);
});
describe("test column props", () => {
test("test column label", async () => {
const wrapper = mount(OTable, { props: { data, columns } });
await nextTick(); // await child component rendering
const table = wrapper.find("table");
expect(table.exists()).toBeTruthy();
const ths = table.findAll("th");
expect(ths).toHaveLength(columns.length);
for (let i = 0; i < ths.length; i++) {
expect(ths[i].text()).toBe(columns[i].label);
}
});
test("test column field", async () => {
const wrapper = mount(OTable, { props: { data, columns } });
await nextTick(); // await child component rendering
const table = wrapper.find("table");
expect(table.exists()).toBeTruthy();
const trs = table.findAll("tr");
expect(trs).toHaveLength(data.length + 1);
for (let i = 1; i < trs.length; i++) {
const tds = trs[i].findAll("td");
expect(tds).toHaveLength(columns.length);
for (let j = 0; j < columns.length; j++) {
expect(tds[j].text()).toBe(
String(data[i - 1][columns[j].field!]),
);
}
}
});
test("test column formatter", async () => {
const columns: TableColumn<(typeof data)[number]>[] = [
{
label: "ID",
width: "40",
numeric: true,
sortable: true,
thAttrs: { style: { "min-width": "40px" } },
formatter: (a, b): string => {
expect(a).toBe(b);
return "abc";
},
},
{
label: "First Name",
sortable: true,
formatter: (a, b) => {
expect(a).toBe(b);
return "abc";
},
},
];
const wrapper = mount(OTable, { props: { data, columns } });
await nextTick(); // await child component rendering
const table = wrapper.find("table");
expect(table.exists()).toBeTruthy();
const trs = table.findAll("tr");
expect(trs).toHaveLength(data.length + 1);
for (let i = 1; i < trs.length; i++) {
const tds = trs[i].findAll("td");
expect(tds).toHaveLength(columns.length);
for (let j = 0; j < columns.length; j++) {
expect(tds[j].text()).toBe(
columns[j].formatter?.(data[i], data[i]),
);
}
}
});
test("test column thAttrs and tdAttrs", async () => {
const columns: TableColumn<(typeof data)[number]>[] = [
{
field: "id",
label: "ID",
width: "40",
numeric: true,
sortable: true,
thAttrs: { class: "th-id" },
tdAttrs: { class: "td-id" },
},
{
field: "abc",
label: "ABC",
sortable: true,
thAttrs: { class: "th-abc" },
tdAttrs: { class: "td-abc" },
},
];
const wrapper = mount(OTable, { props: { data, columns } });
await nextTick(); // await child component rendering
const table = wrapper.find("table");
expect(table.exists()).toBeTruthy();
const ths = table.findAll("th");
expect(ths).toHaveLength(columns.length);
expect(ths[0].classes("th-id")).toBeTruthy();
expect(ths[1].classes("th-abc")).toBeTruthy();
const tds = table.findAll("td");
expect(tds).toHaveLength(columns.length * data.length);
for (let i = 0; i < tds.length; i++) {
expect(
tds[i].classes(i % 2 === 0 ? "td-id" : "td-abc"),
).toBeTruthy();
}
});
});
describe("test filterable", () => {
const data = [
{ id: 1, name: "Jesse" },
{ id: 2, name: "João" },
{ id: 3, name: "Tina" },
{ id: 4, name: "Anne" },
{ id: 5, name: "Clarence" },
];
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.clearAllTimers();
vi.restoreAllMocks();
});
test("displays filter row when at least one column is filterable", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
},
});
await nextTick();
const header = wrapper.find("thead");
expect(header.exists()).toBeTruthy();
const headRows = header.findAll("tr");
expect(headRows).toHaveLength(2);
const inputs = headRows[1].findAll("input");
expect(inputs).toHaveLength(1);
});
test("displays filter input only on filterable columns", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
},
});
await nextTick();
const headRows = wrapper.findAll("thead tr");
expect(headRows).toHaveLength(2);
const filterCells = headRows[1].findAll("th");
expect(filterCells[0].find("input").exists()).toBeFalsy(); // ID column is not filterable
expect(filterCells[1].find("input").exists()).toBeTruthy(); // Name column is filterable
});
test("displays filtered data when filtering", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
},
});
await nextTick();
let bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(data.length); // check all is shown
const header = wrapper.find("thead");
const inputs = header.findAll("input");
expect(inputs).toHaveLength(1);
const input = inputs[0];
// displays a subset of data when filtering by "J"
await input.setValue("J");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(2); // Jesse and João
expect(wrapper.emitted("filters-change")).toHaveLength(1);
// displays a subset of data when filtering by name without accent
await input.setValue("Joao");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(1); // João
expect(wrapper.emitted("filters-change")).toHaveLength(2);
// displays a subset of data when filtering by name with accent
await input.setValue("João");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(1); // João
expect(wrapper.emitted("filters-change")).toHaveLength(3);
// reset filter
await input.setValue("");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(data.length); // check all is shown again
expect(wrapper.emitted("filters-change")).toHaveLength(4);
});
test("displays filtered data when filtering and updating data", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
},
});
await nextTick();
const input = wrapper.find("thead input");
expect(input.exists()).toBeTruthy();
await input.setValue("J");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
let bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(2); // Jesse and João
await wrapper.setProps({
data: [...data, { id: 6, name: "Justin" }],
});
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(3); // Jesse, João and Justin
});
test("debounce search filtering when filter-debounce is defined", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
filterDebounce: 1000,
},
});
await nextTick(); // await child component rendering
const input = wrapper.find("thead input");
expect(input.exists()).toBeTruthy();
for (let i = 0; i < 10; i++) {
await input.setValue("J".repeat(10 - i));
await input.trigger("input");
const bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(5); // No filtering yet
}
vi.runAllTimers(); // run debounce timer
const bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(5); // Filtering after debounce
});
test("hide detail row when data is filtered out", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data,
detailed: true,
// open detail for first data row
detailedRows: [data[0]],
},
slots: { detail: "DETAIL" },
});
await nextTick();
const input = wrapper.find("thead input");
expect(input.exists()).toBeTruthy();
let bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(data.length + 1); // data rows + detail for row 1
expect(bodyRows[1].text()).toBe("DETAIL"); // check second row is detal row
await input.setValue("T");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick();
bodyRows = wrapper.findAll("tbody tr");
expect(bodyRows).toHaveLength(1); // Tina
});
});
describe("test sorting", () => {
const random_words = [
"Apfel", // A
"Ball", // B
"Clown", // C
"Dach", // D
"Elefant", // E
"Fisch", // F
"Garten", // G
"Haus", // H
"Insel", // I
"Jacke", // J
"Kugel", // K
"Lampe", // L
"Maus", // M
"Nase", // N
"Ozean", // O
"Pferd", // P
"Qualle", // Q
"Rose", // R
"Sonne", // S
"Tiger", // T
"Uhr", // U
"Vogel", // V
"Wolke", // W
"Xylofon", // X
"Yacht", // Y
"Zebra", // Z
];
test("tests sort default when paginated", async () => {
const perPage = 10;
let lastId = 1;
const data = random_words.map((word) => ({
id: lastId++,
name: word,
}));
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", sortable: true },
],
perPage,
paginated: true,
data,
defaultSort: "name",
},
});
await nextTick(); // await column rendered
const head = wrapper.find("thead");
const ths = head.findAll("th");
expect(ths).toHaveLength(2);
expect(ths[1].classes("o-table__th--sortable")).toBeTruthy();
// check arrow pointing upwards
expect(
ths[1].find('[data-oruga="icon"]').attributes("style"),
).toBeFalsy();
const body = wrapper.find("tbody");
const trs = body.findAll("tr");
expect(trs).toHaveLength(perPage);
// check default sort
for (let idx = 0; idx < perPage; idx++) {
const tds = trs[idx].findAll("td");
expect(tds).toHaveLength(2);
expect(tds[1].text()).toBe(data[idx].name);
}
});
test("tests sort default desc when paginated", async () => {
const perPage = 10;
let lastId = 1;
const data = random_words.map((word) => ({
id: lastId++,
name: word,
}));
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", sortable: true },
],
perPage,
paginated: true,
data,
defaultSort: "name",
defaultSortDirection: "desc",
},
});
await nextTick(); // await column rendered
const head = wrapper.find("thead");
const ths = head.findAll("th");
expect(ths).toHaveLength(2);
expect(ths[1].classes("o-table__th--sortable")).toBeTruthy();
// check arrow pointing downwards
expect(
ths[1].find('[data-oruga="icon"]').attributes("style"),
).toBeTruthy();
const body = wrapper.find("tbody");
const trs = body.findAll("tr");
expect(trs).toHaveLength(perPage);
// check default desc sort
for (let idx = 0; idx < perPage; idx++) {
const tds = trs[idx].findAll("td");
expect(tds).toHaveLength(2);
expect(tds[1].text()).toBe(data[data.length - 1 - idx].name);
}
});
test("tests sort change when paginated", async () => {
const perPage = 10;
let lastId = 1;
const data = random_words.map((word) => ({
id: lastId++,
name: word,
}));
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", sortable: true },
],
perPage,
paginated: true,
data,
defaultSort: "name",
},
});
await nextTick(); // await column rendered
const head = wrapper.find("thead");
const ths = head.findAll("th");
expect(ths).toHaveLength(2);
expect(ths[1].classes("o-table__th--sortable")).toBeTruthy();
// check arrow pointing upwards
expect(
ths[1].find('[data-oruga="icon"]').attributes("style"),
).toBeFalsy();
const body = wrapper.find("tbody");
let trs = body.findAll("tr");
expect(trs).toHaveLength(perPage);
// check asc sort
for (let idx = 0; idx < perPage; idx++) {
const tds = trs[idx].findAll("td");
expect(tds).toHaveLength(2);
expect(tds[1].text()).toBe(data[idx].name);
}
await ths[1].trigger("click");
// check arrow pointing downwards
expect(
ths[1].find('[data-oruga="icon"]').attributes("style"),
).toBeTruthy();
trs = body.findAll("tr");
expect(trs).toHaveLength(perPage);
// check desc sort
for (let idx = 0; idx < perPage; idx++) {
const tds = trs[idx].findAll("td");
expect(tds).toHaveLength(2);
expect(tds[1].text()).toBe(data[data.length - 1 - idx].name);
}
});
test("tests sort correctly after data update when paginated", async () => {
const perPage = 10;
let lastId = 1;
const data = random_words.map((word) => ({
id: lastId++,
name: word,
}));
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", sortable: true },
],
perPage,
paginated: true,
data,
defaultSort: "name",
},
});
await nextTick(); // await column rendered
const head = wrapper.find("thead");
const ths = head.findAll("th");
expect(ths).toHaveLength(2);
expect(ths[1].classes("o-table__th--sortable")).toBeTruthy();
// check arrow pointing upwards
expect(
ths[1].find('[data-oruga="icon"]').attributes("style"),
).toBeFalsy();
const body = wrapper.find("tbody");
let trs = body.findAll("tr");
// check only item count of one page is displayed
expect(trs).toHaveLength(perPage);
// update table data
const newData = [
...random_words.slice(0, 3).map((word) => ({
id: lastId++,
name: word,
})),
...data,
];
await wrapper.setProps({ data: newData });
trs = body.findAll("tr");
// check only item count of one page is displayed
expect(trs).toHaveLength(perPage);
const sortedData = newData.sort((a, b) =>
a.name.localeCompare(b.name),
);
// check default desc sort
for (let idx = 0; idx < perPage; idx++) {
const tds = trs[idx].findAll("td");
expect(tds).toHaveLength(2);
expect(tds[1].text()).toBe(sortedData[idx].name);
}
});
});
describe("test checkable", () => {
test("tests header has checkAll checkbox", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name" },
],
checkable: true,
paginated: false,
data: data,
},
});
await nextTick();
const head = wrapper.find("thead");
expect(head.exists()).toBeTruthy();
const checkboxes = head.findAll("input");
expect(checkboxes).toHaveLength(1);
});
test("tests isAllUncheckable method", async () => {
const isRowCheckable = vi.fn(() => false);
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
checkable: true,
isRowCheckable: isRowCheckable,
paginated: false,
data: [
{
id: 1,
first_name: "Jesse",
last_name: "Simmons",
date: "2016-10-15 13:43:27",
gender: "Male",
},
],
},
});
await nextTick();
const body = wrapper.find("tbody");
const checkboxes = body.findAll("input");
expect(checkboxes).toHaveLength(1);
expect(checkboxes[0].attributes("disabled")).toBe("");
});
test("tests checkAll working correctly", async () => {
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name" },
],
checkable: true,
paginated: false,
data: data,
},
});
await nextTick();
const head = wrapper.find("thead");
expect(head.exists()).toBeTruthy();
const headerCheckboxes = head.findAll("input");
expect(headerCheckboxes).toHaveLength(1);
const checkAllCheckbox = headerCheckboxes[0];
const body = wrapper.find("tbody");
expect(body.exists()).toBeTruthy();
const checkboxes = body.findAll("input");
expect(checkboxes).toHaveLength(data.length);
// assert no row is checked
checkboxes.forEach((checkbox) => {
expect(checkbox.element.checked).toBeFalsy();
});
// check checkAll checkbox
await checkAllCheckbox.setValue(true);
// assert all rows area are checked
checkboxes.forEach((checkbox) => {
expect(checkbox.element.checked).toBeTruthy();
});
// decheck checkAll checkbox again
await checkAllCheckbox.setValue(false);
// assert no row is checked
checkboxes.forEach((checkbox) => {
expect(checkbox.element.checked).toBeFalsy();
});
});
});
describe("test pageable", () => {
test("show correct amount of rows when per page is set to 3", async () => {
const perPage = 3;
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
paginated: true,
data: data,
perPage: perPage,
},
});
await nextTick(); // await child component rendering
const trsPageOne = wrapper.findAll("tbody tr");
expect(trsPageOne).toHaveLength(perPage);
await wrapper.setProps({ currentPage: 2 });
const trsPageTwo = wrapper.findAll("tbody tr");
expect(trsPageTwo).toHaveLength(
Math.min(data.length - perPage, perPage),
);
});
test("show correct amount of rows when per page is set to 5", async () => {
const perPage = 5;
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
paginated: true,
data: data,
perPage: perPage,
},
});
await nextTick(); // await child component rendering
const trsPageOne = wrapper.findAll("tbody tr");
expect(trsPageOne).toHaveLength(perPage);
await wrapper.setProps({ currentPage: 2 });
const trsPageTwo = wrapper.findAll("tbody tr");
expect(trsPageTwo).toHaveLength(
Math.min(data.length - perPage, perPage),
);
});
test("show correct amount of rows when pageable and has filter", async () => {
const data = [
{ id: 1, name: "Jesse" },
{ id: 2, name: "João" },
{ id: 3, name: "Tina" },
{ id: 4, name: "Marco" },
{ id: 5, name: "Hannes" },
{ id: 6, name: "Anne" },
{ id: 7, name: "Clarence" },
];
const perPage = 5;
vi.useFakeTimers(); // use fake timers for debounce
const wrapper = mount(OTable, {
props: {
columns: [
{ label: "ID", field: "id", numeric: true },
{ label: "Name", field: "name", filterable: true },
],
data: data,
paginated: true,
perPage: perPage,
},
});
await nextTick(); // await child component rendering
const body = wrapper.find("tbody");
const trs = body.findAll("tr");
expect(trs).toHaveLength(perPage);
const input = wrapper.find("thead input");
expect(input.exists()).toBeTruthy();
await input.setValue("j");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick(); // await child component rendering
expect(wrapper.findAll("tbody tr")).toHaveLength(2); // Jesse/João
await input.setValue("e");
await input.trigger("input");
vi.runAllTimers(); // run debounce timer
await nextTick(); // await child component rendering
expect(wrapper.findAll("tbody tr")).toHaveLength(4); // Jesse/Anne/Hannes/Clarence
vi.useRealTimers();
});
});
});