Chart.spec.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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 {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: ["scattermapbox"],
  90. modes: ["markers"],
  91. axisNames: [["lon", "lat"]]
  92. });
  93. const mapLayout = JSON.stringify({
  94. dragmode: "zoom",
  95. mapbox: {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 {asFragment} = render(
  261. <Chart
  262. id="table"
  263. updateVarName="data_var"
  264. data={undefined}
  265. defaultConfig={chartConfig}
  266. updateVars="varname=varname"
  267. figure={figure}
  268. />
  269. );
  270. expect(asFragment()).toMatchSnapshot();
  271. });
  272. it("handles plotConfig prop correctly", () => {
  273. const consoleInfoSpy = jest.spyOn(console, "info");
  274. // Case 1: plotConfig is a valid JSON string
  275. render(<Chart plotConfig='{"autosizable": true}' defaultConfig={chartConfig} />);
  276. expect(consoleInfoSpy).not.toHaveBeenCalled();
  277. // Case 2: plotConfig is not a valid JSON string
  278. render(<Chart plotConfig="not a valid json" defaultConfig={chartConfig} />);
  279. expect(consoleInfoSpy).toHaveBeenCalledWith(
  280. "Error while parsing Chart.plot_config\nUnexpected token 'o', \"not a valid json\" is not valid JSON"
  281. );
  282. // Case 3: plotConfig is not an object
  283. render(<Chart plotConfig='"not an object"' defaultConfig={chartConfig} />);
  284. expect(consoleInfoSpy).toHaveBeenCalledWith("Error Chart.plot_config is not a dictionary");
  285. consoleInfoSpy.mockRestore();
  286. });
  287. });
  288. describe("Chart Component as Map", () => {
  289. it("renders", async () => {
  290. const {getByTestId} = render(
  291. <Chart data={mapValue} defaultConfig={mapConfig} layout={mapLayout} testId="test" />
  292. );
  293. const elt = getByTestId("test");
  294. await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
  295. });
  296. });
  297. describe("getColNameFromIndexed function", () => {
  298. it("returns the column name when the input string matches the pattern", () => {
  299. const colName = "1/myColumn";
  300. const result = getColNameFromIndexed(colName);
  301. expect(result).toBe("myColumn");
  302. });
  303. it("returns the input string when the input string does not match the pattern", () => {
  304. const colName = "myColumn";
  305. const result = getColNameFromIndexed(colName);
  306. expect(result).toBe("myColumn");
  307. });
  308. it("returns the input string when the input string is empty", () => {
  309. const colName = "";
  310. const result = getColNameFromIndexed(colName);
  311. expect(result).toBe("");
  312. });
  313. });
  314. describe("getValue function", () => {
  315. it("returns the value from column when the input string matches the pattern", () => {
  316. const values: TraceValueType = {myColumn: [1, 2, 3]};
  317. const arr: string[] = ["myColumn"];
  318. const idx = 0;
  319. const result = getValue(values, arr, idx);
  320. expect(result).toEqual([1, 2, 3]);
  321. });
  322. it("returns undefined when returnUndefined is true and value is empty", () => {
  323. const values: TraceValueType = {myColumn: []};
  324. const arr: string[] = ["myColumn"];
  325. const idx = 0;
  326. const returnUndefined = true;
  327. const result = getValue(values, arr, idx, returnUndefined);
  328. expect(result).toBeUndefined();
  329. });
  330. it("returns empty array when returnUndefined is false and value is empty", () => {
  331. const values: TraceValueType = {myColumn: []};
  332. const arr: string[] = ["myColumn"];
  333. const idx = 0;
  334. const returnUndefined = false;
  335. const result = getValue(values, arr, idx, returnUndefined);
  336. expect(result).toEqual([]);
  337. });
  338. });
  339. describe("getRealIndex function", () => {
  340. it("should return 0 if index is not a number", () => {
  341. const data = {};
  342. const dataKey = "someKey";
  343. const props = {figure: false};
  344. const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
  345. const getRealIndex = result.current;
  346. expect(getRealIndex(undefined)).toBe(0);
  347. });
  348. it("should return index if figure is true", () => {
  349. const data = {};
  350. const dataKey = "someKey";
  351. const props = {figure: true};
  352. const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
  353. const getRealIndex = result.current;
  354. expect(getRealIndex(5)).toBe(5); // index is a number
  355. });
  356. it("should return index if figure is false and tp_index does not exist", () => {
  357. const data = {
  358. someKey: {}
  359. };
  360. const dataKey = "someKey";
  361. const props = {figure: false};
  362. const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
  363. const getRealIndex = result.current;
  364. expect(getRealIndex(2)).toBe(2); // index is a number
  365. });
  366. });
  367. describe("getValueFromCol function", () => {
  368. it("should return an empty array when values is undefined", () => {
  369. const result = getValueFromCol(undefined, "test");
  370. expect(result).toEqual([]);
  371. });
  372. it("should return an empty array when values[col] is undefined", () => {
  373. const values = {test: [1, 2, 3]};
  374. const result = getValueFromCol(values, "nonexistent");
  375. expect(result).toEqual([]);
  376. });
  377. });
  378. describe("getAxis function", () => {
  379. it("should return undefined when traces length is less than idx", () => {
  380. const traces = [["test"]];
  381. const columns: Record<string, ColumnDesc> = {
  382. test: {
  383. dfid: "test",
  384. type: "testType",
  385. index: 0
  386. }
  387. };
  388. const result = getAxis(traces, 2, columns, 0);
  389. expect(result).toBeUndefined();
  390. });
  391. it("should return undefined when traces[idx] length is less than axis", () => {
  392. const traces = [["test"]];
  393. const columns: Record<string, ColumnDesc> = {
  394. test: {
  395. dfid: "test",
  396. type: "testType",
  397. index: 0
  398. }
  399. };
  400. const result = getAxis(traces, 0, columns, 2);
  401. expect(result).toBeUndefined();
  402. });
  403. it("should return undefined when traces[idx][axis] is undefined", () => {
  404. const traces = [["test"]];
  405. const columns: Record<string, ColumnDesc> = {
  406. test: {
  407. dfid: "test",
  408. type: "testType",
  409. index: 0
  410. }
  411. };
  412. const result = getAxis(traces, 0, columns, 1);
  413. expect(result).toBeUndefined();
  414. });
  415. it("should return undefined when columns[traces[idx][axis]] is undefined", () => {
  416. const traces = [["test"]];
  417. const columns: Record<string, ColumnDesc> = {
  418. test: {
  419. dfid: "test",
  420. type: "testType",
  421. index: 0
  422. }
  423. };
  424. const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1
  425. expect(result).toBeUndefined();
  426. });
  427. it("should return dfid when all conditions are met", () => {
  428. const traces = [["test"]];
  429. const columns: Record<string, ColumnDesc> = {
  430. test: {
  431. dfid: "test",
  432. type: "testType",
  433. index: 0
  434. }
  435. };
  436. const result = getAxis(traces, 0, columns, 0);
  437. expect(result).toBe("test");
  438. });
  439. });
  440. describe("click function", () => {
  441. it("should return when div is not found", () => {
  442. // Create a mock HTMLElement without 'div.svg-container'
  443. const mockElement = document.createElement("div");
  444. // Create a mock Event
  445. const mockEvent = new Event("click");
  446. // Call the click function with the mock HTMLElement and Event
  447. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  448. // Since there's no 'div.svg-container', the function should return without making any changes
  449. // We can check this by verifying that no 'full-screen' class was added
  450. expect(mockElement.classList.contains("full-screen")).toBe(false);
  451. });
  452. it("should set data-height when div.svg-container is found but data-height is not set", () => {
  453. // Create a mock HTMLElement
  454. const mockElement = document.createElement("div");
  455. // Create a mock div with class 'svg-container' and append it to the mockElement
  456. const mockDiv = document.createElement("div");
  457. mockDiv.className = "svg-container";
  458. mockElement.appendChild(mockDiv);
  459. // Create a mock Event
  460. const mockEvent = {
  461. ...new Event("click"),
  462. currentTarget: document.createElement("div")
  463. };
  464. // Call the click function with the mock HTMLElement and Event
  465. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  466. // Check that the 'data-height' attribute was set
  467. expect(mockElement.getAttribute("data-height")).not.toBeNull();
  468. });
  469. it("should set data-title attribute", () => {
  470. // Create a mock HTMLElement
  471. const mockElement = document.createElement("div");
  472. // Create a mock div with class 'svg-container' and append it to the mockElement
  473. const mockDiv = document.createElement("div");
  474. mockDiv.className = "svg-container";
  475. mockElement.appendChild(mockDiv);
  476. // Create a mock Event with a mock currentTarget
  477. const mockEvent = {
  478. ...new Event("click"),
  479. currentTarget: mockElement
  480. };
  481. // Call the click function with the mock HTMLElement and Event
  482. (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
  483. // Check that the 'data-title' attribute was set
  484. expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen");
  485. });
  486. });