From 5b2970bd598847caf37f123ac28fd764cce48492 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Fri, 19 Jul 2024 15:41:35 +0700 Subject: [PATCH] initial commit --- .../src/context/taipyReducers.spec.ts | 1059 ++++++++++++++++- .../taipy-gui/src/context/taipyReducers.ts | 46 +- 2 files changed, 1057 insertions(+), 48 deletions(-) diff --git a/frontend/taipy-gui/src/context/taipyReducers.spec.ts b/frontend/taipy-gui/src/context/taipyReducers.spec.ts index 03b6f122dc..c5cdd42405 100644 --- a/frontend/taipy-gui/src/context/taipyReducers.spec.ts +++ b/frontend/taipy-gui/src/context/taipyReducers.spec.ts @@ -12,69 +12,1078 @@ */ import "@testing-library/jest-dom"; +import { + addRows, + AlertMessage, + BlockMessage, + createAckAction, + createAlertAction, + createBlockAction, + createDownloadAction, + createIdAction, + createNavigateAction, + createPartialAction, + createRequestChartUpdateAction, + createRequestDataUpdateAction, + createRequestInfiniteTableUpdateAction, + createRequestTableUpdateAction, + createRequestUpdateAction, + createSendActionNameAction, + createSendUpdateAction, + FileDownloadProps, + getPayload, + getWsMessageListener, + INITIAL_STATE, + initializeWebSocket, + messageToAction, + NamePayload, + NavigateMessage, + retreiveBlockUi, + storeBlockUi, + TaipyBaseAction, + taipyReducer, + Types, +} from "./taipyReducers"; +import { WsMessage } from "./wsUtils"; +import { changeFavicon, getLocalStorageValue, IdMessage } from "./utils"; +import { Socket } from "socket.io-client"; +import { Dispatch } from "react"; +import { parseData } from "../utils/dataFormat"; +import * as wsUtils from "./wsUtils"; -import { taipyReducer, INITIAL_STATE, TaipyBaseAction, createAlertAction, AlertMessage } from "./taipyReducers"; - +jest.mock("./utils", () => ({ + ...jest.requireActual("./utils"), + changeFavicon: jest.fn(), + messageToAction: jest.fn(), + getLocalStorageValue: jest.fn(), +})); +jest.mock("../utils/dataFormat", () => ({ + parseData: jest.fn(), +})); +const sendWsMessageSpy = jest.spyOn(wsUtils, "sendWsMessage"); describe("reducer", () => { it("store socket connected", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SOCKET_CONNECTED"} as TaipyBaseAction).isSocketConnected).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { type: "SOCKET_CONNECTED" } as TaipyBaseAction).isSocketConnected, + ).toBeDefined(); }); it("returns update", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "UPDATE", name: "name", payload: {value: "value"}} as TaipyBaseAction).data.name).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "UPDATE", + name: "name", + payload: { value: "value" }, + } as TaipyBaseAction).data.name, + ).toBeDefined(); }); it("store locations", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_LOCATIONS", payload: {value: {loc: "loc"}}} as TaipyBaseAction).locations).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_LOCATIONS", + payload: { value: { loc: "loc" } }, + } as TaipyBaseAction).locations, + ).toBeDefined(); }); it("set alert", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_ALERT", atype: "i", message: "message", system: "system"} as TaipyBaseAction).alerts).toHaveLength(1); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_ALERT", + atype: "i", + message: "message", + system: "system", + } as TaipyBaseAction).alerts, + ).toHaveLength(1); }); it("set show block", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_BLOCK", action: "action", message: "message"} as TaipyBaseAction).block).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_BLOCK", + action: "action", + message: "message", + } as TaipyBaseAction).block, + ).toBeDefined(); }); it("set hide block", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_BLOCK", action: "action", message: "message", close: true} as TaipyBaseAction).block).toBeUndefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_BLOCK", + action: "action", + message: "message", + close: true, + } as TaipyBaseAction).block, + ).toBeUndefined(); }); it("set navigate", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "NAVIGATE", to: "navigateTo", tab: "_blank"} as TaipyBaseAction).navigateTo).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "NAVIGATE", + to: "navigateTo", + tab: "_blank", + } as TaipyBaseAction).navigateTo, + ).toBeDefined(); }); it("set client id", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "CLIENT_ID", id: "id"} as TaipyBaseAction).id).toBeDefined(); + expect(taipyReducer({ ...INITIAL_STATE }, { type: "CLIENT_ID", id: "id" } as TaipyBaseAction).id).toBeDefined(); }); it("set Acknowledgement", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "ACKNOWLEDGEMENT", id: "id"} as TaipyBaseAction)).toEqual(INITIAL_STATE); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "ACKNOWLEDGEMENT", + id: "id", + } as TaipyBaseAction), + ).toEqual(INITIAL_STATE); }); it("remove Acknowledgement", async () => { - expect(taipyReducer({...INITIAL_STATE, ackList: ["ack"]}, {type: "ACKNOWLEDGEMENT", id: "ack"} as TaipyBaseAction)).toEqual(INITIAL_STATE); + expect( + taipyReducer({ ...INITIAL_STATE, ackList: ["ack"] }, { + type: "ACKNOWLEDGEMENT", + id: "ack", + } as TaipyBaseAction), + ).toEqual(INITIAL_STATE); }); it("set Theme", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_THEME", payload: {value: "dark"}} as TaipyBaseAction).theme).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_THEME", + payload: { value: "dark" }, + } as TaipyBaseAction).theme, + ).toBeDefined(); }); it("set TimeZone", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_TIMEZONE", payload: {timeZone: "tz"}} as TaipyBaseAction).timeZone).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_TIMEZONE", + payload: { timeZone: "tz" }, + } as TaipyBaseAction).timeZone, + ).toBeDefined(); }); it("set default TimeZone", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_TIMEZONE", payload: {}} as TaipyBaseAction).timeZone).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "SET_TIMEZONE", + payload: {}, + } as TaipyBaseAction).timeZone, + ).toBeDefined(); }); it("set Menu", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "SET_MENU", menu: {}} as TaipyBaseAction).menu).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { type: "SET_MENU", menu: {} } as TaipyBaseAction).menu, + ).toBeDefined(); }); it("sets download", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "DOWNLOAD_FILE", content: {}} as TaipyBaseAction).download).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "DOWNLOAD_FILE", + content: {}, + } as TaipyBaseAction).download, + ).toBeDefined(); }); it("resets download", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "DOWNLOAD_FILE"} as TaipyBaseAction).download).toBeUndefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { type: "DOWNLOAD_FILE" } as TaipyBaseAction).download, + ).toBeUndefined(); }); it("sets partial", async () => { - expect(taipyReducer({...INITIAL_STATE}, {type: "PARTIAL", name: "partial", create: true} as TaipyBaseAction).data.partial).toBeDefined(); + expect( + taipyReducer({ ...INITIAL_STATE }, { + type: "PARTIAL", + name: "partial", + create: true, + } as TaipyBaseAction).data.partial, + ).toBeDefined(); }); it("resets partial", async () => { - expect(taipyReducer({...INITIAL_STATE, data: {partial: true}}, {type: "PARTIAL", name: "partial"} as TaipyBaseAction).data.partial).toBeUndefined(); + expect( + taipyReducer({ ...INITIAL_STATE, data: { partial: true } }, { + type: "PARTIAL", + name: "partial", + } as TaipyBaseAction).data.partial, + ).toBeUndefined(); }); it("creates an alert action", () => { - expect(createAlertAction({atype: "I", message: "message"} as AlertMessage).type).toBe("SET_ALERT"); - expect(createAlertAction({atype: "err", message: "message"} as AlertMessage).atype).toBe("error"); - expect(createAlertAction({atype: "Wa", message: "message"} as AlertMessage).atype).toBe("warning"); - expect(createAlertAction({atype: "sUc", message: "message"} as AlertMessage).atype).toBe("success"); - expect(createAlertAction({atype: " ", message: "message"} as AlertMessage).atype).toBe(""); + expect(createAlertAction({ atype: "I", message: "message" } as AlertMessage).type).toBe("SET_ALERT"); + expect(createAlertAction({ atype: "err", message: "message" } as AlertMessage).atype).toBe("error"); + expect(createAlertAction({ atype: "Wa", message: "message" } as AlertMessage).atype).toBe("warning"); + expect(createAlertAction({ atype: "sUc", message: "message" } as AlertMessage).atype).toBe("success"); + expect(createAlertAction({ atype: " ", message: "message" } as AlertMessage).atype).toBe(""); + }); +}); + +describe("storeBlockUi function", () => { + let setItemSpy: jest.MockedFunction<(key: string, value: string) => void>; + beforeEach(() => { + setItemSpy = jest.spyOn(Storage.prototype, "setItem") as jest.MockedFunction< + (key: string, value: string) => void + >; + global.localStorage = { + setItem: setItemSpy, + removeItem: jest.fn(), + getItem: jest.fn(), + clear: jest.fn(), + length: 0, + key: jest.fn(), + }; + }); + afterEach(() => { + setItemSpy.mockRestore(); + }); + it("stores message block in localStorage when document not visible", () => { + Object.defineProperty(document, "visibilityState", { value: "hidden", configurable: true }); + const block: BlockMessage = { + action: "yourAction", + noCancel: false, + close: false, + message: "yourMessage", + }; + storeBlockUi(block)(); + expect(localStorage.setItem).toHaveBeenCalledWith("TaipyBlockUi", JSON.stringify(block)); + }); + it("does not set localStorage when message block is defined and document is 'visible", () => { + Object.defineProperty(document, "visibilityState", { value: "visible", configurable: true }); + const block: BlockMessage = { + action: "yourAction", + noCancel: false, + close: false, + message: "yourMessage", + }; + storeBlockUi(block)(); + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); +}); + +describe("createNavigateAction function", () => { + it("should create a navigate action with the correct properties", () => { + const to = "testTo"; + const params = { testParam: "testValue" }; + const tab = "testTab"; + const force = true; + const action = createNavigateAction(to, params, tab, force); + expect(action.type).toEqual(Types.Navigate); + expect(action.to).toEqual(to); + expect(action.params).toEqual(params); + expect(action.tab).toEqual(tab); + expect(action.force).toEqual(force); + }); +}); + +describe("createRequestUpdateAction function", () => { + it("should create a request update action with the correct properties", () => { + const id = "testId"; + const context = "testContext"; + const names = ["name1", "name2"]; + const forceRefresh = true; + const stateContext = { key: "value" }; + const action = createRequestUpdateAction(id, context, names, forceRefresh, stateContext); + expect(action.type).toEqual(Types.RequestUpdate); + expect(action.context).toEqual(context); + expect(action.payload.id).toEqual(id); + expect(action.payload.names).toEqual(names); + expect(action.payload.refresh).toEqual(forceRefresh); + expect(action.payload.state_context).toEqual(stateContext); + }); +}); + +describe("createRequestDataUpdateAction function", () => { + it("should create a request data update action with the correct properties", () => { + const name = "testName"; + const id = "testId"; + const context = "testContext"; + const columns = ["column1", "column2"]; + const pageKey = "testPageKey"; + const payload = { key: "value" }; + const allData = true; + const library = "testLibrary"; + const action = createRequestDataUpdateAction(name, id, context, columns, pageKey, payload, allData, library); + expect(action.type).toEqual(Types.RequestDataUpdate); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.id).toEqual(id); + expect(action.payload.columns).toEqual(columns); + expect(action.payload.pagekey).toEqual(pageKey); + expect(action.payload.key).toEqual(payload.key); + expect(action.payload.alldata).toEqual(allData); + expect(action.payload.library).toEqual(library); + }); +}); + +describe("createRequestInfiniteTableUpdateAction function", () => { + it("should create a request infinite table update action with the correct properties", () => { + const name = "testName"; + const id = "testId"; + const context = "testContext"; + const columns = ["column1", "column2"]; + const pageKey = "testPageKey"; + const start = 0; + const end = 10; + const orderBy = "testOrderBy"; + const sort = "testSort"; + const aggregates = ["aggregate1", "aggregate2"]; + const applies = { key: "value" }; + const styles = { styleKey: "styleValue" }; + const tooltips = { tooltipKey: "tooltipValue" }; + const handleNan = true; + const compare = "testCompare"; + const compareDatas = "testCompareDatas"; + const stateContext = { stateKey: "stateValue" }; + const reverse = true; + const filters = [ + { + field: "testField", + operator: "testOperator", + value: "testValue", + col: "yourColValue", + action: "yourActionValue", + }, + ]; + const action = createRequestInfiniteTableUpdateAction( + name, + id, + context, + columns, + pageKey, + start, + end, + orderBy, + sort, + aggregates, + applies, + styles, + tooltips, + handleNan, + filters, + compare, + compareDatas, + stateContext, + reverse, + ); + expect(action.type).toEqual(Types.RequestDataUpdate); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.id).toEqual(id); + expect(action.payload.columns).toEqual(columns); + expect(action.payload.pagekey).toEqual(pageKey); + expect(action.payload.start).toEqual(start); + expect(action.payload.end).toEqual(end); + expect(action.payload.orderby).toEqual(orderBy); + expect(action.payload.sort).toEqual(sort); + expect(action.payload.aggregates).toEqual(aggregates); + expect(action.payload.applies).toEqual(applies); + expect(action.payload.styles).toEqual(styles); + expect(action.payload.tooltips).toEqual(tooltips); + expect(action.payload.handlenan).toEqual(handleNan); + expect(action.payload.filters).toEqual(filters); + expect(action.payload.compare).toEqual(compare); + expect(action.payload.compare_datas).toEqual(compareDatas); + expect(action.payload.state_context).toEqual(stateContext); + expect(action.payload.reverse).toEqual(reverse); + }); +}); + +describe("createRequestTableUpdateAction function", () => { + it("should create a request table update action with the correct properties", () => { + const name = "testName"; + const id = "testId"; + const context = "testContext"; + const columns = ["column1", "column2"]; + const pageKey = "testPageKey"; + const start = 0; + const end = 10; + const orderBy = "testOrderBy"; + const sort = "testSort"; + const aggregates = ["aggregate1", "aggregate2"]; + const applies = { key: "value" }; + const styles = { styleKey: "styleValue" }; + const tooltips = { tooltipKey: "tooltipValue" }; + const handleNan = true; + const filters = [ + { field: "testField", operator: "testOperator", value: "testValue", col: "testCol", action: "testAction" }, + ]; + const compare = "testCompare"; + const compareDatas = "testCompareDatas"; + const stateContext = { stateKey: "stateValue" }; + const action = createRequestTableUpdateAction( + name, + id, + context, + columns, + pageKey, + start, + end, + orderBy, + sort, + aggregates, + applies, + styles, + tooltips, + handleNan, + filters, + compare, + compareDatas, + stateContext, + ); + expect(action.type).toEqual(Types.RequestDataUpdate); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.id).toEqual(id); + expect(action.payload.columns).toEqual(columns); + expect(action.payload.pagekey).toEqual(pageKey); + expect(action.payload.start).toEqual(start); + expect(action.payload.end).toEqual(end); + expect(action.payload.orderby).toEqual(orderBy); + expect(action.payload.sort).toEqual(sort); + expect(action.payload.aggregates).toEqual(aggregates); + expect(action.payload.applies).toEqual(applies); + expect(action.payload.styles).toEqual(styles); + expect(action.payload.tooltips).toEqual(tooltips); + expect(action.payload.handlenan).toEqual(handleNan); + expect(action.payload.filters).toEqual(filters); + expect(action.payload.compare).toEqual(compare); + expect(action.payload.compare_datas).toEqual(compareDatas); + expect(action.payload.state_context).toEqual(stateContext); + }); +}); + +describe("createRequestChartUpdateAction function", () => { + it("should create a request chart update action with the correct properties", () => { + const name = "testName"; + const id = "testId"; + const context = "testContext"; + const columns = ["column1", "column2"]; + const pageKey = "testPageKey"; + const decimatorPayload = { key: "value" }; + const action = createRequestChartUpdateAction(name, id, context, columns, pageKey, decimatorPayload); + expect(action.type).toEqual(Types.RequestDataUpdate); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.id).toEqual(id); + expect(action.payload.columns).toEqual(columns); + expect(action.payload.pagekey).toEqual(pageKey); + expect(action.payload.decimatorPayload).toEqual(decimatorPayload); + }); +}); + +describe("createSendActionNameAction function", () => { + it("should create a send action name action with the correct properties", () => { + const name = "testName"; + const context = "testContext"; + const value = { key: "value" }; + const args = ["arg1", "arg2"]; + const action = createSendActionNameAction(name, context, value, ...args); + expect(action.type).toEqual(Types.Action); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.key).toEqual(value.key); + expect(action.payload.args).toEqual(args); + }); + it("should create a send action name action with value as action when value is not an object", () => { + const name = "testName"; + const context = "testContext"; + const value = "testValue"; + const args = ["arg1", "arg2"]; + const action = createSendActionNameAction(name, context, value, ...args); + expect(action.type).toEqual(Types.Action); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.payload.action).toEqual(value); + expect(action.payload.args).toEqual(args); + }); +}); + +describe("getPayload function", () => { + it("should create a payload with the correct properties", () => { + const value = "testValue"; + const onChange = "testOnChange"; + const relName = "testRelName"; + const payload = getPayload(value, onChange, relName); + expect(payload.value).toEqual(value); + expect(payload.on_change).toEqual(onChange); + expect(payload.relvar).toEqual(relName); + }); + it("should create a payload with only value when other parameters are not provided", () => { + const value = "testValue"; + const payload = getPayload(value); + expect(payload.value).toEqual(value); + expect(payload.on_change).toBeUndefined(); + expect(payload.relvar).toBeUndefined(); + }); +}); + +describe("createSendUpdateAction function", () => { + it("should create a send update action with the correct properties", () => { + const name = "testName"; + const value = "testValue"; + const context = "testContext"; + const onChange = "testOnChange"; + const propagate = true; + const relName = "testRelName"; + const action = createSendUpdateAction(name, value, context, onChange, propagate, relName); + expect(action.type).toEqual(Types.SendUpdate); + expect(action.name).toEqual(name); + expect(action.context).toEqual(context); + expect(action.propagate).toEqual(propagate); + expect(action.payload.value).toEqual(value); + expect(action.payload.on_change).toEqual(onChange); + expect(action.payload.relvar).toEqual(relName); + }); +}); + +describe("taipyReducer function", () => { + it("should not change state for SOCKET_CONNECTED action if isSocketConnected is already true", () => { + const action = { type: Types.SocketConnected }; + const initialState = { ...INITIAL_STATE, isSocketConnected: true }; + const newState = taipyReducer(initialState, action); + const expectedState = { ...initialState, isSocketConnected: true }; + expect(newState).toEqual(expectedState); + }); + it("should handle UPDATE action", () => { + const action = { + type: Types.Update, + payload: { + value: { someKey: "someValue" }, + infinite: false, + pagekey: "somePageKey", + }, + name: "someName", + }; + const newState = taipyReducer({ ...INITIAL_STATE }, action); + expect(newState.data[action.name]).toEqual({ [action.payload.pagekey]: action.payload.value }); + }); + it("should handle SET_LOCATIONS action", () => { + const action = { + type: Types.SetLocations, + payload: { value: { location1: "value1", location2: "value2" } }, + }; + const newState = taipyReducer({ ...INITIAL_STATE }, action); + expect(newState.locations).toEqual(action.payload.value); + }); + it("should handle SET_ALERT action", () => { + const action = { + type: Types.SetAlert, + atype: "error", + message: "some error message", + system: true, + duration: 3000, + }; + const newState = taipyReducer({ ...INITIAL_STATE }, action); + expect(newState.alerts).toContainEqual({ + atype: action.atype, + message: action.message, + system: action.system, + duration: action.duration, + }); + }); + it("should handle DELETE_ALERT action", () => { + const initialState = { + ...INITIAL_STATE, + alerts: [ + { atype: "error", message: "First Alert", system: true, duration: 5000 }, + { atype: "warning", message: "Second Alert", system: false, duration: 3000 }, + ], + }; + const action = { type: Types.DeleteAlert }; + const newState = taipyReducer(initialState, action); + expect(newState.alerts).toEqual([{ atype: "warning", message: "Second Alert", system: false, duration: 3000 }]); + }); + it("should not modify state if no alerts are present", () => { + const initialState = { ...INITIAL_STATE, alerts: [] }; + const action = { type: Types.DeleteAlert }; + const newState = taipyReducer(initialState, action); + expect(newState).toEqual(initialState); + }); + it("should handle DELETE_ALERT action", () => { + const initialState = { + ...INITIAL_STATE, + alerts: [ + { + message: "alert1", + atype: "type1", + system: true, + duration: 5000, + }, + { + message: "alert2", + atype: "type2", + system: false, + duration: 3000, + }, + ], + }; + const action = { type: Types.DeleteAlert }; + const newState = taipyReducer(initialState, action); + expect(newState.alerts).toEqual([ + { + message: "alert2", + atype: "type2", + system: false, + duration: 3000, + }, + ]); + }); + it("should handle SET_BLOCK action", () => { + const initialState = { ...INITIAL_STATE, block: undefined }; + const action = { + type: Types.SetBlock, + noCancel: false, + action: "blockAction", + close: false, + message: "blockMessage", + }; + const newState = taipyReducer(initialState, action); + expect(newState.block).toEqual({ + noCancel: false, + action: "blockAction", + close: false, + message: "blockMessage", + }); + }); + it("should handle NAVIGATE action", () => { + const initialState = { + ...INITIAL_STATE, + navigateTo: undefined, + navigateParams: undefined, + navigateTab: undefined, + navigateForce: undefined, + }; + const action = { + type: Types.Navigate, + to: "newLocation", + params: { key: "value" }, + tab: "newTab", + force: true, + }; + const newState = taipyReducer(initialState, action); + expect(newState.navigateTo).toEqual("newLocation"); + expect(newState.navigateParams).toEqual({ key: "value" }); + expect(newState.navigateTab).toEqual("newTab"); + expect(newState.navigateForce).toEqual(true); + }); + it("should handle CLIENT_ID action", () => { + const initialState = { ...INITIAL_STATE, id: "oldId" }; + const action = { type: Types.ClientId, id: "newId" }; + const newState = taipyReducer(initialState, action); + expect(newState.id).toEqual("newId"); + }); + it("should handle ACKNOWLEDGEMENT action", () => { + const initialState = { ...INITIAL_STATE, ackList: ["ack1", "ack2"] }; + const action = { type: Types.Acknowledgement, id: "ack1" }; + const newState = taipyReducer(initialState, action); + expect(newState.ackList).toEqual(["ack2"]); + }); + it("should handle SET_MENU action", () => { + const initialState = { ...INITIAL_STATE, menu: {} }; + const action = { type: Types.SetMenu, menu: { menu1: "item1", menu2: "item2" } }; + const newState = taipyReducer(initialState, action); + expect(newState.menu).toEqual({ menu1: "item1", menu2: "item2" }); + }); + it("should handle DOWNLOAD_FILE action", () => { + const initialState = { ...INITIAL_STATE, download: undefined }; + const action = { type: Types.DownloadFile, content: "fileContent", name: "fileName", onAction: "fileAction" }; + const newState = taipyReducer(initialState, action); + expect(newState.download).toEqual({ content: "fileContent", name: "fileName", onAction: "fileAction" }); + }); + it("should handle PARTIAL action", () => { + const initialState = { ...INITIAL_STATE, data: { test: false } }; + const actionCreate = { + type: Types.Partial, + name: "test", + create: true, + }; + let newState = taipyReducer(initialState, actionCreate); + expect(newState.data.test).toEqual(true); + + const actionDelete = { + type: Types.Partial, + name: "test", + create: false, + }; + newState = taipyReducer(newState, actionDelete); + expect(newState.data.test).toBeUndefined(); + }); + it("should handle MULTIPLE_UPDATE action", () => { + const initialState = { ...INITIAL_STATE, data: { test1: false, test2: false } }; + const action = { + type: Types.MultipleUpdate, + payload: [ + { + name: "test1", + payload: { value: true }, + }, + { + name: "test2", + payload: { value: true }, + }, + ], + }; + const newState = taipyReducer(initialState, action); + expect(newState.data.test1).toEqual(true); + expect(newState.data.test2).toEqual(true); + }); + it("should handle SetTimeZone action with fromBackend true", () => { + const initialState = { ...INITIAL_STATE, timeZone: "oldTimeZone" }; + const action = { type: Types.SetTimeZone, payload: { timeZone: "newTimeZone", fromBackend: true } }; + const newState = taipyReducer(initialState, action); + expect(newState.timeZone).toEqual("newTimeZone"); + }); + it("should handle SetTimeZone action with fromBackend false and localStorage value", () => { + const initialState = { ...INITIAL_STATE, timeZone: "oldTimeZone" }; + const localStorageTimeZone = "localStorageTimeZone"; + localStorage.setItem("timeZone", localStorageTimeZone); + const action = { type: Types.SetTimeZone, payload: { timeZone: "newTimeZone", fromBackend: false } }; + const newState = taipyReducer(initialState, action); + expect(newState.timeZone).toEqual("UTC"); + localStorage.removeItem("timeZone"); + }); + it("should handle SetTimeZone action with fromBackend false and no localStorage value", () => { + const initialState = { ...INITIAL_STATE, timeZone: "oldTimeZone" }; + const action = { type: Types.SetTimeZone, payload: { timeZone: "newTimeZone", fromBackend: false } }; + const newState = taipyReducer(initialState, action); + expect(newState.timeZone).toEqual("UTC"); + }); + it("should handle SetTimeZone action with no change in timeZone", () => { + const initialState = { ...INITIAL_STATE, timeZone: "oldTimeZone" }; + const action = { type: Types.SetTimeZone, payload: { timeZone: "oldTimeZone", fromBackend: true } }; + const newState = taipyReducer(initialState, action); + expect(newState).toEqual(initialState); + }); +}); + +describe("addRows function", () => { + it("should replace existing rows with new rows at the specified start index", () => { + const previousRows = [ + { id: 1, value: "row1" }, + { id: 2, value: "row2" }, + ]; + const newRows = [ + { id: 3, value: "row3" }, + { id: 4, value: "row4" }, + ]; + const start = 1; + const result = addRows(previousRows, newRows, start); + const expected = [ + { id: 1, value: "row1" }, + { id: 3, value: "row3" }, + { id: 4, value: "row4" }, + ]; + expect(result).toEqual(expected); + }); +}); + +describe("retreiveBlockUi function", () => { + it("should retrieve block message from localStorage", () => { + const mockBlockMessage = { action: "testAction", noCancel: false, close: false, message: "testMessage" }; + Storage.prototype.getItem = jest.fn(() => JSON.stringify(mockBlockMessage)); + const result = retreiveBlockUi(); + expect(result).toEqual(mockBlockMessage); + }); + + it("should return an empty object if localStorage is empty", () => { + Storage.prototype.getItem = jest.fn(() => null); + const result = retreiveBlockUi(); + expect(result).toEqual({}); + }); + + it("should return an empty object if localStorage contains invalid JSON", () => { + Storage.prototype.getItem = jest.fn(() => "{ invalid json"); + const result = retreiveBlockUi(); + expect(result).toEqual({}); + }); +}); + +describe("messageToAction function", () => { + it("should handle 'MU' type with payload as an array", () => { + const message: WsMessage = { + type: "MU", + payload: [ + { + name: "test1", + payload: { + value: true, + }, + }, + { + name: "test2", + payload: { + value: true, + }, + }, + ], + name: "someName", + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + }; + const result = messageToAction(message); + const expected = { + type: Types.MultipleUpdate, + payload: [ + { name: "test1", payload: { value: true } }, + { name: "test2", payload: { value: true } }, + ], + }; + expect(result).toEqual(expected); + }); + it("should handle 'U' type", () => { + const message: WsMessage = { + type: "U", + payload: [ + { + name: "test", + payload: { + value: true, + }, + }, + ], + name: "test", + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + }; + const result = messageToAction(message); + const expected = { + type: "UPDATE", + name: "test", + payload: [{ name: "test", payload: { value: true } }], + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + }; + expect(result).toEqual(expected); + }); + it('should call createAlertAction if message type is "AL"', () => { + const message: WsMessage & Partial = { + type: "AL", + atype: "I", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + }; + const result = messageToAction(message); + const expectedResult = createAlertAction(message as unknown as AlertMessage); + expect(result).toEqual(expectedResult); + }); + it('should call createBlockAction if message type is "BL"', () => { + const message: WsMessage & Partial = { + type: "BL", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + }; + const result = messageToAction(message); + const expectedResult = createBlockAction(message as unknown as BlockMessage); + expect(result).toEqual(expectedResult); + }); + it('should call createNavigateAction if message type is "NA"', () => { + const message: WsMessage & Partial = { + type: "NA", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + to: "someDestination", + params: { key: "value" }, + tab: "someTab", + force: true, + }; + const result = messageToAction(message); + const expectedResult = createNavigateAction(message.to, message.params, message.tab, message.force); + expect(result).toEqual(expectedResult); + }); + it('should call createIdAction if message type is "ID"', () => { + const message: WsMessage & Partial = { + type: "ID", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + id: "someId", + }; + const result = messageToAction(message); + if (message.id) { + const expectedResult = createIdAction(message.id); + expect(result).toEqual(expectedResult); + } + }); + it('should call createDownloadAction if message type is "DF"', () => { + const message: WsMessage & Partial = { + type: "DF", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + content: "someContent", + onAction: "someOnAction", + }; + const result = messageToAction(message); + const expectedResult = createDownloadAction(message as unknown as FileDownloadProps); + expect(result).toEqual(expectedResult); + }); + it('should call createPartialAction if message type is "PR"', () => { + const message: WsMessage = { + type: "PR", + name: "someName", + payload: "key", + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + }; + const result = messageToAction(message); + const expectedResult = createPartialAction(message.name, true); + expect(result).toEqual(expectedResult); + }); + it('should call createAckAction if message type is "ACK"', () => { + const message: WsMessage & Partial = { + type: "ACK", + name: "someName", + payload: {}, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + id: "someId", + }; + const result = messageToAction(message); + if (message.id) { + const expectedResult = createAckAction(message.id); + expect(result).toEqual(expectedResult); + } + }); + it('should call changeFavicon if message type is "FV"', () => { + const message: WsMessage = { + type: "FV", + name: "someName", + payload: { + key1: "value1", + }, + propagate: true, + client_id: "someClientId", + module_context: "someModuleContext", + ack_id: "someAckId", + }; + messageToAction(message); + expect(changeFavicon).toHaveBeenCalled(); + }); +}); + +describe("getWsMessageListener function", () => { + it("should handle 'MS' type with payload as an array", () => { + const mockDispatchWsMessage = jest.fn(); + const mockPayloads = [ + { type: "MU", name: "test1", payload: {} }, + { type: "MU", name: "test2", payload: {} }, + { type: "MU", name: "test3", payload: {} }, + ]; + const mockMessage: WsMessage = { + type: "MS", + name: "testName", + payload: mockPayloads, + propagate: true, + client_id: "testClientId", + module_context: "testModuleContext", + }; + const dispatchWsMessage = getWsMessageListener(mockDispatchWsMessage); + dispatchWsMessage(mockMessage); + expect(mockDispatchWsMessage).toHaveBeenCalledTimes(mockPayloads.length); + }); + it("should handle message with payload as NamePayload array", async () => { + const mockDispatch = jest.fn(); + const mockPayloads: NamePayload[] = [ + { name: "test1", payload: { value: "value1" } }, + { name: "test2", payload: { value: "value2" } }, + { name: "test3", payload: { value: "value3" } }, + ]; + const mockMessage: WsMessage = { + type: "MU", + name: "testName", + payload: mockPayloads, + propagate: true, + client_id: "testClientId", + module_context: "testModuleContext", + }; + (parseData as jest.Mock).mockImplementation((value) => Promise.resolve(value)); + + const dispatchWsMessage = getWsMessageListener(mockDispatch); + dispatchWsMessage(mockMessage); + expect(parseData).toHaveBeenCalledTimes(mockPayloads.length); + }); +}); + +describe("initializeWebSocket function", () => { + let mockSocket: jest.Mocked; + const mockDispatch: Dispatch = jest.fn(); + beforeEach(() => { + mockSocket = { + on: jest.fn(), + connect: jest.fn(), + } as unknown as jest.Mocked; + (getLocalStorageValue as jest.Mock).mockReturnValue("mockId"); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + it("should set up event listeners and connect the socket if socket is provided", () => { + initializeWebSocket(mockSocket, mockDispatch); + expect(mockSocket.on).toHaveBeenCalledWith("connect", expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith("connect_error", expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith("disconnect", expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith("message", expect.any(Function)); + expect(mockSocket.connect).toHaveBeenCalled(); + expect(changeFavicon).toHaveBeenCalled(); + }); + it("should dispatch SocketConnected action on connect", () => { + initializeWebSocket(mockSocket, mockDispatch); + + const connectCallback = mockSocket.on.mock.calls.find((call) => call[0] === "connect")?.[1]; + expect(connectCallback).toBeDefined(); + + if (connectCallback) { + connectCallback(); + expect(sendWsMessageSpy).toHaveBeenCalledWith( + mockSocket, + "ID", + "TaipyClientId", + "mockId", + "mockId", + undefined, + false, + expect.any(Function), + ); + } + }); + it("should not throw if socket is undefined", () => { + expect(() => initializeWebSocket(undefined, mockDispatch)).not.toThrow(); + }); + it("should attempt to reconnect on server disconnection", () => { + initializeWebSocket(mockSocket, mockDispatch); + const disconnectCallback = mockSocket.on.mock.calls.find((call) => call[0] === "disconnect")?.[1]; + expect(disconnectCallback).toBeDefined(); + if (disconnectCallback) { + disconnectCallback("io server disconnect"); + expect(mockSocket.connect).toHaveBeenCalled(); + } + }); + it("should attempt to reconnect on connect_error", () => { + jest.useFakeTimers(); + initializeWebSocket(mockSocket, mockDispatch); + const connectErrorCallback = mockSocket.on.mock.calls.find((call) => call[0] === "connect_error")?.[1]; + expect(connectErrorCallback).toBeDefined(); + if (connectErrorCallback) { + connectErrorCallback(); + jest.advanceTimersByTime(500); + expect(mockSocket.connect).toHaveBeenCalled(); + } + jest.useRealTimers(); }); }); diff --git a/frontend/taipy-gui/src/context/taipyReducers.ts b/frontend/taipy-gui/src/context/taipyReducers.ts index 099d6f7aee..2ffee80c45 100644 --- a/frontend/taipy-gui/src/context/taipyReducers.ts +++ b/frontend/taipy-gui/src/context/taipyReducers.ts @@ -25,7 +25,7 @@ import { MenuProps } from "../utils/lov"; import { changeFavicon, getLocalStorageValue, IdMessage, storeClientId } from "./utils"; import { ligthenPayload, sendWsMessage, TAIPY_CLIENT_ID, WsMessage } from "./wsUtils"; -enum Types { +export enum Types { SocketConnected = "SOCKET_CONNECTED", Update = "UPDATE", MultipleUpdate = "MULTIPLE_UPDATE", @@ -80,7 +80,7 @@ export interface TaipyBaseAction { type: Types; } -interface NamePayload { +export interface NamePayload { name: string; payload: Record; } @@ -118,7 +118,7 @@ export interface BlockMessage { interface TaipyBlockAction extends TaipyBaseAction, BlockMessage {} -interface NavigateMessage { +export interface NavigateMessage { to?: string; params?: Record; tab?: string; @@ -173,7 +173,7 @@ const getUserTheme = (mode: PaletteMode) => { }, }, }, - }) + }), ); }; @@ -203,7 +203,7 @@ export const taipyInitialize = (initialState: TaipyState): TaipyState => ({ socket: io("/", { autoConnect: false, path: `${getBaseURL()}socket.io` }), }); -const messageToAction = (message: WsMessage) => { +export const messageToAction = (message: WsMessage) => { if (message.type) { if (message.type === "MU" && Array.isArray(message.payload)) { return createMultipleUpdateAction(message.payload as NamePayload[]); @@ -218,7 +218,7 @@ const messageToAction = (message: WsMessage) => { (message as unknown as NavigateMessage).to, (message as unknown as NavigateMessage).params, (message as unknown as NavigateMessage).tab, - (message as unknown as NavigateMessage).force + (message as unknown as NavigateMessage).force, ); } else if (message.type === "ID") { return createIdAction((message as unknown as IdMessage).id); @@ -235,7 +235,7 @@ const messageToAction = (message: WsMessage) => { return {} as TaipyBaseAction; }; -const getWsMessageListener = (dispatch: Dispatch) => { +export const getWsMessageListener = (dispatch: Dispatch) => { const dispatchWsMessage = (message: WsMessage) => { if (message.type === "MU" && Array.isArray(message.payload)) { const payloads = message.payload as NamePayload[]; @@ -284,13 +284,13 @@ export const initializeWebSocket = (socket: Socket | undefined, dispatch: Dispat } }; -const addRows = (previousRows: Record[], newRows: Record[], start: number) => +export const addRows = (previousRows: Record[], newRows: Record[], start: number) => newRows.reduce((arr, row) => { arr[start++] = row; return arr; }, previousRows.concat([])); -const storeBlockUi = (block?: BlockMessage) => () => { +export const storeBlockUi = (block?: BlockMessage) => () => { if (localStorage) { if (block) { document.visibilityState !== "visible" && localStorage.setItem("TaipyBlockUi", JSON.stringify(block)); @@ -462,7 +462,7 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta action.payload, state.id, action.context, - action.propagate + action.propagate, ); break; case Types.Action: @@ -479,12 +479,12 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta return state; }; -const createUpdateAction = (payload: NamePayload): TaipyAction => ({ +export const createUpdateAction = (payload: NamePayload): TaipyAction => ({ ...payload, type: Types.Update, }); -const createMultipleUpdateAction = (payload: NamePayload[]): TaipyMultipleAction => ({ +export const createMultipleUpdateAction = (payload: NamePayload[]): TaipyMultipleAction => ({ type: Types.MultipleUpdate, payload: payload, }); @@ -512,7 +512,7 @@ export const createSendUpdateAction = ( context: string | undefined, onChange?: string, propagate = true, - relName?: string + relName?: string, ): TaipyAction => ({ type: Types.SendUpdate, name: name, @@ -521,7 +521,7 @@ export const createSendUpdateAction = ( payload: getPayload(value, onChange, relName), }); -const getPayload = (value: unknown, onChange?: string, relName?: string) => { +export const getPayload = (value: unknown, onChange?: string, relName?: string) => { const ret: Record = { value: value }; if (relName) { ret.relvar = relName; @@ -565,7 +565,7 @@ export const createRequestChartUpdateAction = ( context: string | undefined, columns: string[], pageKey: string, - decimatorPayload: unknown | undefined + decimatorPayload: unknown | undefined, ): TaipyAction => createRequestDataUpdateAction( name, @@ -576,7 +576,7 @@ export const createRequestChartUpdateAction = ( { decimatorPayload: decimatorPayload, }, - true + true, ); export const createRequestTableUpdateAction = ( @@ -597,7 +597,7 @@ export const createRequestTableUpdateAction = ( filters?: Array, compare?: string, compareDatas?: string, - stateContext?: Record + stateContext?: Record, ): TaipyAction => createRequestDataUpdateAction( name, @@ -619,7 +619,7 @@ export const createRequestTableUpdateAction = ( compare: compare, compare_datas: compareDatas, state_context: stateContext, - }) + }), ); export const createRequestInfiniteTableUpdateAction = ( @@ -641,7 +641,7 @@ export const createRequestInfiniteTableUpdateAction = ( compare?: string, compareDatas?: string, stateContext?: Record, - reverse?: boolean + reverse?: boolean, ): TaipyAction => createRequestDataUpdateAction( name, @@ -665,7 +665,7 @@ export const createRequestInfiniteTableUpdateAction = ( compare_datas: compareDatas, state_context: stateContext, reverse: !!reverse, - }) + }), ); /** @@ -696,7 +696,7 @@ export const createRequestDataUpdateAction = ( pageKey: string, payload: Record, allData = false, - library?: string + library?: string, ): TaipyAction => { payload = payload || {}; if (id !== undefined) { @@ -734,7 +734,7 @@ export const createRequestUpdateAction = ( context: string | undefined, names: string[], forceRefresh = false, - stateContext?: Record + stateContext?: Record, ): TaipyAction => ({ type: Types.RequestUpdate, name: "", @@ -807,7 +807,7 @@ export const createNavigateAction = ( to?: string, params?: Record, tab?: string, - force?: boolean + force?: boolean, ): TaipyNavigateAction => ({ type: Types.Navigate, to,