Chart.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, { useCallback } from "react";
  14. import { fireEvent, render, renderHook, waitFor } from "@testing-library/react";
  15. import "@testing-library/jest-dom";
  16. import userEvent from "@testing-library/user-event";
  17. import Chart, {
  18. getAxis,
  19. getColNameFromIndexed,
  20. getValue,
  21. getValueFromCol,
  22. TaipyPlotlyButtons,
  23. TraceValueType,
  24. } from "./Chart";
  25. import { TaipyContext } from "../../context/taipyContext";
  26. import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers";
  27. import { ColumnDesc } from "./tableUtils";
  28. import { ModeBarButtonAny } from "plotly.js";
  29. const chartValue = {
  30. default: {
  31. Code: ["AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT", "AUT"],
  32. Day_str: [
  33. "2020-04-01T00:00:00.000000Z",
  34. "2020-04-02T00:00:00.000000Z",
  35. "2020-04-03T00:00:00.000000Z",
  36. "2020-04-04T00:00:00.000000Z",
  37. "2020-04-05T00:00:00.000000Z",
  38. "2020-04-06T00:00:00.000000Z",
  39. "2020-04-07T00:00:00.000000Z",
  40. "2020-04-08T00:00:00.000000Z",
  41. "2020-04-09T00:00:00.000000Z",
  42. "2020-04-10T00:00:00.000000Z",
  43. ],
  44. Entity: [
  45. "Austria",
  46. "Austria",
  47. "Austria",
  48. "Austria",
  49. "Austria",
  50. "Austria",
  51. "Austria",
  52. "Austria",
  53. "Austria",
  54. "Austria",
  55. ],
  56. "Daily hospital occupancy": [856, 823, 829, 826, 712, 824, 857, 829, 820, 771],
  57. },
  58. };
  59. const chartConfig = JSON.stringify({
  60. columns: { Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } },
  61. traces: [["Day_str", "Daily hospital occupancy"]],
  62. xaxis: ["x"],
  63. yaxis: ["y"],
  64. types: ["scatter"],
  65. modes: ["lines+markers"],
  66. axisNames: [],
  67. });
  68. const mapValue = {
  69. default: {
  70. Lat: [
  71. 48.4113, 18.0057, 48.6163, 48.5379, 48.5843, 48.612, 48.6286, 48.6068, 48.4489, 48.6548, 18.5721, 48.3734,
  72. 17.6398, 48.5765, 48.4407, 48.2286,
  73. ],
  74. Lon: [
  75. -112.8352, -65.804, -113.4784, -114.0702, -111.0188, -110.7939, -109.4629, -114.9123, -112.9705, -113.965,
  76. -66.5401, -111.5245, -64.7246, -112.1932, -113.3159, -104.5863,
  77. ],
  78. Globvalue: [
  79. 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,
  80. 0.0983, 0.0992,
  81. ],
  82. },
  83. };
  84. const mapConfig = JSON.stringify({
  85. columns: { Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } },
  86. traces: [["Lat", "Lon"]],
  87. xaxis: ["x"],
  88. yaxis: ["y"],
  89. types: ["scattermap"],
  90. modes: ["markers"],
  91. axisNames: [["lon", "lat"]],
  92. });
  93. const mapLayout = JSON.stringify({
  94. dragmode: "zoom",
  95. map: { style: "open-street-map", center: { lat: 38, lon: -90 }, zoom: 3 },
  96. margin: { r: 0, t: 0, b: 0, l: 0 },
  97. });
  98. interface Props {
  99. figure?: boolean;
  100. }
  101. interface Clickable {
  102. click: (gd: HTMLElement, evt: Event) => void;
  103. }
  104. type DataKey = string;
  105. type Data = Record<DataKey, { tp_index?: number[] }>;
  106. const useGetRealIndex = (data: Data, dataKey: DataKey, props: Props) => {
  107. return useCallback(
  108. (index?: number) =>
  109. typeof index === "number"
  110. ? props.figure
  111. ? index
  112. : data[dataKey] && data[dataKey].tp_index
  113. ? data[dataKey]!.tp_index![index]
  114. : index
  115. : 0,
  116. [data, dataKey, props.figure]
  117. );
  118. };
  119. describe("Chart Component", () => {
  120. it("renders", async () => {
  121. const { getByTestId } = render(<Chart data={chartValue} defaultConfig={chartConfig} testId="test" />);
  122. const elt = getByTestId("test");
  123. expect(elt.tagName).toBe("DIV");
  124. });
  125. it("displays the right info for class", async () => {
  126. const { getByTestId } = render(
  127. <Chart data={chartValue} defaultConfig={chartConfig} testId="test" className="taipy-chart" />
  128. );
  129. const elt = getByTestId("test");
  130. expect(elt).toHaveClass("taipy-chart");
  131. });
  132. it("is disabled", async () => {
  133. const { getByTestId } = render(
  134. <Chart data={chartValue} defaultConfig={chartConfig} testId="test" active={false} />
  135. );
  136. const elt = getByTestId("test");
  137. expect(elt.querySelector(".modebar")).toBeNull();
  138. });
  139. it("is enabled by default", async () => {
  140. const { getByTestId } = render(<Chart data={undefined} defaultConfig={chartConfig} testId="test" />);
  141. const elt = getByTestId("test");
  142. await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
  143. });
  144. it("is enabled by active", async () => {
  145. const { getByTestId } = render(
  146. <Chart data={undefined} defaultConfig={chartConfig} testId="test" active={true} />
  147. );
  148. const elt = getByTestId("test");
  149. await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
  150. });
  151. it("dispatch 2 well formed messages at first render", async () => {
  152. const dispatch = jest.fn();
  153. const state: TaipyState = INITIAL_STATE;
  154. const selProps = { selected0: JSON.stringify([2, 4, 6]) };
  155. render(
  156. <TaipyContext.Provider value={{ state, dispatch }}>
  157. <Chart
  158. id="chart"
  159. data={undefined}
  160. updateVarName="data_var"
  161. defaultConfig={chartConfig}
  162. updateVars="varname=varname"
  163. {...selProps}
  164. />
  165. </TaipyContext.Provider>
  166. );
  167. expect(dispatch).toHaveBeenCalledWith({
  168. name: "",
  169. payload: { id: "chart", names: ["varname"], refresh: false },
  170. type: "REQUEST_UPDATE",
  171. });
  172. expect(dispatch).toHaveBeenCalledWith({
  173. name: "data_var",
  174. payload: {
  175. alldata: true,
  176. pagekey: "Day-Daily hospital occupancy",
  177. columns: ["Day", "Daily hospital occupancy"],
  178. decimatorPayload: undefined,
  179. id: "chart",
  180. },
  181. type: "REQUEST_DATA_UPDATE",
  182. });
  183. });
  184. it("dispatch a well formed message on selection", async () => {
  185. const dispatch = jest.fn();
  186. const state: TaipyState = INITIAL_STATE;
  187. const { getByTestId } = render(
  188. <TaipyContext.Provider value={{ state, dispatch }}>
  189. <Chart data={undefined} updateVarName="data_var" defaultConfig={chartConfig} testId="test" />
  190. </TaipyContext.Provider>
  191. );
  192. const elt = getByTestId("test");
  193. await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
  194. const modebar = elt.querySelector(".modebar");
  195. modebar && (await userEvent.click(modebar));
  196. expect(dispatch).toHaveBeenCalledWith({
  197. name: "data_var",
  198. payload: {
  199. alldata: true,
  200. columns: ["Day", "Daily hospital occupancy"],
  201. decimatorPayload: undefined,
  202. pagekey: "Day-Daily hospital occupancy",
  203. },
  204. type: "REQUEST_DATA_UPDATE",
  205. });
  206. });
  207. xit("dispatch a well formed message on relayout", async () => {
  208. const dispatch = jest.fn();
  209. const state: TaipyState = { ...INITIAL_STATE, data: { table: undefined } };
  210. const { getByLabelText, rerender } = render(
  211. <TaipyContext.Provider value={{ state, dispatch }}>
  212. <Chart
  213. id="table"
  214. updateVarName="data_var"
  215. data={state.data.table as undefined}
  216. defaultConfig={chartConfig}
  217. updateVars="varname=varname"
  218. />
  219. </TaipyContext.Provider>
  220. );
  221. const newState = { ...state, data: { ...state.data, table: chartValue } };
  222. rerender(
  223. <TaipyContext.Provider value={{ state: newState, dispatch }}>
  224. <Chart
  225. id="table"
  226. updateVarName="data_var"
  227. data={newState.data.table as Record<string, TraceValueType>}
  228. defaultConfig={chartConfig}
  229. updateVars="varname=varname"
  230. />
  231. </TaipyContext.Provider>
  232. );
  233. const elt = getByLabelText("Go to next page");
  234. await userEvent.click(elt);
  235. expect(dispatch).toHaveBeenCalledWith({
  236. name: "data_var",
  237. payload: {
  238. columns: ["Entity"],
  239. end: 200,
  240. id: "table",
  241. orderby: "",
  242. pagekey: "100-200--asc",
  243. sort: "asc",
  244. start: 100,
  245. },
  246. type: "REQUEST_DATA_UPDATE",
  247. });
  248. });
  249. xit("displays the received data", async () => {
  250. const { getAllByText, rerender } = render(
  251. <Chart data={undefined} defaultConfig={chartConfig} updateVars="varname=varname" />
  252. );
  253. rerender(<Chart data={chartValue} defaultConfig={chartConfig} updateVars="varname=varname" />);
  254. const elts = getAllByText("Austria");
  255. expect(elts.length).toBeGreaterThan(1);
  256. expect(elts[0].tagName).toBe("TD");
  257. });
  258. it("Chart renders correctly", () => {
  259. const figure = [{ data: [], layout: { title: "Mock Title" } }];
  260. const { getByTestId } = render(
  261. <Chart
  262. id="table"
  263. updateVarName="data_var"
  264. data={undefined}
  265. defaultConfig={chartConfig}
  266. updateVars="varname=varname"
  267. figure={figure}
  268. testId="chart"
  269. />
  270. );
  271. expect(getByTestId("chart")).toBeInTheDocument();
  272. });
  273. it("handles plotConfig prop correctly", () => {
  274. const consoleInfoSpy = jest.spyOn(console, "info");
  275. // Case 1: plotConfig is a valid JSON string
  276. render(<Chart plotConfig='{"autosizable": true}' defaultConfig={chartConfig} />);
  277. expect(consoleInfoSpy).not.toHaveBeenCalled();
  278. // Case 2: plotConfig is not a valid JSON string
  279. render(<Chart plotConfig="not a valid json" defaultConfig={chartConfig} />);
  280. expect(consoleInfoSpy).toHaveBeenCalledWith(
  281. "Error while parsing Chart.plot_config\nUnexpected token 'o', \"not a valid json\" is not valid JSON"
  282. );
  283. // Case 3: plotConfig is not an object
  284. render(<Chart plotConfig='"not an object"' defaultConfig={chartConfig} />);
  285. expect(consoleInfoSpy).toHaveBeenCalledWith("Error Chart.plot_config is not a dictionary");
  286. consoleInfoSpy.mockRestore();
  287. });
  288. it("handles fullscreen toggle correctly", async () => {
  289. // Render the Chart component
  290. render(<Chart plotConfig='{"autosizable": true}' defaultConfig={chartConfig} />);
  291. await waitFor(() => {
  292. const fullscreenButton = document.querySelector(".modebar-btn[data-title='Full screen']");
  293. fireEvent.click(fullscreenButton as Element);
  294. const exitFullscreenButton = document.querySelector(".modebar-btn[data-title='Exit Full screen']");
  295. fireEvent.click(exitFullscreenButton as Element);
  296. const divElement = document.querySelector(".js-plotly-plot");
  297. expect(divElement).toHaveStyle("width: 100%");
  298. });
  299. });
  300. });
  301. describe("Chart Component as Map", () => {
  302. it("renders", async () => {
  303. const { getByTestId } = render(
  304. <Chart data={mapValue} defaultConfig={mapConfig} layout={mapLayout} testId="test" />
  305. );
  306. const elt = getByTestId("test");
  307. await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
  308. });
  309. });
  310. describe("getColNameFromIndexed function", () => {
  311. it("returns the column name when the input string matches the pattern", () => {
  312. const colName = "1/myColumn";
  313. const result = getColNameFromIndexed(colName);
  314. expect(result).toBe("myColumn");
  315. });
  316. it("returns the input string when the input string does not match the pattern", () => {
  317. const colName = "myColumn";
  318. const result = getColNameFromIndexed(colName);
  319. expect(result).toBe("myColumn");
  320. });
  321. it("returns the input string when the input string is empty", () => {
  322. const colName = "";
  323. const result = getColNameFromIndexed(colName);
  324. expect(result).toBe("");
  325. });
  326. });
  327. describe("getValue function", () => {
  328. it("returns the value from column when the input string matches the pattern", () => {
  329. const values: TraceValueType = { myColumn: [1, 2, 3] };
  330. const arr: string[] = ["myColumn"];
  331. const idx = 0;
  332. const result = getValue(values, arr, idx);
  333. expect(result).toEqual([1, 2, 3]);
  334. });
  335. it("returns undefined when returnUndefined is true and value is empty", () => {
  336. const values: TraceValueType = { myColumn: [] };
  337. const arr: string[] = ["myColumn"];
  338. const idx = 0;
  339. const returnUndefined = true;
  340. const result = getValue(values, arr, idx, returnUndefined);
  341. expect(result).toBeUndefined();
  342. });
  343. it("returns empty array when returnUndefined is false and value is empty", () => {
  344. const values: TraceValueType = { myColumn: [] };
  345. const arr: string[] = ["myColumn"];
  346. const idx = 0;
  347. const returnUndefined = false;
  348. const result = getValue(values, arr, idx, returnUndefined);
  349. expect(result).toEqual([]);
  350. });
  351. });
  352. describe("getRealIndex function", () => {
  353. it("should return 0 if index is not a number", () => {
  354. const data = {};
  355. const dataKey = "someKey";
  356. const props = { figure: false };
  357. const { result } = renderHook(() => useGetRealIndex(data, dataKey, props));
  358. const getRealIndex = result.current;
  359. expect(getRealIndex(undefined)).toBe(0);
  360. });
  361. it("should return index if figure is true", () => {
  362. const data = {};
  363. const dataKey = "someKey";
  364. const props = { figure: true };
  365. const { result } = renderHook(() => useGetRealIndex(data, dataKey, props));
  366. const getRealIndex = result.current;
  367. expect(getRealIndex(5)).toBe(5); // index is a number
  368. });
  369. it("should return index if figure is false and tp_index does not exist", () => {
  370. const data = {
  371. someKey: {},
  372. };
  373. const dataKey = "someKey";
  374. const props = { figure: false };
  375. const { result } = renderHook(() => useGetRealIndex(data, dataKey, props));
  376. const getRealIndex = result.current;
  377. expect(getRealIndex(2)).toBe(2); // index is a number
  378. });
  379. });
  380. describe("getValueFromCol function", () => {
  381. it("should return an empty array when values is undefined", () => {
  382. const result = getValueFromCol(undefined, "test");
  383. expect(result).toEqual([]);
  384. });
  385. it("should return an empty array when values[col] is undefined", () => {
  386. const values = { test: [1, 2, 3] };
  387. const result = getValueFromCol(values, "nonexistent");
  388. expect(result).toEqual([]);
  389. });
  390. });
  391. describe("getAxis function", () => {
  392. it("should return undefined when traces length is less than idx", () => {
  393. const traces = [["test"]];
  394. const columns: Record<string, ColumnDesc> = {
  395. test: {
  396. dfid: "test",
  397. type: "testType",
  398. index: 0,
  399. },
  400. };
  401. const result = getAxis(traces, 2, columns, 0);
  402. expect(result).toBeUndefined();
  403. });
  404. it("should return undefined when traces[idx] length is less than axis", () => {
  405. const traces = [["test"]];
  406. const columns: Record<string, ColumnDesc> = {
  407. test: {
  408. dfid: "test",
  409. type: "testType",
  410. index: 0,
  411. },
  412. };
  413. const result = getAxis(traces, 0, columns, 2);
  414. expect(result).toBeUndefined();
  415. });
  416. it("should return undefined when traces[idx][axis] is undefined", () => {
  417. const traces = [["test"]];
  418. const columns: Record<string, ColumnDesc> = {
  419. test: {
  420. dfid: "test",
  421. type: "testType",
  422. index: 0,
  423. },
  424. };
  425. const result = getAxis(traces, 0, columns, 1);
  426. expect(result).toBeUndefined();
  427. });
  428. it("should return undefined when columns[traces[idx][axis]] is undefined", () => {
  429. const traces = [["test"]];
  430. const columns: Record<string, ColumnDesc> = {
  431. test: {
  432. dfid: "test",
  433. type: "testType",
  434. index: 0,
  435. },
  436. };
  437. const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1
  438. expect(result).toBeUndefined();
  439. });
  440. it("should return dfid when all conditions are met", () => {
  441. const traces = [["test"]];
  442. const columns: Record<string, ColumnDesc> = {
  443. test: {
  444. dfid: "test",
  445. type: "testType",
  446. index: 0,
  447. },
  448. };
  449. const result = getAxis(traces, 0, columns, 0);
  450. expect(result).toBe("test");
  451. });
  452. });
  453. describe("click function", () => {
  454. it("should return when div is not found", () => {
  455. // Create a mock HTMLElement without 'div.svg-container'
  456. const mockElement = document.createElement("div");
  457. // Create a mock Event
  458. const mockEvent = new Event("click");
  459. // Call the click function with the mock HTMLElement and Event
  460. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  461. // Since there's no 'div.svg-container', the function should return without making any changes
  462. // We can check this by verifying that no 'full-screen' class was added
  463. expect(mockElement.classList.contains("full-screen")).toBe(false);
  464. });
  465. it("should set data-height when data-height is not set", () => {
  466. // Create a mock HTMLElement
  467. const mockElement = document.createElement("div");
  468. // Create a mock div with class 'svg-container' and append it to the mockElement
  469. const mockDiv = document.createElement("div");
  470. mockDiv.className = "svg-container";
  471. mockElement.appendChild(mockDiv);
  472. // Create a mock Event
  473. const mockEvent = {
  474. ...new Event("click"),
  475. currentTarget: document.createElement("div"),
  476. };
  477. // Call the click function with the mock HTMLElement and Event
  478. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  479. // Check that the 'data-height' attribute was set
  480. expect(mockElement.getAttribute("data-height")).not.toBeNull();
  481. });
  482. it("should set data-title attribute", () => {
  483. // Create a mock HTMLElement
  484. const mockElement = document.createElement("div");
  485. // Create a mock div with class 'svg-container' and append it to the mockElement
  486. const mockDiv = document.createElement("div");
  487. mockDiv.className = "svg-container";
  488. mockElement.appendChild(mockDiv);
  489. // Create a mock Event with a mock currentTarget
  490. const mockEvent = {
  491. ...new Event("click"),
  492. currentTarget: mockElement,
  493. };
  494. // Call the click function with the mock HTMLElement and Event
  495. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  496. // Check that the 'data-title' attribute was set
  497. expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen");
  498. });
  499. });