diff --git a/packages/jv-scheduler/jest.config.ts b/packages/jv-scheduler/jest.config.ts index 22b1e5b0..11728256 100644 --- a/packages/jv-scheduler/jest.config.ts +++ b/packages/jv-scheduler/jest.config.ts @@ -5,10 +5,7 @@ const conf: Config = { watchPathIgnorePatterns: ["node_modules", "dist", "coverage", "build"], testEnvironment: "jsdom", transform: { - "^.+\\.([tj]s|[tj]sx)$": "ts-jest", - }, - moduleNameMapper: { - "\\.(css|less)$": "/styleMock.js", + "^.+\\.tsx?$": "ts-jest", }, }; diff --git a/packages/jv-scheduler/package.json b/packages/jv-scheduler/package.json index 814b3fa8..39e3f2d5 100644 --- a/packages/jv-scheduler/package.json +++ b/packages/jv-scheduler/package.json @@ -13,7 +13,8 @@ "scripts": { "audit:all": "yarn audit", "audit:runtime": "yarn audit --groups dependencies", - "test": "jest", + "test": "jest -- action.test.ts", + "coverage": "jest --coverage -- action.test.ts", "build": "vite build", "clean": "rm -rf ./dist" }, @@ -44,6 +45,7 @@ "@testing-library/react": "^13.3.0", "@types/jest": "^28.1.6", "@types/react": "^18.0.15", + "@types/redux-mock-store": "^1.5.0", "@types/xregexp": "^4.4.0", "@vitejs/plugin-react": "^4.3.1", "i18next": "^23.11.5", @@ -54,6 +56,7 @@ "jest-css-modules-transform": "^4.4.2", "jest-environment-jsdom": "^29.7.0", "path": "^0.12.7", + "redux-mock-store": "^1.5.5", "ts-jest": "^29.1.5", "ts-node": "^10.9.2", "typescript": "^4.7.4", diff --git a/packages/jv-scheduler/src/components/Stepper/DefaultData.tsx b/packages/jv-scheduler/src/components/Stepper/DefaultData.tsx index a0d5c0ed..2c024db5 100644 --- a/packages/jv-scheduler/src/components/Stepper/DefaultData.tsx +++ b/packages/jv-scheduler/src/components/Stepper/DefaultData.tsx @@ -109,7 +109,7 @@ export const OutputStepDefaultMessage = () => { ), timezones = useSelector((state: IState) => state.userTimeZones); - const currentTimezone = timezones.filter( + const currentTimezone = timezones?.filter( (item: { code: string }) => item.code === outputTimezone, ), OutputTimeZone = `${currentTimezone[0]?.code} - ${currentTimezone[0]?.description}`; diff --git a/packages/jv-scheduler/src/components/Stepper/StepIcon.tsx b/packages/jv-scheduler/src/components/Stepper/StepIcon.tsx index a32d6f31..4b099049 100644 --- a/packages/jv-scheduler/src/components/Stepper/StepIcon.tsx +++ b/packages/jv-scheduler/src/components/Stepper/StepIcon.tsx @@ -19,18 +19,24 @@ type CommonIconProps = { dataName: string; }; -const CommonIcon = ({ icon, className, dataName }: CommonIconProps) => { +const CommonIcon = ({ + icon, + className, + dataName, + ...rest +}: CommonIconProps) => { return ( ); }; export const StepIcon = (props: any) => { - const { icon } = props; + const { icon, ...rest } = props; return (
{icon === SUCCESS_STATE && ( @@ -38,6 +44,7 @@ export const StepIcon = (props: any) => { icon="checkmarkRound" className="jv-uColor-success" dataName="" + {...rest} /> )} {icon === ERROR_STATE && ( @@ -45,6 +52,7 @@ export const StepIcon = (props: any) => { icon="warningRound" className="jv-uColor-error" dataName="" + {...rest} /> )} {icon === INCOMPLETE_DEFAULT_STATE && ( @@ -52,6 +60,7 @@ export const StepIcon = (props: any) => { icon="checkmarkRound" className="jv-uColor-incomplete" dataName="" + {...rest} /> )} {icon === INCOMPLETE_STATE && ( @@ -59,6 +68,7 @@ export const StepIcon = (props: any) => { icon="circleSolid" className="jv-uColor-incomplete" dataName="" + {...rest} /> )}
diff --git a/packages/jv-scheduler/src/components/Tabs/TabsContent/Output.tsx b/packages/jv-scheduler/src/components/Tabs/TabsContent/Output.tsx index 1a1308f2..aa7282bd 100644 --- a/packages/jv-scheduler/src/components/Tabs/TabsContent/Output.tsx +++ b/packages/jv-scheduler/src/components/Tabs/TabsContent/Output.tsx @@ -22,7 +22,7 @@ import { OUTPUT_TAB, OUTPUT_TIME_ZONE, } from "../../../constants/schedulerConstants"; -import { MessageAPIError } from "../../apiFailureError/scheduleAPIError"; +import { MessageAPIError } from "../../apiFailureError/MessageAPIError"; import { IState, translationProps } from "../../../types/scheduleType"; import { updateChangeToStore } from "../../../utils/schedulerUtils"; import i18nScheduler from "../../../i18n"; diff --git a/packages/jv-scheduler/src/components/apiFailureError/scheduleAPIError.tsx b/packages/jv-scheduler/src/components/apiFailureError/MessageAPIError.tsx similarity index 100% rename from packages/jv-scheduler/src/components/apiFailureError/scheduleAPIError.tsx rename to packages/jv-scheduler/src/components/apiFailureError/MessageAPIError.tsx diff --git a/packages/jv-scheduler/src/utils/configurationUtils.ts b/packages/jv-scheduler/src/utils/configurationUtils.ts index 5c1c7b4f..c749cf5a 100644 --- a/packages/jv-scheduler/src/utils/configurationUtils.ts +++ b/packages/jv-scheduler/src/utils/configurationUtils.ts @@ -36,20 +36,20 @@ const checkAvailabilityOfBasicConfig = ( contextPath: string, ) => { const error: { [key: string]: string } = {}; - if (!resourceURI) { + if (!resourceURI || resourceURI.length === 0) { error["resource.uri.missing.configuration"] = "resourceURI is required in the configuration"; - } else if (!server) { + } else if (!server || server.length === 0) { error["server.missing.configuration"] = "server is required in the configuration"; - } else if (!contextPath) { + } else if (!contextPath || contextPath.length === 0) { error["contextPath.missing.configuration"] = "contextPath is required in the configuration"; } return error; }; -const checkRequiredDataForHiddenTabs = (tabName: string, tabData: any) => { +const checkRequiredDataForHiddenTabs = (tabName: string, tabData: any = {}) => { const error: { [key: string]: string } = {}; switch (tabName) { case SCHEDULE_TAB: { diff --git a/packages/jv-scheduler/test/actions/action.test.ts b/packages/jv-scheduler/test/actions/action.test.ts new file mode 100644 index 00000000..7bbbfc0f --- /dev/null +++ b/packages/jv-scheduler/test/actions/action.test.ts @@ -0,0 +1,597 @@ +import configureMockStore from "redux-mock-store"; +import { + setApiFailure, + setOutputFormats, + setUserTimeZones, + setPropertiesDetails, + setSchedulerUIConfig, + setRepositoryFolderData, + setFakeRootData, + setTabsConfig, + setVisitedTab, + setCurrentActiveTab, + setStepperProperties, + setVisibleFields, + scheduleValidationError, + getOutputFormats, + getUserTimeZones, + getFolderData, + getFakeRootData, + setParametersTabConfig, + currentTabValidationError, + allTabValidationError, + createScheduleJob, + parametersTabErrorOrLoading, +} from "../../src/actions/action"; +import { + SET_SCHEDULE_APIS_FAILURE_ERROR, + SET_OUTPUT_FORMATS, + SET_USER_TIME_ZONES, + SET_PROPERTIES_DETAILS, + SET_SCHEDULER_UI_CONFIG, + SET_REPOSITORY_FOLDER_DATA, + SET_FAKE_ROOT, + SET_TABS_CONFIG, + SCHEDULE_ERROR_OCCURRED, + SET_VISITED_TABS, + SET_ACTIVE_TAB, + SET_STEPPER_PROPERTIES, + SET_VISIBLE_FIELDS, + SET_PARAMETERS_TAB_CONFIG, + SET_PARAMETERS_TAB_LOADING, +} from "../../src/constants/actionConstants"; +import { + createDummySchedule, + createSchedule, + getFakeRootDataFromService, + getOutputFormatsFromService, + getRepositoryFolderData, + getUserTimezonesFromService, +} from "../../src/services/schedulerServices"; +import thunk from "redux-thunk"; +import { removeRootFolderPath } from "../../src/utils/treeUtils"; +import { + getErrorsForCurrentTab, + getStateOfCurrentActiveTab, +} from "../../src/utils/schedulerUtils"; +import { allTabs } from "../../src/constants/schedulerConstants"; + +jest.mock("../../src/services/schedulerServices"); +jest.mock("../../src/utils/treeUtils", () => ({ + removeRootFolderPath: jest.fn(), +})); +jest.mock("../../src/utils/schedulerUtils", () => ({ + getErrorsForCurrentTab: jest.fn(), + getStateOfCurrentActiveTab: jest.fn(), +})); + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe("Action Creators", () => { + let store: any; + let handleStateChange: jest.Mock; + let handleCreateScheduleAPI: jest.Mock; + let enableCreateBtn: jest.Mock; + + beforeEach(() => { + store = mockStore({ + currentActiveTab: "tab1", + stepperConfiguration: { show: true }, + schedulerUIConfig: { + events: { scheduleBtnClick: jest.fn() }, + dryRun: false, + }, + scheduleInfo: { + scheduleJobDescription: "Test Description", + scheduleJobName: "Test Name", + outputFormats: { outputFormat: ["pdf"] }, + mailNotification: { resultSendType: "SEND_ATTACHMENT" }, + repositoryDestination: {}, + }, + }); + handleCreateScheduleAPI = jest.fn(); + handleStateChange = jest.fn(); + enableCreateBtn = jest.fn(); + }); + + it("should create an action to set API failure", () => { + const failedApi = { userOutputFormatApiFailure: true }; + const failedApiName = "userOutputFormatApiFailure"; + const expectedAction = { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { failedApi, failedApiName }, + }; + expect(setApiFailure(failedApi, failedApiName)).toEqual(expectedAction); + }); + it("should create an action to set output formats", () => { + const outputFormats = ["PDF", "HTML"]; + const expectedAction = { + type: SET_OUTPUT_FORMATS, + payload: { outputFormats }, + }; + expect(setOutputFormats(outputFormats)).toEqual(expectedAction); + }); + it("should create an action to set user time zones", () => { + const timeZones = ["UTC", "PST"]; + const expectedAction = { + type: SET_USER_TIME_ZONES, + payload: { userTimeZones: timeZones }, + }; + expect(setUserTimeZones(timeZones)).toEqual(expectedAction); + }); + it("should create an action to set properties details", () => { + const scheduleInfo = { someProperty: "someValue" }; + const expectedAction = { + type: SET_PROPERTIES_DETAILS, + payload: { newScheduleInfo: scheduleInfo }, + }; + expect(setPropertiesDetails(scheduleInfo)).toEqual(expectedAction); + }); + it("should create an action to set scheduler UI config", () => { + const schedulerUIConfig = { + resourceURI: "/test", + server: "https://test.com", + contextPath: "test-pro", + }; + const expectedAction = { + type: SET_SCHEDULER_UI_CONFIG, + payload: { schedulerUIConfig }, + }; + expect(setSchedulerUIConfig(schedulerUIConfig)).toEqual(expectedAction); + }); + it("should create an action to set repository folder data", () => { + const folderData = { folder1: "data1", folder2: "data2" }; + const expectedAction = { + type: SET_REPOSITORY_FOLDER_DATA, + payload: { folderData }, + }; + expect(setRepositoryFolderData(folderData)).toEqual(expectedAction); + }); + it("should create an action to set fake root data", () => { + const fakeRootData = { key: "value" }; + const expectedAction = { + type: SET_FAKE_ROOT, + payload: { fakeRoot: fakeRootData }, + }; + expect(setFakeRootData(fakeRootData)).toEqual(expectedAction); + }); + it("should create an action to set tabs configuration", () => { + const tabsConfiguration = { + currentActiveTab: "schedule", + tabsConfiguration: { + tabsToShow: [ + { + key: "schedule", + label: "Schedule", + value: "schedule", + }, + { + key: "parameters", + label: "Parameters", + value: "parameters", + }, + ], + }, + }; + const expectedAction = { + type: SET_TABS_CONFIG, + payload: tabsConfiguration, + }; + expect(setTabsConfig(tabsConfiguration)).toEqual(expectedAction); + }); + it("should create an action to set visited tabs", () => { + const tabs = ["tab1", "tab2"]; + const expectedAction = { + type: SET_VISITED_TABS, + payload: { tabs }, + }; + expect(setVisitedTab(tabs)).toEqual(expectedAction); + }); + + it("should create an action to set the current active tab", () => { + const activeTab = "schedule"; + const expectedAction = { + type: SET_ACTIVE_TAB, + payload: { activeTab }, + }; + expect(setCurrentActiveTab(activeTab)).toEqual(expectedAction); + }); + + it("should create an action to set stepper properties", () => { + const updatedStepperData = { + baseOutputFilename: "testFile", + name: "test", + }; + const expectedAction = { + type: SET_STEPPER_PROPERTIES, + payload: { updatedStepperData }, + }; + expect(setStepperProperties(updatedStepperData)).toEqual(expectedAction); + }); + + it("should create an action to set visible fields", () => { + const fieldsVisibility = { + baseOutputFilename: true, + }; + const expectedAction = { + type: SET_VISIBLE_FIELDS, + payload: { fieldsVisibility }, + }; + expect(setVisibleFields(fieldsVisibility)).toEqual(expectedAction); + }); + + it("should create an action for schedule validation error", () => { + const errors = { folderURI: "someError" }; + const expectedAction = { + type: SCHEDULE_ERROR_OCCURRED, + payload: { errors }, + }; + expect(scheduleValidationError(errors)).toEqual(expectedAction); + }); + it("creates SET_SCHEDULE_APIS_FAILURE_ERROR when fetching output formats fails", async () => { + const error = { error: "Failed to fetch" }; + (getOutputFormatsFromService as jest.Mock).mockResolvedValue(error); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { userOutputFormatApiFailure: true }, + failedApiName: "userOutputFormatApiFailure", + }, + }, + ]; + + await store.dispatch(getOutputFormats() as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_OUTPUT_FORMATS and SET_SCHEDULE_APIS_FAILURE_ERROR when fetching output formats is successful", async () => { + const outputFormats = { dashboard: { outputFormats: ["PDF", "HTML"] } }; + (getOutputFormatsFromService as jest.Mock).mockResolvedValue(outputFormats); + + const expectedActions = [ + { type: SET_OUTPUT_FORMATS, payload: { outputFormats: ["PDF", "HTML"] } }, + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { userOutputFormatApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(getOutputFormats() as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_PROPERTIES_DETAILS and SET_USER_TIME_ZONES when fetching time zones is successful", async () => { + const timezones = [{ code: "UTC" }, { code: "PST" }]; + (getUserTimezonesFromService as jest.Mock).mockResolvedValue(timezones); + + const expectedActions = [ + { + type: SET_PROPERTIES_DETAILS, + payload: { newScheduleInfo: { outputTimeZone: "UTC" } }, + }, + { type: SET_USER_TIME_ZONES, payload: { userTimeZones: timezones } }, + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { userTimezoneApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(getUserTimeZones() as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_PROPERTIES_DETAILS with specified timezone and SET_USER_TIME_ZONES when fetching time zones is successful", async () => { + const timezones = [{ code: "UTC" }, { code: "PST" }]; + (getUserTimezonesFromService as jest.Mock).mockResolvedValue(timezones); + + const expectedActions = [ + { + type: SET_PROPERTIES_DETAILS, + payload: { newScheduleInfo: { outputTimeZone: "PST" } }, + }, + { type: SET_USER_TIME_ZONES, payload: { userTimeZones: timezones } }, + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { userTimezoneApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(getUserTimeZones("PST") as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_SCHEDULE_APIS_FAILURE_ERROR when fetching time zones fails", async () => { + const error = { error: "Failed to fetch" }; + (getUserTimezonesFromService as jest.Mock).mockResolvedValue(error); + + const expectedActions = [ + { + type: SET_PROPERTIES_DETAILS, + payload: { newScheduleInfo: { outputTimeZone: "UTC" } }, + }, + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { userTimezoneApiFailure: true }, + failedApiName: "userTimezoneApiFailure", + }, + }, + ]; + + await store.dispatch(getUserTimeZones("UTC") as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_REPOSITORY_FOLDER_DATA and SET_SCHEDULE_APIS_FAILURE_ERROR when fetching folder data is successful", async () => { + const folderPath = "/test/path"; + const repositoryData = { + resourceLookup: { folder1: "data1", folder2: "data2" }, + }; + (getRepositoryFolderData as jest.Mock).mockResolvedValue(repositoryData); + (removeRootFolderPath as jest.Mock).mockReturnValue(folderPath); + + const expectedActions = [ + { + type: SET_REPOSITORY_FOLDER_DATA, + payload: { + folderData: { [folderPath]: repositoryData.resourceLookup }, + }, + }, + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { treeLoadApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(getFolderData(folderPath) as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_SCHEDULE_APIS_FAILURE_ERROR when fetching folder data fails", async () => { + const folderPath = "/test/path"; + const error = { error: "Failed to fetch" }; + (getRepositoryFolderData as jest.Mock).mockResolvedValue(error); + (removeRootFolderPath as jest.Mock).mockReturnValue(folderPath); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { treeLoadApiFailure: true }, + failedApiName: "treeLoadApiFailure", + }, + }, + ]; + + await store.dispatch(getFolderData(folderPath) as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_FAKE_ROOT and SET_SCHEDULE_APIS_FAILURE_ERROR when fetching fake root data is successful", async () => { + const fakeRootData = [{ id: "1", name: "root" }]; + (getFakeRootDataFromService as jest.Mock).mockResolvedValue(fakeRootData); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { initialTreeDataLoadApiFailure: false }, + failedApiName: "", + }, + }, + { type: SET_FAKE_ROOT, payload: { fakeRoot: fakeRootData } }, + ]; + + await store.dispatch(getFakeRootData() as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("dispatches SET_SCHEDULE_APIS_FAILURE_ERROR when fetching fake root data fails", async () => { + const error = { error: "Failed to fetch" }; + (getFakeRootDataFromService as jest.Mock).mockResolvedValue(error); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { initialTreeDataLoadApiFailure: true }, + failedApiName: "initialTreeDataLoadApiFailure", + }, + }, + ]; + + await store.dispatch(getFakeRootData() as any); + expect(store.getActions()).toEqual(expectedActions); + }); + + it("should create an action to set parameters tab config", () => { + const expectedAction = { + type: SET_PARAMETERS_TAB_CONFIG, + payload: { parametersTabConfig: undefined }, + }; + expect(setParametersTabConfig(undefined)).toEqual(expectedAction); + }); + + it("dispatches SET_STEPPER_PROPERTIES and SCHEDULE_ERROR_OCCURRED when stepper is shown", async () => { + const currentTabValues = { someTabData: "value" }; + const currentTabErrors = { someError: "error" }; + + (getStateOfCurrentActiveTab as jest.Mock).mockReturnValue(currentTabValues); + (getErrorsForCurrentTab as jest.Mock).mockResolvedValue(currentTabErrors); + + const expectedActions = [ + { + type: SET_STEPPER_PROPERTIES, + payload: { updatedStepperData: currentTabValues }, + }, + { type: SCHEDULE_ERROR_OCCURRED, payload: { errors: currentTabErrors } }, + ]; + + await store.dispatch(currentTabValidationError(handleStateChange) as any); + expect(store.getActions()).toEqual(expectedActions); + expect(handleStateChange).toHaveBeenCalled(); + }); + + it("dispatches SCHEDULE_ERROR_OCCURRED when stepper is not shown", async () => { + store = mockStore({ + currentActiveTab: "tab1", + scheduleInfo: { someData: "value" }, + stepperConfiguration: { show: false }, + }); + const currentTabErrors = { someError: "error" }; + + (getErrorsForCurrentTab as jest.Mock).mockResolvedValue(currentTabErrors); + + const expectedActions = [ + { type: SCHEDULE_ERROR_OCCURRED, payload: { errors: currentTabErrors } }, + ]; + + await store.dispatch(currentTabValidationError(handleStateChange) as any); + expect(store.getActions()).toEqual(expectedActions); + expect(handleStateChange).toHaveBeenCalled(); + }); + + it("dispatches SCHEDULE_ERROR_OCCURRED and SET_VISITED_TABS when errors are present", async () => { + const currentStateError = { someError: "error" }; + (getErrorsForCurrentTab as jest.Mock).mockResolvedValue(currentStateError); + + const expectedActions = [ + { type: SCHEDULE_ERROR_OCCURRED, payload: { errors: currentStateError } }, + { type: SET_VISITED_TABS, payload: { tabs: allTabs } }, + ]; + + await store.dispatch(allTabValidationError(handleCreateScheduleAPI) as any); + expect(store.getActions()).toEqual(expectedActions); + expect(handleCreateScheduleAPI).toHaveBeenCalledWith(true); + }); + + it("dispatches SCHEDULE_ERROR_OCCURRED and does not set visited tabs when no errors are present", async () => { + const currentStateError = { someError: "" }; + (getErrorsForCurrentTab as jest.Mock).mockResolvedValue(currentStateError); + + const expectedActions = [ + { type: SCHEDULE_ERROR_OCCURRED, payload: { errors: currentStateError } }, + ]; + + await store.dispatch(allTabValidationError(handleCreateScheduleAPI) as any); + expect(store.getActions()).toEqual(expectedActions); + expect(handleCreateScheduleAPI).toHaveBeenCalledWith(false); + }); + + it("dispatches SET_SCHEDULE_APIS_FAILURE_ERROR with false when schedule creation is successful", async () => { + const jobInfo = { id: "123" }; + (createSchedule as jest.Mock).mockResolvedValue(jobInfo); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { createScheduleApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(createScheduleJob(enableCreateBtn) as any); + expect(store.getActions()).toEqual(expectedActions); + expect( + store.getState().schedulerUIConfig.events.scheduleBtnClick, + ).toHaveBeenCalledWith(true, jobInfo); + expect(enableCreateBtn).toHaveBeenCalled(); + }); + + it("dispatches SET_SCHEDULE_APIS_FAILURE_ERROR with true when schedule creation fails", async () => { + const error = new Error("Failed to create schedule"); + (createSchedule as jest.Mock).mockRejectedValue(error); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { createScheduleApiFailure: true }, + failedApiName: "createScheduleApiFailure", + }, + }, + ]; + + await store.dispatch(createScheduleJob(enableCreateBtn) as any); + expect(store.getActions()).toEqual(expectedActions); + expect( + store.getState().schedulerUIConfig.events.scheduleBtnClick, + ).toHaveBeenCalledWith(false, error); + expect(enableCreateBtn).toHaveBeenCalled(); + }); + + it("uses createDummySchedule when dryRun is true", async () => { + store = mockStore({ + schedulerUIConfig: { + events: { scheduleBtnClick: jest.fn() }, + dryRun: true, + }, + scheduleInfo: { + scheduleJobDescription: "Test Description", + scheduleJobName: "Test Name", + outputFormats: { outputFormat: ["pdf"] }, + mailNotification: { resultSendType: "SEND_ATTACHMENT" }, + repositoryDestination: {}, + }, + }); + const jobInfo = { id: "123" }; + (createDummySchedule as jest.Mock).mockResolvedValue(jobInfo); + + const expectedActions = [ + { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { + failedApi: { createScheduleApiFailure: false }, + failedApiName: "", + }, + }, + ]; + + await store.dispatch(createScheduleJob(enableCreateBtn) as any); + expect(store.getActions()).toEqual(expectedActions); + expect( + store.getState().schedulerUIConfig.events.scheduleBtnClick, + ).toHaveBeenCalledWith(true, jobInfo); + expect(enableCreateBtn).toHaveBeenCalled(); + }); + it("should create an action to set parameters tab loading state", () => { + const parametersTabConfig = { isLoaded: true, isError: false }; + const expectedAction = { + type: SET_PARAMETERS_TAB_LOADING, + payload: { parametersTabConfig }, + }; + + expect(parametersTabErrorOrLoading(parametersTabConfig)).toEqual( + expectedAction, + ); + }); + + it("should create an action to set parameters tab error state", () => { + const parametersTabConfig = { isLoaded: false, isError: true }; + const expectedAction = { + type: SET_PARAMETERS_TAB_LOADING, + payload: { parametersTabConfig }, + }; + + expect(parametersTabErrorOrLoading(parametersTabConfig)).toEqual( + expectedAction, + ); + }); +}); diff --git a/packages/jv-scheduler/test/apiFailureError/ErrorConfirmationDialog.test.tsx b/packages/jv-scheduler/test/apiFailureError/ErrorConfirmationDialog.test.tsx new file mode 100644 index 00000000..6aa863fa --- /dev/null +++ b/packages/jv-scheduler/test/apiFailureError/ErrorConfirmationDialog.test.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { ErrorConfirmationDialog } from "../../src/components/apiFailureError/ErrorConfirmationDialog"; + +const mockStore = configureStore([]); +const initialState = { + scheduleApisFailure: ["createScheduleApiFailure"], + lastApiCalledFailed: "createScheduleApiFailure", +}; + +describe("ErrorConfirmationDialog component", () => { + let store: any; + + beforeEach(() => { + store = mockStore(initialState); + store.dispatch = jest.fn(); + }); + + test("renders error message and sub-container message correctly", () => { + render( + + + , + ); + + expect( + screen.getByText( + "You can close this error message and try to save the schedule again.", + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + "A network error is preventing the schedule from being saved.", + ), + ).toBeInTheDocument(); + }); + + test("dispatches setApiFailure action on cancel button click", () => { + render( + + + , + ); + + const cancelButton = screen.getByText(/cancel/i); + cancelButton.click(); + + expect(store.dispatch).toHaveBeenCalled(); + }); +}); diff --git a/packages/jv-scheduler/test/components/EntryPoint.test.tsx b/packages/jv-scheduler/test/components/EntryPoint.test.tsx new file mode 100644 index 00000000..8f894a87 --- /dev/null +++ b/packages/jv-scheduler/test/components/EntryPoint.test.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +// import { useTranslation } from "react-i18next"; +import EntryPoint, { renderScheduler } from "../../src/components/EntryPoint"; +import { getSchedulerData } from "../../src/utils/configurationUtils"; +import { VisualizeClient } from "@jaspersoft/jv-tools"; +import { SchedulerConfig } from "../../src/types/scheduleType"; + +jest.mock("react-dom/client", () => ({ + createRoot: jest.fn().mockReturnValue({ + render: jest.fn(), + unmount: jest.fn(), + }), +})); + +jest.mock("i18next", () => ({ + use: jest.fn().mockReturnThis(), + init: jest.fn(), +})); + +jest.mock("react-i18next", () => ({ + useTranslation: jest.fn().mockReturnValue({ + i18n: { + changeLanguage: jest.fn(), + }, + }), +})); + +jest.mock("../../src/utils/configurationUtils", () => ({ + getSchedulerData: jest.fn(), +})); +const mockErrorFunction = jest.fn(); +const mockConfig: SchedulerConfig = { + server: "http://localhost:8080", + contextPath: "/jasperserver-pro", + locale: "en", + events: { + error: mockErrorFunction, + success: jest.fn(), + }, +}; + +const mockVisualizeClient = {} as VisualizeClient; + +describe("EntryPoint component", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders without crashing", async () => { + (getSchedulerData as jest.Mock).mockResolvedValue({}); + render( + , + ); + await waitFor(() => + expect(screen.queryByText("Error fetching data")).not.toBeInTheDocument(), + ); + }); + + test("calls error event when root element is not found", () => { + const container = document.getElementById("root") as HTMLElement; + renderScheduler(mockVisualizeClient, "test-uri", container, mockConfig); + expect(mockErrorFunction).toHaveBeenCalledWith({ + "container.not.found": "Root element is not found", + }); + }); + + // test("renders SchedulerMain when data is fetched successfully", async () => { + // (getSchedulerData as jest.Mock).mockResolvedValue({}); + // render( + // + // ); + // screen.logTestingPlaygroundURL(); + // await waitFor(() => expect(screen.getByText("SchedulerMain")).toBeInTheDocument()); + // }); + + // test("calls error event when data fetch fails", async () => { + // (getSchedulerData as jest.Mock).mockRejectedValue(new Error("Fetch error")); + // render( + // + // ); + // await waitFor(() => expect(mockConfig.events?.error).toHaveBeenCalledWith("Fetch error")); + // }); + // + // test("changes language based on config locale", async () => { + // (getSchedulerData as jest.Mock).mockResolvedValue({}); + // render( + // + // ); + // await waitFor(() => expect(useTranslation().i18n.changeLanguage).toHaveBeenCalledWith("fr")); + // }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/DefaultData.test.tsx b/packages/jv-scheduler/test/components/Stepper/DefaultData.test.tsx new file mode 100644 index 00000000..d1dc6a3c --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/DefaultData.test.tsx @@ -0,0 +1,246 @@ +import React from "react"; +import { render, screen, cleanup } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { + NotificationStepDefaultMessage, + ParametersStepDefaultMessage, + ScheduleStepDefaultMessage, + OutputStepDefaultMessage, +} from "../../../src/components/Stepper/DefaultData"; +import moment from "moment"; +import { useSelector } from "react-redux"; + +jest.mock("i18next", () => ({ + use: jest.fn().mockReturnThis(), + init: jest.fn(), +})); + +// Mock the useSelector hook +jest.mock("react-redux", () => ({ + useSelector: jest.fn(), +})); + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("ParametersStepDefaultMessage component", () => { + const mockState = { + scheduleInfo: { + scheduleJobName: "Test Job Name", + trigger: { + simpleTrigger: { + startDate: moment().toISOString(), + startType: "DATE_TIME", + }, + }, + repositoryDestination: { + outputDescription: "Test Description", + }, + }, + stepperState: { + scheduleJobDescription: "Test Job Description", + recurrenceInterval: 5, + recurrenceIntervalUnit: "HOURS", + subject: "Test Subject", + address: ["test@example.com", "test2@example.com"], + resultSendType: "SEND_LINK", + messageText: "Test Message", + baseOutputFilename: "TestFileName", + outputFormat: ["pdf", "html"], + outputTimeZone: "UTC", + }, + userTimeZones: [{ code: "UTC", description: "Coordinated Universal Time" }], + }; + + beforeEach(() => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector(mockState), + ); + }); + + afterEach(() => { + cleanup(); + }); + + test("renders parameters step default message correctly", () => { + const { unmount } = render(); + + expect( + screen.getByText("stepper.parameters.ic.helpertext"), + ).toBeInTheDocument(); + unmount(); + }); + + test("renders recipients correctly", () => { + const { unmount } = render(); + expect( + screen.getByText(/stepper\.notifications\.recipients\.key/i), + ).toBeInTheDocument(); + unmount(); + }); + + test("renders subject correctly", () => { + const { unmount } = render(); + expect( + screen.getByText(/stepper\.notifications\.subject\.key/i), + ).toBeInTheDocument(); + unmount(); + }); + + test("renders message correctly", () => { + render(); + expect( + screen.getByText(/stepper\.notifications\.message\.key/i), + ).toBeInTheDocument(); + }); + + test("renders access type correctly", () => { + render(); + expect( + screen.getByText(/stepper\.notifications\.access\.key/i), + ).toBeInTheDocument(); + }); + + test("renders helper texts when data is missing", () => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ + stepperState: {}, + userTimeZones: [], + }), + ); + render(); + + expect( + screen.getByText("stepper.notifications.recipients.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.notifications.subject.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.notifications.message.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.notifications.access.helpertext"), + ).toBeInTheDocument(); + }); + + test("renders job name correctly", () => { + render(); + expect(screen.getByText("Test Job Name")).toBeInTheDocument(); + }); + + test("renders job description correctly", () => { + render(); + expect(screen.getByText("Test Job Description")).toBeInTheDocument(); + }); + + test("renders recurrence interval correctly", () => { + render(); + expect( + screen.getByText(/stepper\.schedule\.recurrence\.helpertext/i), + ).toBeInTheDocument(); + }); + + test("renders start date correctly", () => { + render(); + expect( + screen.getByText(/stepper\.schedule\.startnow\.value/i), + ).toBeInTheDocument(); + }); + + test("renders helper texts when data is missing", () => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ + scheduleInfo: {}, + stepperState: {}, + }), + ); + render(); + expect( + screen.getByText("stepper.schedule.jobname.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.schedule.description.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.schedule.recurrence.helpertext"), + ).toBeInTheDocument(); + expect( + screen.getByText("stepper.schedule.startnow.value"), + ).toBeInTheDocument(); + }); + test("renders file name correctly", () => { + render(); + expect(screen.getByText("stepper.output.filename.key")).toBeInTheDocument(); + expect(screen.getByText("TestFileName")).toBeInTheDocument(); + }); + + test("renders file description correctly", () => { + render(); + expect( + screen.getByText("stepper.output.description.key"), + ).toBeInTheDocument(); + expect(screen.getByText("Test Description")).toBeInTheDocument(); + }); + + test("renders output timezone correctly", () => { + render(); + expect(screen.getByText("stepper.output.timezone.key")).toBeInTheDocument(); + expect( + screen.getByText("UTC - Coordinated Universal Time"), + ).toBeInTheDocument(); + }); + + test("renders output formats correctly", () => { + render(); + expect(screen.getByText("stepper.output.formats.key")).toBeInTheDocument(); + expect( + screen.getByText("output.format.pdf, output.format.html"), + ).toBeInTheDocument(); + }); + + test("renders helper text when file name is missing", () => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ + ...mockState, + stepperState: { ...mockState.stepperState, baseOutputFilename: "" }, + }), + ); + render(); + expect(screen.getByText("stepper.output.helpertext")).toBeInTheDocument(); + }); + + test("renders helper text when file description is missing", () => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ + ...mockState, + scheduleInfo: { repositoryDestination: { outputDescription: "" } }, + }), + ); + render(); + expect( + screen.getByText("stepper.output.description.helpertext"), + ).toBeInTheDocument(); + }); + + test("renders additional formats when more than max limit", () => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ + ...mockState, + stepperState: { + ...mockState.stepperState, + outputFormat: ["pdf", "html", "csv", "xml", "csv1", "pdf1", "xls"], + }, + }), + ); + render(); + expect( + screen.getByText( + /output\.format\.pdf, output\.format\.html, output\.format\.csv, output\.format\.xml, output\.format\.csv1 \(\+2 schedule\.more\)/i, + ), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/ErrorTemplate.test.tsx b/packages/jv-scheduler/test/components/Stepper/ErrorTemplate.test.tsx new file mode 100644 index 00000000..5293f0eb --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/ErrorTemplate.test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { ErrorTemplate } from "../../../src/components/Stepper/ErrorTemplate"; + +describe("ErrorTemplate component", () => { + it("renders error text with correct class", () => { + const errorMessage = "This is an error message"; + render(); + const errorElement = screen.getByText(errorMessage); + expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveClass( + "jv-mText jv-uMargin-b-01 jv-uColor-error", + ); + }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/FieldHeader.test.tsx b/packages/jv-scheduler/test/components/Stepper/FieldHeader.test.tsx new file mode 100644 index 00000000..a0c55ad5 --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/FieldHeader.test.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { FieldHeader } from "../../../src/components/Stepper/FieldHeader"; + +describe("FieldHeader component", () => { + it("renders text with correct class", () => { + const headerText = "This is a header"; + render(); + const headerElement = screen.getByText(headerText); + expect(headerElement).toBeInTheDocument(); + expect(headerElement).toHaveClass("jv-uGrey-light jv-uMargin-b-01"); + }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/KeyValueTemplate.test.tsx b/packages/jv-scheduler/test/components/Stepper/KeyValueTemplate.test.tsx new file mode 100644 index 00000000..5c7913d2 --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/KeyValueTemplate.test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { KeyValueTemplate } from "../../../src/components/Stepper/KeyValueTemplate"; + +describe("KeyValueTemplate component", () => { + it("renders KeyValueTemplate with title and value", () => { + render(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("Test Value")).toBeInTheDocument(); + }); + + it("applies className and dataName props", () => { + render( + , + ); + const typographyElement = screen.getByText("Test Title").closest("p"); + expect(typographyElement).toHaveClass("test-class"); + expect(typographyElement).toHaveAttribute("data-name", "test-data-name"); + }); + + it("renders with undefined value", () => { + render(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.queryByText("undefined")).not.toBeInTheDocument(); + }); + + it("renders with null value", () => { + render(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.queryByText("null")).not.toBeInTheDocument(); + }); + + it("renders with numeric value", () => { + render(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("123")).toBeInTheDocument(); + }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/StepIcon.test.tsx b/packages/jv-scheduler/test/components/Stepper/StepIcon.test.tsx new file mode 100644 index 00000000..ee7e3013 --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/StepIcon.test.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { StepIcon } from "../../../src/components/Stepper/StepIcon"; +import { + ERROR_STATE, + INCOMPLETE_DEFAULT_STATE, + INCOMPLETE_STATE, + SUCCESS_STATE, +} from "../../../src/constants/schedulerConstants"; + +describe("StepIcon component", () => { + test("renders success icon", () => { + render(); + expect(screen.getByTestId("step-icon")).toHaveClass("jv-uColor-success"); + }); + + test("renders error icon", () => { + render(); + expect(screen.getByTestId("step-icon")).toHaveClass("jv-uColor-error"); + }); + + test("renders incomplete default icon", () => { + render( + , + ); + expect(screen.getByTestId("step-icon")).toHaveClass("jv-uColor-incomplete"); + }); + + test("renders incomplete icon", () => { + render(); + expect(screen.getByTestId("step-icon")).toHaveClass("jv-uColor-incomplete"); + }); +}); diff --git a/packages/jv-scheduler/test/components/Stepper/Stepper.test.tsx b/packages/jv-scheduler/test/components/Stepper/Stepper.test.tsx new file mode 100644 index 00000000..f6a93b6c --- /dev/null +++ b/packages/jv-scheduler/test/components/Stepper/Stepper.test.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { useSelector } from "react-redux"; +import Stepper from "../../../src/components/Stepper/Stepper"; +import moment from "moment"; +import { + NOTIFICATIONS_TAB, + OUTPUT_TAB, + PARAMETERS_TAB, + SCHEDULE_TAB, +} from "../../../src/constants/schedulerConstants"; + +jest.mock("i18next", () => ({ + use: jest.fn().mockReturnThis(), + init: jest.fn(), +})); + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +jest.mock("react-redux", () => ({ + useSelector: jest.fn(), +})); + +describe("Stepper component", () => { + const mockState = { + currentActiveTab: SCHEDULE_TAB, + visitedTabs: [SCHEDULE_TAB, OUTPUT_TAB, NOTIFICATIONS_TAB, PARAMETERS_TAB], + tabsConfiguration: { + stepsToShow: [ + { name: SCHEDULE_TAB, title: "Schedule" }, + { name: OUTPUT_TAB, title: "Output" }, + { name: NOTIFICATIONS_TAB, title: "Notifications" }, + ], + }, + scheduleInfo: { + scheduleJobName: "Test Job Name", + trigger: { + simpleTrigger: { + startDate: moment().toISOString(), + startType: "DATE_TIME", + }, + }, + repositoryDestination: { + outputDescription: "Test Description", + }, + }, + stepperState: { + scheduleJobDescription: "Test Job Description", + recurrenceInterval: 5, + recurrenceIntervalUnit: "HOURS", + subject: "Test Subject", + address: ["test@example.com", "test2@example.com"], + resultSendType: "SEND_LINK", + messageText: "Test Message", + baseOutputFilename: "TestFileName", + outputFormat: ["pdf", "html"], + outputTimeZone: "UTC", + }, + userTimeZones: [{ code: "UTC", description: "Coordinated Universal Time" }], + scheduleErrors: { + scheduleJobName: null, + scheduleJobDescription: null, + address: null, + subject: null, + messageText: null, + recurrenceInterval: null, + recurrenceIntervalUnit: null, + startDate: null, + baseOutputFilename: null, + outputFormat: null, + folderURI: null, + baseOutputFileDescription: null, + parameters: null, + }, + }; + + beforeEach(() => { + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector(mockState), + ); + }); + + it("renders JVStepper with correct props", () => { + render(); + screen.logTestingPlaygroundURL(); + expect(screen.getByText("Schedule")).toBeInTheDocument(); + expect(screen.getByText("Output")).toBeInTheDocument(); + expect(screen.getByText("Notifications")).toBeInTheDocument(); + }); +}); diff --git a/packages/jv-scheduler/test/components/common/CommonComponents.test.tsx b/packages/jv-scheduler/test/components/common/CommonComponents.test.tsx new file mode 100644 index 00000000..df24de47 --- /dev/null +++ b/packages/jv-scheduler/test/components/common/CommonComponents.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright © 2024. Cloud Software Group, Inc. + * This file is subject to the license terms contained + * in the license file that is distributed with this file. + */ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { JVTypographyComponent } from "../../../src/components/common/CommonComponents"; + +describe("Common components", () => { + const renderComponent = () => { + render( + , + ); + }; + + test("renders Typography component", () => { + renderComponent(); + expect(screen.getByTestId("typography")).toBeInTheDocument(); + }); +}); diff --git a/packages/jv-scheduler/test/components/common/ErrorDialog.test.tsx b/packages/jv-scheduler/test/components/common/ErrorDialog.test.tsx new file mode 100644 index 00000000..446abbe1 --- /dev/null +++ b/packages/jv-scheduler/test/components/common/ErrorDialog.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright © 2024. Cloud Software Group, Inc. + * This file is subject to the license terms contained + * in the license file that is distributed with this file. + */ + +import React from "react"; +import { screen } from "@testing-library/dom"; +import { cleanup, render } from "@testing-library/react"; +import "@testing-library/jest-dom"; +// import userEvent from "@testing-library/user-event"; +import { ErrorDialog } from "../../../src/components/common/ErrorDialog"; + +describe("Error dialog", () => { + const mockCancelBtnFunction = jest.fn(); + beforeEach(() => { + cleanup(); + }); + const renderComponent = () => { + render( + , + ); + }; + + test("renders error dialog and cancel button", () => { + renderComponent(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: /cancel/i, + }), + ).toBeInTheDocument(); + }); + + // test("calls handleCancelBtn function when cancel button is clicked", async () => { + // const mockCancelBtnFunction = () => { + // console.log("mock cancel button function") + // }; + // renderComponent(mockCancelBtnFunction); + // const button = screen.getByTestId("actionBtn"); + // console.log(button.onclick?.toString(), "button onclick event"); + // await act(async () => { + // userEvent.click(button); + // }); + // expect(mockCancelBtnFunction).toHaveBeenCalled(); + // }); +}); diff --git a/packages/jv-scheduler/test/components/loader/Loader.test.tsx b/packages/jv-scheduler/test/components/loader/Loader.test.tsx new file mode 100644 index 00000000..d7adf2ca --- /dev/null +++ b/packages/jv-scheduler/test/components/loader/Loader.test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import Loader from "../../../src/components/loader/Loader"; + +jest.mock("@jaspersoft/jv-ui-components", () => ({ + JVCircularProgress: jest.fn(() =>
), +})); + +describe("Loader component", () => { + test("renders Loader component with two JVCircularProgress elements", () => { + render(); + const progressElements = screen.getAllByTestId("circular-progress"); + expect(progressElements).toHaveLength(2); + }); +}); diff --git a/packages/jv-scheduler/test/reducer/reducer.test.ts b/packages/jv-scheduler/test/reducer/reducer.test.ts new file mode 100644 index 00000000..e98eb848 --- /dev/null +++ b/packages/jv-scheduler/test/reducer/reducer.test.ts @@ -0,0 +1,237 @@ +import { rootReducer, initialState } from "../../src/reducer/reducer"; +import { + SET_OUTPUT_FORMATS, + SET_SCHEDULE_APIS_FAILURE_ERROR, + SET_REPOSITORY_FOLDER_DATA, + SET_SCHEDULER_UI_CONFIG, + SET_USER_TIME_ZONES, + SET_PROPERTIES_DETAILS, + SET_FAKE_ROOT, + SET_VISITED_TABS, + SET_ACTIVE_TAB, + SET_STEPPER_PROPERTIES, + SET_TABS_CONFIG, + SET_VISIBLE_FIELDS, + SCHEDULE_ERROR_OCCURRED, + SET_STEPPER_CONFIG, + SET_VISUALIZE_DATA, + SET_PARAMETERS_TAB_LOADING, + SET_PARAMETERS_TAB_CONFIG, +} from "../../src/constants/actionConstants"; + +describe("rootReducer", () => { + it("should return the initial state", () => { + expect(rootReducer(undefined, { type: "", payload: {} })).toEqual( + initialState, + ); + }); + + it("should handle SET_USER_TIME_ZONES", () => { + const action = { + type: SET_USER_TIME_ZONES, + payload: { userTimeZones: [{ code: "UTC", description: "UTC" }] }, + }; + const expectedState = { + ...initialState, + userTimeZones: action.payload.userTimeZones, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_OUTPUT_FORMATS", () => { + const action = { + type: SET_OUTPUT_FORMATS, + payload: { outputFormats: ["pdf"] }, + }; + const expectedState = { + ...initialState, + outputFormats: action.payload.outputFormats, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_SCHEDULER_UI_CONFIG", () => { + const action = { + type: SET_SCHEDULER_UI_CONFIG, + payload: { schedulerUIConfig: { server: "localhost" } }, + }; + const expectedState = { + ...initialState, + schedulerUIConfig: action.payload.schedulerUIConfig, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_REPOSITORY_FOLDER_DATA", () => { + const action = { + type: SET_REPOSITORY_FOLDER_DATA, + payload: { folderData: { folder: "data" } }, + }; + const expectedState = { + ...initialState, + folderData: { ...initialState.folderData, ...action.payload.folderData }, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_FAKE_ROOT", () => { + const action = { + type: SET_FAKE_ROOT, + payload: { fakeRoot: { id: "root" } }, + }; + const expectedState = { + ...initialState, + fakeRoot: action.payload.fakeRoot, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_PROPERTIES_DETAILS", () => { + const action = { + type: SET_PROPERTIES_DETAILS, + payload: { newScheduleInfo: { name: "test" } }, + }; + const expectedState = { + ...initialState, + scheduleInfo: { + ...initialState.scheduleInfo, + ...action.payload.newScheduleInfo, + }, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_SCHEDULE_APIS_FAILURE_ERROR", () => { + const action = { + type: SET_SCHEDULE_APIS_FAILURE_ERROR, + payload: { failedApi: { error: "error" }, failedApiName: "apiName" }, + }; + const expectedState = { + ...initialState, + scheduleApisFailure: { + ...initialState.scheduleApisFailure, + ...action.payload.failedApi, + }, + lastApiCalledFailed: action.payload.failedApiName, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_VISITED_TABS", () => { + const action = { type: SET_VISITED_TABS, payload: { tabs: ["tab1"] } }; + const expectedState = { ...initialState, visitedTabs: action.payload.tabs }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_ACTIVE_TAB", () => { + const action = { type: SET_ACTIVE_TAB, payload: { activeTab: "tab1" } }; + const expectedState = { + ...initialState, + currentActiveTab: action.payload.activeTab, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_STEPPER_PROPERTIES", () => { + const action = { + type: SET_STEPPER_PROPERTIES, + payload: { updatedStepperData: { step: 1 } }, + }; + const expectedState = { + ...initialState, + stepperState: { + ...initialState.stepperState, + ...action.payload.updatedStepperData, + }, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_STEPPER_CONFIG", () => { + const action = { type: SET_STEPPER_CONFIG, payload: { show: true } }; + const expectedState = { + ...initialState, + stepperConfiguration: action.payload, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_TABS_CONFIG", () => { + const action = { + type: SET_TABS_CONFIG, + payload: { + currentActiveTab: "tab1", + tabsConfiguration: { tabsToShow: [] }, + }, + }; + const expectedState = { + ...initialState, + currentActiveTab: action.payload.currentActiveTab, + tabsConfiguration: action.payload.tabsConfiguration, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_VISIBLE_FIELDS", () => { + const action = { + type: SET_VISIBLE_FIELDS, + payload: { fieldsVisibility: { field: true } }, + }; + const expectedState = { + ...initialState, + fieldsVisibility: { + ...initialState.fieldsVisibility, + ...action.payload.fieldsVisibility, + }, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SCHEDULE_ERROR_OCCURRED", () => { + const action = { + type: SCHEDULE_ERROR_OCCURRED, + payload: { errors: { error: "error" } }, + }; + const expectedState = { + ...initialState, + scheduleErrors: { + ...initialState.scheduleErrors, + ...action.payload.errors, + }, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_VISUALIZE_DATA", () => { + const action = { type: SET_VISUALIZE_DATA, payload: { visualize: true } }; + const expectedState = { + ...initialState, + visualize: action.payload.visualize, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_PARAMETERS_TAB_LOADING", () => { + const action = { + type: SET_PARAMETERS_TAB_LOADING, + payload: { parametersTabConfig: { isLoaded: true, isError: false } }, + }; + const expectedState = { + ...initialState, + parametersTabLoading: action.payload.parametersTabConfig, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); + + it("should handle SET_PARAMETERS_TAB_CONFIG", () => { + const action = { + type: SET_PARAMETERS_TAB_CONFIG, + payload: { parametersTabConfig: { config: "config" } }, + }; + const expectedState = { + ...initialState, + parametersTabConfig: action.payload.parametersTabConfig, + }; + expect(rootReducer(initialState, action)).toEqual(expectedState); + }); +}); diff --git a/packages/jv-scheduler/test/services/schedulerServices.test.ts b/packages/jv-scheduler/test/services/schedulerServices.test.ts new file mode 100644 index 00000000..919fe26e --- /dev/null +++ b/packages/jv-scheduler/test/services/schedulerServices.test.ts @@ -0,0 +1,183 @@ +import axios from "axios"; +import { + checkPermissionOnResource, + createDummySchedule, + createSchedule, + getFakeRootDataFromService, + getOutputFormatsFromService, + getRepositoryFolderData, + getUserTimezonesFromService, +} from "../../src/services/schedulerServices"; +import store from "../../src/store/store"; + +jest.mock("axios"); +jest.mock("../../src/store/store"); + +describe("schedulerServices", () => { + const mockState = { + schedulerUIConfig: { + server: "http://localhost", + contextPath: "/jasperserver", + timezone: "UTC", + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + store.getState = jest.fn().mockReturnValue(mockState); + + describe("checkPermissionOnResource", () => { + it("should return data on success", async () => { + const mockData = { permissionMask: 1 }; + (axios.get as jest.Mock).mockResolvedValue({ data: mockData }); + + const result = await checkPermissionOnResource( + "/path", + "http://localhost", + "/jasperserver", + ); + expect(result).toEqual(mockData); + expect(axios.get).toHaveBeenCalledWith( + "http://localhost/jasperserver/rest_v2/resources/path", + { withCredentials: true, headers: { Accept: "application/json" } }, + ); + }); + + it("should return error on failure", async () => { + (axios.get as jest.Mock).mockRejectedValue(new Error("Failed")); + + const result = await checkPermissionOnResource( + "/path", + "http://localhost", + "/jasperserver", + ); + expect(result).toEqual({ error: "Failed to check permission folder" }); + }); + }); + + describe("getUserTimezonesFromService", () => { + it("should return data on success", async () => { + const mockData = { timezones: ["UTC"] }; + (axios.get as jest.Mock).mockResolvedValue({ data: mockData }); + + const result = await getUserTimezonesFromService(); + expect(result).toEqual(mockData); + expect(axios.get).toHaveBeenCalledWith( + "http://localhost/jasperserver/rest_v2/settings/userTimeZones", + { headers: { Accept: "application/json" } }, + ); + }); + + it("should return error on failure", async () => { + (axios.get as jest.Mock).mockRejectedValue(new Error("Failed")); + + const result = await getUserTimezonesFromService(); + expect(result).toEqual({ error: "Failed to fetch user timezones" }); + }); + }); + + describe("getOutputFormatsFromService", () => { + it("should return data on success", async () => { + const mockData = { formats: ["pdf"] }; + (axios.get as jest.Mock).mockResolvedValue({ data: mockData }); + + const result = await getOutputFormatsFromService(); + expect(result).toEqual(mockData); + expect(axios.get).toHaveBeenCalledWith( + "http://localhost/jasperserver/rest_v2/settings/alertingSettings", + { headers: { Accept: "application/json" } }, + ); + }); + + it("should return error on failure", async () => { + (axios.get as jest.Mock).mockRejectedValue(new Error("Failed")); + + const result = await getOutputFormatsFromService(); + expect(result).toEqual({ error: "Failed to fetch output options" }); + }); + }); + + describe("getRepositoryFolderData", () => { + it("should return data on success", async () => { + const mockData = { folders: [] }; + (axios.get as jest.Mock).mockResolvedValue({ data: mockData }); + + const result = await getRepositoryFolderData("/path"); + expect(result).toEqual(mockData); + expect(axios.get).toHaveBeenCalledWith( + "http://localhost/jasperserver/rest_v2/api/resources?folderUri=%2Fpath&recursive=false&type=folder&offset=0&limit=5000&forceTotalCount=true&forceFullPage=true", + { withCredentials: true, headers: { Accept: "application/json" } }, + ); + }); + + it("should return error on failure", async () => { + (axios.get as jest.Mock).mockRejectedValue(new Error("Failed")); + + const result = await getRepositoryFolderData("/path"); + expect(result).toEqual({ + error: "Failed to fetch repository folder data", + }); + }); + }); + + describe("getFakeRootDataFromService", () => { + it("should return data on success", async () => { + const mockData = + '
{"children":[],"label":"root","extra":{}}
'; + (axios.get as jest.Mock).mockResolvedValue({ data: mockData }); + + const result = await getFakeRootDataFromService(); + expect(result).toEqual([ + { + id: "/root", + label: "root", + uri: "/", + resourceType: "folder", + permissionMask: 2, + }, + ]); + }); + + it("should return error on failure", async () => { + (axios.get as jest.Mock).mockRejectedValue(new Error("Failed")); + + const result = await getFakeRootDataFromService(); + expect(result).toEqual({ error: "Failed to fetch base folder data" }); + }); + }); + + describe("createSchedule", () => { + it("should create schedule successfully", async () => { + const mockData = { id: 1 }; + (axios.put as jest.Mock).mockResolvedValue({ data: mockData }); + + const scheduleInfo = { name: "test" }; + const result = await createSchedule(scheduleInfo); + expect(result.data).toEqual(mockData); + expect(axios.put).toHaveBeenCalledWith( + "http://localhost/jasperserver/rest_v2/jobs", + scheduleInfo, + { + withCredentials: true, + headers: { + Accept: "application/job+json", + "Content-Type": "application/job+json", + "x-requested-with": "XMLHttpRequest, OWASP CSRFGuard Project", + "X-Remote-Domain": "http://localhost", + "X-Suppress-Basic": "true", + }, + }, + ); + }); + }); + + describe("createDummySchedule", () => { + it("should return schedule info", async () => { + const scheduleInfo = { name: "test" }; + const result = await createDummySchedule(scheduleInfo); + expect(result).toEqual(scheduleInfo); + }); + }); +}); diff --git a/packages/jv-scheduler/test/utils/configurationUtils.test.ts b/packages/jv-scheduler/test/utils/configurationUtils.test.ts new file mode 100644 index 00000000..5af65525 --- /dev/null +++ b/packages/jv-scheduler/test/utils/configurationUtils.test.ts @@ -0,0 +1,284 @@ +import { getSchedulerData } from "../../src/utils/configurationUtils"; +import { checkPermissionOnResource } from "../../src/services/schedulerServices"; + +jest.mock("../../src/services/schedulerServices"); + +describe("configurationUtils", () => { + describe("getSchedulerData", () => { + it("should return error if basic resource URI is missing", async () => { + const scheduleConfig = { resourceURI: "" }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "resource.uri.missing.configuration": + "resourceURI is required in the configuration", + }); + }); + + it("should return error if basic server is missing", async () => { + const scheduleConfig = { resourceURI: "/test", server: "" }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "server.missing.configuration": + "server is required in the configuration", + }); + }); + + it("should return error if basic contextPath is missing", async () => { + const scheduleConfig = { + resourceURI: "/test", + server: "https://test.com", + contextPath: "", + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "contextPath.missing.configuration": + "contextPath is required in the configuration", + }); + }); + + it("should return error if resource URI is not found", async () => { + (checkPermissionOnResource as jest.Mock).mockResolvedValue({}); + const scheduleConfig = { + resourceURI: "resourceURI", + server: "server", + contextPath: "contextPath", + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "resource.not.found": "Resource URI was not found", + }); + }); + + it("should return scheduler data if all configurations are correct", async () => { + (checkPermissionOnResource as jest.Mock).mockResolvedValue({ + permissionMask: 1, + }); + const scheduleConfig = { + resourceURI: "resourceURI", + server: "server", + contextPath: "contextPath", + tabs: { tabsData: {}, tabsOrder: [] }, + stepper: { show: true }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result).toEqual({ + showStepper: true, + stepperDefaultState: expect.any(Object), + scheduleInfo: expect.any(Object), + tabsToShow: expect.any(Array), + stepsToShow: expect.any(Array), + fieldsSupportedValues: expect.any(Object), + currentActiveTab: expect.any(String), + fieldsVisibility: expect.any(Object), + }); + }); + }); + it("should return error if scheduler tab is hidden and required data for it is missing", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["parameters", "notifications", "output"], + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + schedule: { + "label.missing.value.schedule.tab.hidden.configuration": + "Value for label is required in the configuration when schedule tab is hidden", + }, + }); + }); + + it("should return error if scheduler tab is visible and required data for it is missing", async () => { + (checkPermissionOnResource as jest.Mock).mockResolvedValue({ + permissionMask: 1, + }); + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "notifications", "output"], + tabsData: { + schedule: { + label: { + show: false, + }, + }, + }, + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "label.hidden.missing.value.configuration": + "Value for label is required in the configuration when label is hidden", + }); + }); + + it("should return error if scheduler tab is visible and required data for it is incorrect", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "notifications", "output"], + tabsData: { + schedule: { + label: { + value: "a".repeat(256), + show: false, + }, + }, + }, + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "label.invalid": + "The scheduled job name is too long. The maximum length is 100 characters.", + }); + }); + + it("should return error if notifications tab is hidden and required data for it is missing", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "output"], + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + notifications: { + "address.hidden.missing.value.notification.tab.hidden.configuration": + "Value for address is required in the configuration when notifications tab is hidden", + "subject.hidden.missing.value.configuration": + "Value for subject is required in the configuration when notifications tab is hidden", + }, + }); + }); + + it("should return error if notifications tab is visible and required data for it is missing", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "notifications", "output"], + tabsData: { + notifications: { + address: { + show: false, + }, + subject: { + show: false, + }, + }, + }, + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "address.hidden.missing.value.configuration": + "Value for address is required in the configuration when address is hidden", + "subject.hidden.missing.value.configuration": + "Value for subject is required in the configuration when subject is hidden", + }); + }); + + it("should return error if notifications tab is hidden and address not in proper format", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "output"], + tabsData: { + notifications: { + address: { + value: "", + }, + subject: { + value: "subject", + }, + }, + }, + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + notifications: { + "address.not.in.proper.format": + "Value for address should be an array of strings", + }, + }); + }); + + it("should return error if output tab is hidden and required data for it is missing", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "notifications"], + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + output: { + "baseOutputFilename.hidden.missing.value.output.tab.hidden.configuration": + "Value for baseOutputFilename is required in the configuration when output tab is hidden", + }, + }); + }); + + it("should return error if output tab is visible and required data for it is missing", async () => { + const scheduleConfig = { + server: "https://mobiledemo.jaspersoft.com", + contextPath: "/jasperserver-pro", + resourceURI: "/test", + stepper: { + show: true, + }, + tabs: { + tabsOrder: ["schedule", "parameters", "notifications"], + tabsData: { + output: { + baseOutputFilename: { + show: false, + }, + }, + }, + }, + }; + const result = await getSchedulerData(scheduleConfig); + expect(result.error).toEqual({ + "baseOutputFilename.hidden.missing.value.configuration": + "Value for baseOutputFilename is required in the configuration when baseOutputFilename is hidden", + }); + }); +}); diff --git a/packages/jv-scheduler/test/utils/schedulerUtils.test.ts b/packages/jv-scheduler/test/utils/schedulerUtils.test.ts new file mode 100644 index 00000000..c995a721 --- /dev/null +++ b/packages/jv-scheduler/test/utils/schedulerUtils.test.ts @@ -0,0 +1,262 @@ +import { + getStateOfCurrentActiveTab, + getErrorsForCurrentTab, + getUriParts, + getExpandedNodeDataFromUri, + getLengthOfObject, + getUriToCompare, + addChildrenToTreeOnLoad, + updateChangeToStore, +} from "../../src/utils/schedulerUtils"; +import { stateValidator } from "../../src/validations/scheduleValidators"; +import { + PUBLIC_FOLDER, + ROOT_FOLDER, + NOTIFICATIONS_TAB, + SCHEDULE_TAB, + OUTPUT_TAB, +} from "../../src/constants/schedulerConstants"; + +jest.mock("../../src/validations/scheduleValidators"); + +describe("schedulerUtils", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("getStateOfCurrentActiveTab", () => { + const scheduleCurrentStateValues = { + scheduleJobDescription: "description", + scheduleJobName: "name", + baseOutputFileDescription: "output description", + baseOutputFilename: "output filename", + mailNotification: { + toAddresses: { address: "test@example.com" }, + subject: "subject", + messageText: "message", + resultSendType: "sendType", + }, + outputFormats: { outputFormat: "format" }, + trigger: { + simpleTrigger: { + recurrenceInterval: 1, + recurrenceIntervalUnit: "unit", + startDate: "startDate", + startType: "startType", + }, + }, + outputTimeZone: "timezone", + repositoryDestination: { folderURI: "folderURI" }, + }; + + it("should return correct state for NOTIFICATIONS_TAB", () => { + const result = getStateOfCurrentActiveTab( + NOTIFICATIONS_TAB, + scheduleCurrentStateValues, + ); + expect(result).toEqual({ + address: "test@example.com", + subject: "subject", + messageText: "message", + startDate: "startDate", + startType: "startType", + folderURI: "folderURI", + resultSendType: "sendType", + }); + }); + + it("should return correct state for SCHEDULE_TAB", () => { + const result = getStateOfCurrentActiveTab( + SCHEDULE_TAB, + scheduleCurrentStateValues, + ); + expect(result).toEqual({ + scheduleJobName: "name", + scheduleJobDescription: "description", + recurrenceInterval: 1, + recurrenceIntervalUnit: "unit", + startDate: "startDate", + startType: "startType", + outputTimeZone: "timezone", + }); + }); + + it("should return correct state for OUTPUT_TAB", () => { + const result = getStateOfCurrentActiveTab( + OUTPUT_TAB, + scheduleCurrentStateValues, + ); + expect(result).toEqual({ + baseOutputFileDescription: "output description", + baseOutputFilename: "output filename", + outputFormat: "format", + outputTimeZone: "timezone", + startDate: "startDate", + startType: "startType", + }); + }); + + it("should return default state for unknown tab", () => { + const result = getStateOfCurrentActiveTab( + "UNKNOWN_TAB", + scheduleCurrentStateValues, + ); + expect(result).toEqual({ + scheduleJobName: "name", + scheduleJobDescription: "description", + baseOutputFileDescription: "output description", + startDate: "startDate", + startType: "startType", + address: "test@example.com", + subject: "subject", + messageText: "message", + folderURI: "folderURI", + resultSendType: "sendType", + recurrenceInterval: 1, + recurrenceIntervalUnit: "unit", + outputTimeZone: "timezone", + baseOutputFilename: "output filename", + outputFormat: "format", + }); + }); + }); + + describe("getErrorsForCurrentTab", () => { + it("should return errors for current tab", async () => { + const currentState = { + baseOutputFilename: "testFileName", + baseOutputFileDescription: "file description", + scheduleJobName: "job name", + scheduleJobDescription: "job description", + mailNotification: { + resultSendType: "SEND_ATTACHMENT", + messageText: "text", + subject: "test", + toAddresses: { address: "test@test.com" }, + }, + trigger: { + simpleTrigger: { + timezone: "UTC", + occurrenceCount: 1, + startType: 1, + recurrenceInterval: 1, + recurrenceIntervalUnit: "DAY", + endDate: null, + startDate: null, + }, + }, + outputTimeZone: "UTC", + outputFormatList: ["pdf"], + outputFormats: { outputFormat: ["pdf"] }, + userTimeZones: [{ code: "UTC", description: "UTC" }] as [ + { code: string; description: string }, + ], + repositoryDestination: { + folderURI: "/test", + saveToRepository: true, + }, + source: { + parameters: { + parameterValues: {}, + }, + }, + }; + (stateValidator as jest.Mock).mockResolvedValue({ error: "error" }); + const result = await getErrorsForCurrentTab("tab", currentState); + expect(result).toEqual({ error: "error" }); + }); + }); + + describe("getUriParts", () => { + it("should return URI parts without resource name", () => { + const result = getUriParts("/root/some/path", false); + expect(result).toEqual(["root", "some", "path"]); + }); + + it("should return URI parts with resource name", () => { + const result = getUriParts("/root/some/path", true); + expect(result).toEqual(["root", "some"]); + }); + }); + + describe("getExpandedNodeDataFromUri", () => { + it("should return expanded node data from URI", () => { + const result = getExpandedNodeDataFromUri("/root/some/path", false); + expect(result).toEqual(["/root", "/root/some", "/root/some/path"]); + }); + + it("should call itemHandlerCallback for each part of the URI", () => { + const itemHandlerCallback = jest.fn(); + getExpandedNodeDataFromUri("/root/some/path", false, itemHandlerCallback); + expect(itemHandlerCallback).toHaveBeenCalledTimes(3); + }); + }); + + describe("getLengthOfObject", () => { + it("should return length of object", () => { + const obj = { a: 1, b: 2 }; + const result = getLengthOfObject(obj); + expect(result).toBe(2); + }); + }); + + describe("getUriToCompare", () => { + it('should return ROOT_FOLDER for "/"', () => { + const result = getUriToCompare("/"); + expect(result).toBe(ROOT_FOLDER); + }); + + it("should return URI with ROOT_FOLDER if not starting with PUBLIC_FOLDER", () => { + const result = getUriToCompare("/some/path"); + expect(result).toBe(`${ROOT_FOLDER}/some/path`); + }); + + it("should return URI if starting with PUBLIC_FOLDER", () => { + const result = getUriToCompare(`${PUBLIC_FOLDER}/some/path`); + expect(result).toBe(`${PUBLIC_FOLDER}/some/path`); + }); + }); + + describe("addChildrenToTreeOnLoad", () => { + it("should add children to tree structure", () => { + const treeStructure = [{ uri: "/" }]; + const childrenDataOfTreeNodes = { + [ROOT_FOLDER]: [{ uri: `${ROOT_FOLDER}/child` }], + }; + const pathWhereChildrensToBeAdded = [ROOT_FOLDER]; + const result = addChildrenToTreeOnLoad( + treeStructure, + childrenDataOfTreeNodes, + pathWhereChildrensToBeAdded, + ); + expect(result[0].children).toEqual([{ uri: `${ROOT_FOLDER}/child` }]); + }); + + it("should remove PUBLIC_FOLDER from children if uri is ROOT_FOLDER", () => { + const treeStructure = [{ uri: "/" }]; + const childrenDataOfTreeNodes = { + [ROOT_FOLDER]: [{ uri: PUBLIC_FOLDER }], + }; + const pathWhereChildrensToBeAdded = [ROOT_FOLDER]; + const result = addChildrenToTreeOnLoad( + treeStructure, + childrenDataOfTreeNodes, + pathWhereChildrensToBeAdded, + ); + expect(result[0].children).toEqual([]); + }); + }); + + describe("updateChangeToStore", () => { + it("should update store with new property value", () => { + const storeData = { key: "value" }; + const updateStore = jest.fn(); + updateChangeToStore(storeData, "newKey", "newValue", true, updateStore); + expect(updateStore).toHaveBeenCalledWith( + storeData, + { newKey: "newValue" }, + true, + ); + }); + }); +}); diff --git a/packages/jv-scheduler/test/utils/treeUtils.test.ts b/packages/jv-scheduler/test/utils/treeUtils.test.ts new file mode 100644 index 00000000..d010889e --- /dev/null +++ b/packages/jv-scheduler/test/utils/treeUtils.test.ts @@ -0,0 +1,109 @@ +import { + removeRootFolderPath, + addRootFolderPath, + isTreeNodeDisable, + getExpandedNodeDataFromUri, + addChildrenToTreeOnLoad, +} from "../../src/utils/treeUtils"; +import { + PUBLIC_FOLDER, + ROOT_FOLDER, +} from "../../src/constants/schedulerConstants"; +import { getUriParts } from "../../src/utils/schedulerUtils"; + +jest.mock("../../src/utils/schedulerUtils"); + +describe("treeUtils", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("removeRootFolderPath", () => { + it("should return folderUri if it starts with PUBLIC_FOLDER", () => { + const folderUri = `${PUBLIC_FOLDER}/some/path`; + expect(removeRootFolderPath(folderUri)).toBe(folderUri); + }); + + it("should remove root folder path if it does not start with PUBLIC_FOLDER", () => { + (getUriParts as jest.Mock).mockReturnValue(["root", "some", "path"]); + const folderUri = "/root/some/path"; + expect(removeRootFolderPath(folderUri)).toBe("/some/path"); + }); + }); + + describe("addRootFolderPath", () => { + it("should return folderUri if it starts with PUBLIC_FOLDER", () => { + const folderUri = `${PUBLIC_FOLDER}/some/path`; + expect(addRootFolderPath(folderUri)).toBe(folderUri); + }); + + it('should return ROOT_FOLDER if folderUri is "/"', () => { + expect(addRootFolderPath("/")).toBe(ROOT_FOLDER); + }); + + it("should add ROOT_FOLDER to folderUri if it does not start with PUBLIC_FOLDER", () => { + const folderUri = "/some/path"; + expect(addRootFolderPath(folderUri)).toBe(`${ROOT_FOLDER}${folderUri}`); + }); + }); + + describe("isTreeNodeDisable", () => { + it("should return true if permissionMask is not 1 or does not have 4", () => { + expect(isTreeNodeDisable({ permissionMask: 0 })).toBe(true); + expect(isTreeNodeDisable({ permissionMask: 2 })).toBe(true); + }); + + it("should return false if permissionMask is 1 or has 4", () => { + expect(isTreeNodeDisable({ permissionMask: 1 })).toBe(false); + expect(isTreeNodeDisable({ permissionMask: 4 })).toBe(false); + }); + }); + + describe("getExpandedNodeDataFromUri", () => { + it("should return expanded node data from URI", () => { + (getUriParts as jest.Mock).mockReturnValue(["root", "some", "path"]); + const resourceUri = "/root/some/path"; + const result = getExpandedNodeDataFromUri(resourceUri, false); + expect(result).toEqual(["/root", "/root/some", "/root/some/path"]); + }); + + it("should call itemHandlerCallback for each part of the URI", () => { + (getUriParts as jest.Mock).mockReturnValue(["root", "some", "path"]); + const resourceUri = "/root/some/path"; + const itemHandlerCallback = jest.fn(); + getExpandedNodeDataFromUri(resourceUri, false, itemHandlerCallback); + expect(itemHandlerCallback).toHaveBeenCalledTimes(3); + }); + }); + + describe("addChildrenToTreeOnLoad", () => { + it("should add children to tree structure", () => { + const treeStructure = [{ uri: "/" }]; + const childrenDataOfTreeNodes = { + [ROOT_FOLDER]: [{ uri: `${ROOT_FOLDER}/child` }], + }; + const pathWhereChildrensToBeAdded = [ROOT_FOLDER]; + const result = addChildrenToTreeOnLoad( + treeStructure, + childrenDataOfTreeNodes, + pathWhereChildrensToBeAdded, + ); + console.log(result, "result"); + expect(result[0].children).toEqual([{ uri: `${ROOT_FOLDER}/child` }]); + }); + + it("should remove PUBLIC_FOLDER from children if uri is ROOT_FOLDER", () => { + const treeStructure = [{ uri: "/" }]; + const childrenDataOfTreeNodes = { + [ROOT_FOLDER]: [{ uri: PUBLIC_FOLDER }], + }; + const pathWhereChildrensToBeAdded = [ROOT_FOLDER]; + const result = addChildrenToTreeOnLoad( + treeStructure, + childrenDataOfTreeNodes, + pathWhereChildrensToBeAdded, + ); + expect(result[0].children).toEqual([]); + }); + }); +}); diff --git a/packages/jv-scheduler/test/validations/scheduleValidators.test.ts b/packages/jv-scheduler/test/validations/scheduleValidators.test.ts new file mode 100644 index 00000000..f93e823b --- /dev/null +++ b/packages/jv-scheduler/test/validations/scheduleValidators.test.ts @@ -0,0 +1,179 @@ +import { + validator, + stateValidator, +} from "../../src/validations/scheduleValidators"; +import { ERROR_FIELDS } from "../../src/constants/schedulerConstants"; +import moment from "moment"; +import { SEND_ATTACHMENT } from "../../src/constants/schedulerConstants"; +import { checkPermissionOnResource } from "../../src/services/schedulerServices"; +import store from "../../src/store/store"; + +jest.mock("../../src/services/schedulerServices", () => ({ + checkPermissionOnResource: jest.fn(), +})); + +jest.mock("../../src/store/store", () => ({ + getState: jest.fn(), +})); + +describe("scheduleValidators", () => { + describe("validator", () => { + it("should validate SCHEDULE_JOB_NAME", () => { + expect(validator(ERROR_FIELDS.SCHEDULE_JOB_NAME, "")).toEqual({ + scheduleJobName: "error.schedule.job.name", + }); + expect( + validator(ERROR_FIELDS.SCHEDULE_JOB_NAME, "a".repeat(101)), + ).toEqual({ scheduleJobName: "error.schedule.job.name.too.long" }); + expect(validator(ERROR_FIELDS.SCHEDULE_JOB_NAME, "validName")).toEqual({ + scheduleJobName: undefined, + }); + }); + + it("should validate SCHEDULE_JOB_DESCRIPTION", () => { + expect( + validator(ERROR_FIELDS.SCHEDULE_JOB_DESCRIPTION, "a".repeat(251)), + ).toEqual({ + scheduleJobDescription: "error.schedule.job.description.too.long", + }); + expect( + validator(ERROR_FIELDS.SCHEDULE_JOB_DESCRIPTION, "validDescription"), + ).toEqual({ scheduleJobDescription: undefined }); + }); + + it("should validate RECURRENCE", () => { + expect(validator(ERROR_FIELDS.RECURRENCE, "")).toEqual({ + recurrenceInterval: "error.recurrence", + }); + expect(validator(ERROR_FIELDS.RECURRENCE, "1.5")).toEqual({ + recurrenceInterval: "error.recurrence.must.be.integer", + }); + expect(validator(ERROR_FIELDS.RECURRENCE, "-1")).toEqual({ + recurrenceInterval: "error.recurrence.must.be.integer", + }); + expect(validator(ERROR_FIELDS.RECURRENCE, "1")).toEqual({ + recurrenceInterval: undefined, + }); + }); + + it("should validate START_DATE", () => { + const extraParams = { startType: 2, outputTimeZone: "UTC" }; + (store.getState as jest.Mock).mockReturnValue({ + schedulerUIConfig: { timezone: "UTC" }, + } as any); + expect(validator(ERROR_FIELDS.START_DATE, "", extraParams)).toEqual({ + startDate: "error.start.date", + }); + expect( + validator( + ERROR_FIELDS.START_DATE, + moment().subtract(1, "day").toISOString(), + extraParams, + ), + ).toEqual({ startDate: "error.past.date" }); + expect( + validator( + ERROR_FIELDS.START_DATE, + moment().add(1, "day").toISOString(), + extraParams, + ), + ).toEqual({ startDate: undefined }); + }); + + it("should validate EMAIL_ADDRESS", () => { + expect(validator(ERROR_FIELDS.EMAIL_ADDRESS, "")).toEqual({ + address: "error.notifications.email.empty", + }); + expect(validator(ERROR_FIELDS.EMAIL_ADDRESS, "invalidEmail")).toEqual({ + address: "error.email.address", + }); + expect(validator(ERROR_FIELDS.EMAIL_ADDRESS, "test@example.com")).toEqual( + { address: undefined }, + ); + }); + + it("should validate EMAIL_SUBJECT", () => { + expect(validator(ERROR_FIELDS.EMAIL_SUBJECT, "")).toEqual({ + subject: "error.enter.subject", + }); + expect(validator(ERROR_FIELDS.EMAIL_SUBJECT, "a".repeat(101))).toEqual({ + subject: "error.subject.too.long", + }); + expect(validator(ERROR_FIELDS.EMAIL_SUBJECT, "validSubject")).toEqual({ + subject: undefined, + }); + }); + + it("should validate MESSAGE", () => { + expect(validator(ERROR_FIELDS.MESSAGE, "a".repeat(2001))).toEqual({ + messageText: "error.message.too.long", + }); + expect(validator(ERROR_FIELDS.MESSAGE, "validMessage")).toEqual({ + messageText: undefined, + }); + }); + + it("should validate FILE_NAME", () => { + expect(validator(ERROR_FIELDS.FILE_NAME, "")).toEqual({ + baseOutputFilename: "error.file.name", + }); + expect(validator(ERROR_FIELDS.FILE_NAME, "a".repeat(201))).toEqual({ + baseOutputFilename: "error.file.name.too.long", + }); + expect(validator(ERROR_FIELDS.FILE_NAME, "invalid/fileName")).toEqual({ + baseOutputFilename: "error.invalid.file.name", + }); + expect(validator(ERROR_FIELDS.FILE_NAME, "validFileName")).toEqual({ + baseOutputFilename: undefined, + }); + }); + + it("should validate OUTPUT_FORMAT", () => { + expect(validator(ERROR_FIELDS.OUTPUT_FORMAT, "")).toEqual({ + outputFormat: "error.output.format", + }); + expect(validator(ERROR_FIELDS.OUTPUT_FORMAT, "pdf")).toEqual({ + outputFormat: undefined, + }); + }); + + it("should validate SEND_TYPE", () => { + expect(validator(ERROR_FIELDS.SEND_TYPE, SEND_ATTACHMENT)).toEqual({ + folderURI: undefined, + }); + }); + + it("should validate FOLDER_URI", async () => { + ( + checkPermissionOnResource as jest.MockedFunction< + typeof checkPermissionOnResource + > + ).mockResolvedValue({ permissionMask: 1 }); + expect(await validator(ERROR_FIELDS.FOLDER_URI, "")).toEqual({ + folderURI: "error.folder.uri.required", + }); + expect(await validator(ERROR_FIELDS.FOLDER_URI, "invalid/uri")).toEqual({ + folderURI: "error.report.schedule.output.folder.resourceuri.format", + }); + expect(await validator(ERROR_FIELDS.FOLDER_URI, "validUri")).toEqual({ + folderURI: undefined, + }); + }); + }); + + describe("stateValidator", () => { + it("should validate state and return errors", async () => { + const state = { + startDate: moment().subtract(1, "day").format(), + scheduleJobName: "", + address: "invalid-email", + }; + const result = await stateValidator(state); + expect(result).toEqual({ + startDate: "error.past.date", + scheduleJobName: "error.schedule.job.name", + address: "error.email.address", + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 659b3a8b..c01264b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3349,6 +3349,13 @@ dependencies: "@types/react" "*" +"@types/redux-mock-store@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.5.0.tgz#53e9caa9f94c6839e5f34b1c9bcea09b911b67e0" + integrity sha512-jcscBazm6j05Hs6xYCca6psTUBbFT2wqMxT7wZEHAYFxHB/I8jYk7d5msrHUlDiSL02HdTqTmkK2oIV8i3C8DA== + dependencies: + redux "^4.0.5" + "@types/resolve@^1.20.2": version "1.20.6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" @@ -7445,6 +7452,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" @@ -8409,6 +8421,13 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux-mock-store@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.5.tgz#ec3676663c081c4ca5a6a14f1ac193b56c3220eb" + integrity sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg== + dependencies: + lodash.isplainobject "^4.0.6" + redux-thunk@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -8422,6 +8441,13 @@ redux@4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.5: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz" @@ -8874,16 +8900,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8966,14 +8983,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9715,16 +9725,7 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==