/* * 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, { useCallback } from "react"; import { fireEvent, render, renderHook, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; import Chart, { getAxis, getColNameFromIndexed, getValue, getValueFromCol, TaipyPlotlyButtons, TraceValueType, } from "./Chart"; import { TaipyContext } from "../../context/taipyContext"; import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers"; import { ColumnDesc } from "./tableUtils"; import { ModeBarButtonAny } from "plotly.js"; const chartValue = { default: { Code: ["AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT"], Day_str: [ "2020-04-01T00:00:00.000000Z", "2020-04-02T00:00:00.000000Z", "2020-04-03T00:00:00.000000Z", "2020-04-04T00:00:00.000000Z", "2020-04-05T00:00:00.000000Z", "2020-04-06T00:00:00.000000Z", "2020-04-07T00:00:00.000000Z", "2020-04-08T00:00:00.000000Z", "2020-04-09T00:00:00.000000Z", "2020-04-10T00:00:00.000000Z", ], Entity: [ "Austria", "Austria", "Austria", "Austria", "Austria", "Austria", "Austria", "Austria", "Austria", "Austria", ], "Daily hospital occupancy": [856, 823, 829, 826, 712, 824, 857, 829, 820, 771], }, }; const chartConfig = JSON.stringify({ columns: { Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } }, traces: [["Day_str", "Daily hospital occupancy"]], xaxis: ["x"], yaxis: ["y"], types: ["scatter"], modes: ["lines+markers"], axisNames: [], }); const mapValue = { default: { Lat: [ 48.4113, 18.0057, 48.6163, 48.5379, 48.5843, 48.612, 48.6286, 48.6068, 48.4489, 48.6548, 18.5721, 48.3734, 17.6398, 48.5765, 48.4407, 48.2286, ], Lon: [ -112.8352, -65.804, -113.4784, -114.0702, -111.0188, -110.7939, -109.4629, -114.9123, -112.9705, -113.965, -66.5401, -111.5245, -64.7246, -112.1932, -113.3159, -104.5863, ], Globvalue: [ 0.0875, 0.0892, 0.0908, 0.0933, 0.0942, 0.095, 0.095, 0.095, 0.0958, 0.0958, 0.0958, 0.0958, 0.0958, 0.0975, 0.0983, 0.0992, ], }, }; const mapConfig = JSON.stringify({ columns: { Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } }, traces: [["Lat", "Lon"]], xaxis: ["x"], yaxis: ["y"], types: ["scattermap"], modes: ["markers"], axisNames: [["lon", "lat"]], }); const mapLayout = JSON.stringify({ dragmode: "zoom", map: { style: "open-street-map", center: { lat: 38, lon: -90 }, zoom: 3 }, margin: { r: 0, t: 0, b: 0, l: 0 }, }); interface Props { figure?: boolean; } interface Clickable { click: (gd: HTMLElement, evt: Event) => void; } type DataKey = string; type Data = Record; const useGetRealIndex = (data: Data, dataKey: DataKey, props: Props) => { return useCallback( (index?: number) => typeof index === "number" ? props.figure ? index : data[dataKey] && data[dataKey].tp_index ? data[dataKey]!.tp_index![index] : index : 0, [data, dataKey, props.figure] ); }; describe("Chart Component", () => { it("renders", async () => { const { getByTestId } = render(); const elt = getByTestId("test"); expect(elt.tagName).toBe("DIV"); }); it("displays the right info for class", async () => { const { getByTestId } = render( ); const elt = getByTestId("test"); expect(elt).toHaveClass("taipy-chart"); }); it("is disabled", async () => { const { getByTestId } = render( ); const elt = getByTestId("test"); expect(elt.querySelector(".modebar")).toBeNull(); }); it("is enabled by default", async () => { const { getByTestId } = render(); const elt = getByTestId("test"); await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); }); it("is enabled by active", async () => { const { getByTestId } = render( ); const elt = getByTestId("test"); await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); }); it("dispatch 2 well formed messages at first render", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const selProps = { selected0: JSON.stringify([2, 4, 6]) }; render( ); expect(dispatch).toHaveBeenCalledWith({ name: "", payload: { id: "chart", names: ["varname"], refresh: false }, type: "REQUEST_UPDATE", }); expect(dispatch).toHaveBeenCalledWith({ name: "data_var", payload: { alldata: true, pagekey: "Day-Daily hospital occupancy", columns: ["Day", "Daily hospital occupancy"], decimatorPayload: undefined, id: "chart", }, type: "REQUEST_DATA_UPDATE", }); }); it("dispatch a well formed message on selection", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByTestId } = render( ); const elt = getByTestId("test"); await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); const modebar = elt.querySelector(".modebar"); modebar && (await userEvent.click(modebar)); expect(dispatch).toHaveBeenCalledWith({ name: "data_var", payload: { alldata: true, columns: ["Day", "Daily hospital occupancy"], decimatorPayload: undefined, pagekey: "Day-Daily hospital occupancy", }, type: "REQUEST_DATA_UPDATE", }); }); xit("dispatch a well formed message on relayout", async () => { const dispatch = jest.fn(); const state: TaipyState = { ...INITIAL_STATE, data: { table: undefined } }; const { getByLabelText, rerender } = render( ); const newState = { ...state, data: { ...state.data, table: chartValue } }; rerender( } defaultConfig={chartConfig} updateVars="varname=varname" /> ); const elt = getByLabelText("Go to next page"); await userEvent.click(elt); expect(dispatch).toHaveBeenCalledWith({ name: "data_var", payload: { columns: ["Entity"], end: 200, id: "table", orderby: "", pagekey: "100-200--asc", sort: "asc", start: 100, }, type: "REQUEST_DATA_UPDATE", }); }); xit("displays the received data", async () => { const { getAllByText, rerender } = render( ); rerender(); const elts = getAllByText("Austria"); expect(elts.length).toBeGreaterThan(1); expect(elts[0].tagName).toBe("TD"); }); it("Chart renders correctly", () => { const figure = [{ data: [], layout: { title: "Mock Title" } }]; const { getByTestId } = render( ); expect(getByTestId("chart")).toBeInTheDocument(); }); it("handles plotConfig prop correctly", () => { const consoleInfoSpy = jest.spyOn(console, "info"); // Case 1: plotConfig is a valid JSON string render(); expect(consoleInfoSpy).not.toHaveBeenCalled(); // Case 2: plotConfig is not a valid JSON string render(); expect(consoleInfoSpy).toHaveBeenCalledWith( "Error while parsing Chart.plot_config\nUnexpected token 'o', \"not a valid json\" is not valid JSON" ); // Case 3: plotConfig is not an object render(); expect(consoleInfoSpy).toHaveBeenCalledWith("Error Chart.plot_config is not a dictionary"); consoleInfoSpy.mockRestore(); }); it("handles fullscreen toggle correctly", async () => { // Render the Chart component render(); await waitFor(() => { const fullscreenButton = document.querySelector(".modebar-btn[data-title='Full screen']"); fireEvent.click(fullscreenButton as Element); const exitFullscreenButton = document.querySelector(".modebar-btn[data-title='Exit Full screen']"); fireEvent.click(exitFullscreenButton as Element); const divElement = document.querySelector(".js-plotly-plot"); expect(divElement).toHaveStyle("width: 100%"); }); }); }); describe("Chart Component as Map", () => { it("renders", async () => { const { getByTestId } = render( ); const elt = getByTestId("test"); await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); }); }); describe("getColNameFromIndexed function", () => { it("returns the column name when the input string matches the pattern", () => { const colName = "1/myColumn"; const result = getColNameFromIndexed(colName); expect(result).toBe("myColumn"); }); it("returns the input string when the input string does not match the pattern", () => { const colName = "myColumn"; const result = getColNameFromIndexed(colName); expect(result).toBe("myColumn"); }); it("returns the input string when the input string is empty", () => { const colName = ""; const result = getColNameFromIndexed(colName); expect(result).toBe(""); }); }); describe("getValue function", () => { it("returns the value from column when the input string matches the pattern", () => { const values: TraceValueType = { myColumn: [1, 2, 3] }; const arr: string[] = ["myColumn"]; const idx = 0; const result = getValue(values, arr, idx); expect(result).toEqual([1, 2, 3]); }); it("returns undefined when returnUndefined is true and value is empty", () => { const values: TraceValueType = { myColumn: [] }; const arr: string[] = ["myColumn"]; const idx = 0; const returnUndefined = true; const result = getValue(values, arr, idx, returnUndefined); expect(result).toBeUndefined(); }); it("returns empty array when returnUndefined is false and value is empty", () => { const values: TraceValueType = { myColumn: [] }; const arr: string[] = ["myColumn"]; const idx = 0; const returnUndefined = false; const result = getValue(values, arr, idx, returnUndefined); expect(result).toEqual([]); }); }); describe("getRealIndex function", () => { it("should return 0 if index is not a number", () => { const data = {}; const dataKey = "someKey"; const props = { figure: false }; const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); const getRealIndex = result.current; expect(getRealIndex(undefined)).toBe(0); }); it("should return index if figure is true", () => { const data = {}; const dataKey = "someKey"; const props = { figure: true }; const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); const getRealIndex = result.current; expect(getRealIndex(5)).toBe(5); // index is a number }); it("should return index if figure is false and tp_index does not exist", () => { const data = { someKey: {}, }; const dataKey = "someKey"; const props = { figure: false }; const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); const getRealIndex = result.current; expect(getRealIndex(2)).toBe(2); // index is a number }); }); describe("getValueFromCol function", () => { it("should return an empty array when values is undefined", () => { const result = getValueFromCol(undefined, "test"); expect(result).toEqual([]); }); it("should return an empty array when values[col] is undefined", () => { const values = { test: [1, 2, 3] }; const result = getValueFromCol(values, "nonexistent"); expect(result).toEqual([]); }); }); describe("getAxis function", () => { it("should return undefined when traces length is less than idx", () => { const traces = [["test"]]; const columns: Record = { test: { dfid: "test", type: "testType", index: 0, }, }; const result = getAxis(traces, 2, columns, 0); expect(result).toBeUndefined(); }); it("should return undefined when traces[idx] length is less than axis", () => { const traces = [["test"]]; const columns: Record = { test: { dfid: "test", type: "testType", index: 0, }, }; const result = getAxis(traces, 0, columns, 2); expect(result).toBeUndefined(); }); it("should return undefined when traces[idx][axis] is undefined", () => { const traces = [["test"]]; const columns: Record = { test: { dfid: "test", type: "testType", index: 0, }, }; const result = getAxis(traces, 0, columns, 1); expect(result).toBeUndefined(); }); it("should return undefined when columns[traces[idx][axis]] is undefined", () => { const traces = [["test"]]; const columns: Record = { test: { dfid: "test", type: "testType", index: 0, }, }; const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1 expect(result).toBeUndefined(); }); it("should return dfid when all conditions are met", () => { const traces = [["test"]]; const columns: Record = { test: { dfid: "test", type: "testType", index: 0, }, }; const result = getAxis(traces, 0, columns, 0); expect(result).toBe("test"); }); }); describe("click function", () => { it("should return when div is not found", () => { // Create a mock HTMLElement without 'div.svg-container' const mockElement = document.createElement("div"); // Create a mock Event const mockEvent = new Event("click"); // Call the click function with the mock HTMLElement and Event (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); // Since there's no 'div.svg-container', the function should return without making any changes // We can check this by verifying that no 'full-screen' class was added expect(mockElement.classList.contains("full-screen")).toBe(false); }); it("should set data-height when data-height is not set", () => { // Create a mock HTMLElement const mockElement = document.createElement("div"); // Create a mock div with class 'svg-container' and append it to the mockElement const mockDiv = document.createElement("div"); mockDiv.className = "svg-container"; mockElement.appendChild(mockDiv); // Create a mock Event const mockEvent = { ...new Event("click"), currentTarget: document.createElement("div"), }; // Call the click function with the mock HTMLElement and Event (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); // Check that the 'data-height' attribute was set expect(mockElement.getAttribute("data-height")).not.toBeNull(); }); it("should set data-title attribute", () => { // Create a mock HTMLElement const mockElement = document.createElement("div"); // Create a mock div with class 'svg-container' and append it to the mockElement const mockDiv = document.createElement("div"); mockDiv.className = "svg-container"; mockElement.appendChild(mockDiv); // Create a mock Event with a mock currentTarget const mockEvent = { ...new Event("click"), currentTarget: mockElement, }; // Call the click function with the mock HTMLElement and Event (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); // Check that the 'data-title' attribute was set expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen"); }); });