/* * Copyright 2021-2024 Avaiga Private Limited * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ import React, { act } from "react"; import { fireEvent, render, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; import PaginatedTable from "./PaginatedTable"; import { TaipyContext } from "../../context/taipyContext"; import { TaipyState, INITIAL_STATE } from "../../context/taipyReducers"; import { TableValueType } from "./tableUtils"; const valueKey = "0-99-Entity,Daily hospital occupancy--asc"; const tableValue = { [valueKey]: { data: [ { Day_str: "2020-04-01T00:00:00.000000Z", "Daily hospital occupancy": 856, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-02T00:00:00.000000Z", "Daily hospital occupancy": 823, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-03T00:00:00.000000Z", "Daily hospital occupancy": 829, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-04T00:00:00.000000Z", "Daily hospital occupancy": 826, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-05T00:00:00.000000Z", "Daily hospital occupancy": 712, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-06T00:00:00.000000Z", "Daily hospital occupancy": 824, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-07T00:00:00.000000Z", "Daily hospital occupancy": 857, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-08T00:00:00.000000Z", "Daily hospital occupancy": 829, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-09T00:00:00.000000Z", "Daily hospital occupancy": 820, Entity: "Austria", Code: "AUT", }, { Day_str: "2020-04-10T00:00:00.000000Z", "Daily hospital occupancy": 771, Entity: "Austria", Code: "AUT", }, ], rowcount: 14477, start: 0, }, }; const tableColumns = JSON.stringify({ Entity: { dfid: "Entity" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy", type: "int64" }, }); const changedValue = { [valueKey]: { data: [ { Day_str: "2020-04-01T00:00:00.000000Z", "Daily hospital occupancy": 856, Entity: "Australia", Code: "AUS", }, ], rowcount: 1, start: 0, }, }; const editableValue = { "0--1-bool,int,float,Code--asc": { data: [ { bool: true, int: 856, float: 1.5, Code: "AUT", }, { bool: false, int: 823, float: 2.5, Code: "ZZZ", }, ], rowcount: 2, start: 0, }, }; const editableColumns = JSON.stringify({ bool: { dfid: "bool", type: "bool", index: 0 }, int: { dfid: "int", type: "int", index: 1 }, float: { dfid: "float", type: "float", index: 2 }, Code: { dfid: "Code", type: "str", index: 3 }, }); const buttonValue = { "0--1-bool,int,float,Code--asc": { data: [ { bool: true, int: 856, float: 1.5, Code: "[Button Label](button action)", }, { bool: false, int: 823, float: 2.5, Code: "ZZZ", }, ], rowcount: 2, start: 0, }, }; const buttonColumns = JSON.stringify({ bool: { dfid: "bool", type: "bool", index: 0 }, int: { dfid: "int", type: "int", index: 1 }, float: { dfid: "float", type: "float", index: 2 }, Code: { dfid: "Code", type: "str", index: 3 }, }); const styledColumns = JSON.stringify({ Entity: { dfid: "Entity" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy", type: "int64", style: "some style function", tooltip: "some tooltip", }, }); describe("PaginatedTable Component", () => { it("renders", async () => { const { getByText } = render(); const elt = getByText("Entity"); expect(elt.tagName).toBe("DIV"); }); it("displays the right info for class", async () => { const { getByText } = render( ); const elt = getByText("Entity").closest("table"); expect(elt?.parentElement?.parentElement?.parentElement).toHaveClass("taipy-table", "taipy-table-paginated"); }); it("is disabled", async () => { const { getByText } = render(); const elt = getByText("Entity"); expect(elt.parentElement).toHaveClass("Mui-disabled"); }); it("is enabled by default", async () => { const { getByText } = render(); const elt = getByText("Entity"); expect(elt.parentElement).not.toHaveClass("Mui-disabled"); }); it("is enabled by active", async () => { const { getByText, getAllByTestId } = render( ); const elt = getByText("Entity"); expect(elt.parentElement).not.toHaveClass("Mui-disabled"); expect(getAllByTestId("ArrowDownwardIcon").length).toBeGreaterThan(0); }); it("Hides sort icons when not active", async () => { const { queryByTestId } = render( ); expect(queryByTestId("ArrowDownwardIcon")).toBeNull(); }); it("dispatch 2 well formed messages at first render", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; render( ); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { id: "table", names: ["varname"], refresh: false }, type: "REQUEST_UPDATE", }); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { columns: ["Entity", "Daily hospital occupancy"], end: 99, id: "table", orderby: "", pagekey: valueKey, handlenan: false, sort: "asc", start: 0, aggregates: [], applies: undefined, styles: undefined, tooltips: undefined, filters: [], }, type: "REQUEST_DATA_UPDATE", }); }); it("dispatch a well formed message on sort", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByText } = render( ); const elt = getByText("Entity"); await userEvent.click(elt); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { columns: ["Entity", "Daily hospital occupancy"], end: 99, id: undefined, orderby: "Entity", pagekey: "0-99-Entity,Daily hospital occupancy-Entity-asc", handlenan: false, sort: "asc", start: 0, aggregates: [], applies: undefined, styles: undefined, tooltips: undefined, filters: [], }, type: "REQUEST_DATA_UPDATE", }); }); it("dispatch a well formed message on page change", async () => { const dispatch = jest.fn(); const state: TaipyState = { ...INITIAL_STATE, data: { table: undefined } }; const { getByLabelText, rerender } = render( ); const newState = { ...state, data: { ...state.data, table: tableValue } }; rerender( ); const elt = getByLabelText("Go to next page"); await userEvent.click(elt); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { columns: ["Entity", "Daily hospital occupancy"], end: 199, id: "table", orderby: "", pagekey: "100-199-Entity,Daily hospital occupancy--asc", handlenan: false, sort: "asc", start: 100, aggregates: [], applies: undefined, styles: undefined, tooltips: undefined, filters: [], }, type: "REQUEST_DATA_UPDATE", }); }); it("displays the received data", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getAllByText, rerender } = render( ); rerender( ); const elts = getAllByText("Austria"); expect(elts.length).toBeGreaterThan(1); expect(elts[0].tagName).toBe("SPAN"); }); it("displays the refreshed data", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getAllByText, rerender, queryByText } = render( ); rerender( ); expect(getAllByText("Austria").length).toBeGreaterThan(1); rerender( ); expect(queryByText("Austria")).toBeNull(); expect(getAllByText("Australia").length).toBe(1); }); it("selects the rows", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const selected = [2, 4, 6]; const { findAllByText, rerender } = render( ); rerender( ); const elts = await waitFor(() => findAllByText("Austria")); elts.forEach((elt: HTMLElement, idx: number) => selected.indexOf(idx) == -1 ? expect(elt.parentElement?.parentElement?.parentElement?.parentElement).not.toHaveClass("Mui-selected") : expect(elt.parentElement?.parentElement?.parentElement?.parentElement).toHaveClass("Mui-selected") ); expect(document.querySelectorAll(".Mui-selected")).toHaveLength(selected.length); }); describe("Edit Mode", () => { it("displays the data with edit buttons", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getAllByTestId, queryAllByTestId, rerender } = render( ); rerender( ); expect(document.querySelectorAll(".MuiSwitch-root")).not.toHaveLength(0); expect(getAllByTestId("EditIcon")).not.toHaveLength(0); expect(queryAllByTestId("CheckIcon")).toHaveLength(0); expect(queryAllByTestId("ClearIcon")).toHaveLength(0); }); it("can edit", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByTestId, queryAllByTestId, getAllByTestId, rerender } = render( ); rerender( ); const edits = getAllByTestId("EditIcon"); await userEvent.click(edits[0]); const checkButton = getByTestId("CheckIcon"); getByTestId("ClearIcon"); await userEvent.click(checkButton); expect(queryAllByTestId("CheckIcon")).toHaveLength(0); expect(queryAllByTestId("ClearIcon")).toHaveLength(0); await userEvent.click(edits[1]); const clearButton = getByTestId("ClearIcon"); const input = document.querySelector("input"); expect(input).not.toBeNull(); if (input) { if (input.type == "checkbox") { await userEvent.click(input); } else { await userEvent.type(input, "1"); } } await userEvent.click(clearButton); expect(queryAllByTestId("CheckIcon")).toHaveLength(0); expect(queryAllByTestId("ClearIcon")).toHaveLength(0); dispatch.mockClear(); await userEvent.click(edits[2]); await userEvent.click(getByTestId("CheckIcon")); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onEdit", args: [], col: "float", index: 0, user_value: 1.5, value: 1.5, }, type: "SEND_ACTION_ACTION", }); await userEvent.click(edits[3]); const input2 = document.querySelector("input"); expect(input2).not.toBeNull(); if (input2) { if (input2.type == "checkbox") { await userEvent.click(input2); await userEvent.click(getByTestId("ClearIcon")); } else { await userEvent.type(input2, "{Esc}"); } } dispatch.mockClear(); await userEvent.click(edits[5]); const input3 = document.querySelector("input"); expect(input3).not.toBeNull(); if (input3) { if (input3.type == "checkbox") { await userEvent.click(input3); await userEvent.click(getByTestId("CheckIcon")); } else { await userEvent.type(input3, "{Enter}"); } } expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onEdit", args: [], col: "int", index: 1, user_value: 823, value: 823, }, type: "SEND_ACTION_ACTION", }); }); }); it("can add", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByTestId } = render( ); dispatch.mockClear(); const addButton = getByTestId("AddIcon"); await userEvent.click(addButton); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onAdd", args: [], index: 0, }, type: "SEND_ACTION_ACTION", }); }); it("can delete", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getAllByTestId, getByTestId, queryAllByTestId, rerender } = render( ); rerender( ); let deleteButtons = getAllByTestId("DeleteIcon"); expect(deleteButtons).not.toHaveLength(0); await userEvent.click(deleteButtons[0]); const checkButton = getByTestId("CheckIcon"); getByTestId("ClearIcon"); await userEvent.click(checkButton); expect(queryAllByTestId("CheckIcon")).toHaveLength(0); expect(queryAllByTestId("ClearIcon")).toHaveLength(0); await userEvent.click(deleteButtons[1]); const clearButton = getByTestId("ClearIcon"); const input = document.querySelector("input"); expect(input).not.toBeNull(); input && (await userEvent.type(input, "1")); await userEvent.click(clearButton); expect(queryAllByTestId("CheckIcon")).toHaveLength(0); expect(queryAllByTestId("ClearIcon")).toHaveLength(0); await userEvent.click(deleteButtons[2]); const input3 = document.querySelector("input"); expect(input3).not.toBeNull(); input3 && (await userEvent.type(input3, "{esc}")); deleteButtons = getAllByTestId("DeleteIcon"); dispatch.mockClear(); await userEvent.click(deleteButtons[0]); const input2 = document.querySelector("input"); expect(input2).not.toBeNull(); input2 && (await userEvent.type(input2, "{enter}")); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onDelete", args: [], index: 0, }, type: "SEND_ACTION_ACTION", }); }); it("can select", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByText, rerender } = render( ); rerender( ); dispatch.mockClear(); const elt = getByText("823"); await userEvent.click(elt); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onSelect", args: [], col: "int", index: 1, reason: "click", value: undefined, }, type: "SEND_ACTION_ACTION", }); }); it("can click on button", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByText, rerender } = render( ); rerender( ); dispatch.mockClear(); const elt = getByText("Button Label"); expect(elt.tagName).toBe("BUTTON"); await userEvent.click(elt); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { action: "onSelect", args: [], col: "Code", index: 0, reason: "button", value: "button action", }, type: "SEND_ACTION_ACTION", }); }); it("should render correctly when style is applied to columns", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; await waitFor(() => { render( ); }); const elt = document.querySelector('table[aria-labelledby="tableTitle"]'); expect(elt).toBeInTheDocument(); }); it("should sort the table in ascending order", async () => { await waitFor(() => { render(); }); const elt = document.querySelector('svg[data-testid="ArrowDownwardIcon"]'); act(() => { fireEvent.click(elt as Element); }); expect(document.querySelector('th[aria-sort="ascending"]')).toBeInTheDocument(); }); it("should handle rows per page change", async () => { const { getByRole, queryByRole } = render(); const rowsPerPageDropdown = getByRole("combobox"); fireEvent.mouseDown(rowsPerPageDropdown); const option = queryByRole("option", { selected: false, name: "50" }); fireEvent.click(option as Element); const table = document.querySelector( 'table[aria-labelledby="tableTitle"].MuiTable-root.MuiTable-stickyHeader.css-cz602z-MuiTable-root' ); expect(table).toBeInTheDocument(); }); it("should allow all rows", async () => { const { getByRole, queryByRole } = render( ); const rowsPerPageDropdown = getByRole("combobox"); fireEvent.mouseDown(rowsPerPageDropdown); const option = queryByRole("option", { selected: false, name: "All" }); expect(option).toBeInTheDocument(); }); it("should display row per page correctly", async () => { const { getByRole, queryByRole } = render( ); const rowsPerPageDropdown = getByRole("combobox"); fireEvent.mouseDown(rowsPerPageDropdown); const option = queryByRole("option", { selected: false, name: "10" }); expect(option).toBeInTheDocument(); }); it("logs error when pageSizeOptions prop is invalid", () => { // Create a spy on console.log const logSpy = jest.spyOn(console, "log"); // Render the component with invalid pageSizeOptions prop render(); // Check if console.log was called with the expected arguments expect(logSpy).toHaveBeenCalledWith( "PaginatedTable pageSizeOptions is wrong ", "not a valid json", expect.any(Error) ); // Clean up the spy logSpy.mockRestore(); }); });