| @@ -0,0 +1,168 @@ | |||
| import { runSaga } from 'redux-saga'; | |||
| import * as api from '../../request/processesReguest'; | |||
| import { render } from "@testing-library/react"; | |||
| import * as redux from "react-redux"; | |||
| import SelectionProcessPage from '../../pages/selectionProcessPage/selectionProcessPage'; "../../pages/SelectionProcessPage/SelectionProcessPage"; | |||
| import store from "../../store"; | |||
| import "../../i18n"; | |||
| import { mockState } from "../../mockState"; | |||
| import { | |||
| FETCH_PROCESSES_REQ | |||
| } from "../../store/actions/processes/processesActionConstants"; | |||
| import { Router } from "react-router-dom"; | |||
| import history from "../../store/utils/history"; | |||
| import { getProcesses, getFilteredProcesses } from '../../store/saga/processSaga'; | |||
| import { setProcesses, setProcessesError } from '../../store/actions/processes/processesAction'; | |||
| describe("SelectionProcessPage render tests", () => { | |||
| const cont = ( | |||
| <redux.Provider store={store}> | |||
| <Router history={history}> | |||
| <SelectionProcessPage /> | |||
| </Router> | |||
| </redux.Provider> | |||
| ); | |||
| let spyOnUseSelector; | |||
| let spyOnUseDispatch; | |||
| let mockDispatch; | |||
| beforeEach(() => { | |||
| // Mock useSelector hook | |||
| spyOnUseSelector = jest.spyOn(redux, "useSelector"); | |||
| spyOnUseSelector.mockReturnValueOnce(mockState.selections).mockReturnValueOnce(mockState.selections.processes).mockReturnValueOnce(mockState.selections.statuses); | |||
| // Mock useDispatch hook | |||
| spyOnUseDispatch = jest.spyOn(redux, "useDispatch"); | |||
| // Mock dispatch function returned from useDispatch | |||
| mockDispatch = jest.fn(); | |||
| spyOnUseDispatch.mockReturnValue(mockDispatch); | |||
| }); | |||
| afterEach(() => { | |||
| jest.restoreAllMocks(); | |||
| }); | |||
| it("Should dispatch get processes request when rendered", () => { | |||
| render(cont); | |||
| expect(mockDispatch).toHaveBeenCalledWith({ | |||
| type: FETCH_PROCESSES_REQ, | |||
| }); | |||
| }); | |||
| it('should load and handle levels with processes in case of success', async () => { | |||
| // we push all dispatched actions to make assertions easier | |||
| // and our tests less brittle | |||
| const dispatchedActions = []; | |||
| // we don't want to perform an actual api call in our tests | |||
| // so we will mock the getAllUsers api with jest | |||
| // this will mutate the dependency which we may reset if other tests | |||
| // are dependent on it | |||
| const mockedCall = { data: mockState.selections.processes }; | |||
| api.getAllLevels = jest.fn(() => Promise.resolve(mockedCall)); | |||
| const fakeStore = { | |||
| getState: () => (mockState.selections.processes), | |||
| dispatch: action => dispatchedActions.push(action), | |||
| }; | |||
| // wait for saga to complete | |||
| await runSaga(fakeStore, getProcesses).done; | |||
| expect(api.getAllLevels.mock.calls.length).toBe(1); | |||
| expect(dispatchedActions).toContainEqual(setProcesses(mockedCall.data)); | |||
| }); | |||
| it('should handle processes load errors in case of failure', async () => { | |||
| const dispatchedActions = []; | |||
| // we simulate an error by rejecting the promise | |||
| // then we assert if our saga dispatched the action(s) correctly | |||
| const error = { response: { data: { message: mockState.selections.fetchSelectionsErrorMessage } } }; | |||
| api.getAllLevels = jest.fn(() => Promise.reject(error)); | |||
| const fakeStore = { | |||
| getState: () => (mockState.users.users), | |||
| dispatch: action => dispatchedActions.push(action), | |||
| }; | |||
| await runSaga(fakeStore, getProcesses).done; | |||
| expect(api.getAllLevels.mock.calls.length).toBe(1); | |||
| expect(dispatchedActions).toContainEqual(setProcessesError(error.response.data.message)); | |||
| }); | |||
| it('should load and handle levels with filtered processes in case of success', async () => { | |||
| // we push all dispatched actions to make assertions easier | |||
| // and our tests less brittle | |||
| const dispatchedActions = []; | |||
| const filter = { | |||
| statuses: ["Zakazan","Odrađen"], | |||
| dateStart: new Date(2023,0,5), | |||
| dateEnd: new Date(2024,1,1) | |||
| }; | |||
| const filteredData = []; | |||
| mockState.selections.processes.forEach(level => { | |||
| const filteredLevel = level; | |||
| filteredLevel.selectionProcesses = level.selectionProcesses.filter(v => v.date >= filter.dateStart && v.date <= filter.dateEnd && filter.statuses.includes(v.status)); | |||
| filteredData.push(filteredLevel); | |||
| }); | |||
| // we don't want to perform an actual api call in our tests | |||
| // so we will mock the getAllUsers api with jest | |||
| // this will mutate the dependency which we may reset if other tests | |||
| // are dependent on it | |||
| const mockedCall = { data: filteredData }; | |||
| api.getAllFilteredProcessesReq = jest.fn(() => Promise.resolve(mockedCall)); | |||
| const fakeStore = { | |||
| getState: () => (mockState.selections.processes), | |||
| dispatch: action => dispatchedActions.push(action), | |||
| }; | |||
| // wait for saga to complete | |||
| await runSaga(fakeStore, getFilteredProcesses,filter).done; | |||
| expect(api.getAllFilteredProcessesReq.mock.calls.length).toBe(1); | |||
| expect(dispatchedActions).toContainEqual(setProcesses(filteredData)); | |||
| }); | |||
| it('should handle process to set it done in case of success', async () => { | |||
| // we push all dispatched actions to make assertions easier | |||
| // and our tests less brittle | |||
| const dispatchedActions = []; | |||
| const filter = { | |||
| statuses: ["Zakazan","Odrađen"], | |||
| dateStart: new Date(2023,0,5), | |||
| dateEnd: new Date(2024,1,1) | |||
| }; | |||
| const filteredData = []; | |||
| mockState.selections.processes.forEach(level => { | |||
| const filteredLevel = level; | |||
| filteredLevel.selectionProcesses = level.selectionProcesses.filter(v => v.date >= filter.dateStart && v.date <= filter.dateEnd && filter.statuses.includes(v.status)); | |||
| filteredData.push(filteredLevel); | |||
| }); | |||
| // we don't want to perform an actual api call in our tests | |||
| // so we will mock the getAllUsers api with jest | |||
| // this will mutate the dependency which we may reset if other tests | |||
| // are dependent on it | |||
| const mockedCall = { data: filteredData }; | |||
| api.getAllFilteredProcessesReq = jest.fn(() => Promise.resolve(mockedCall)); | |||
| const fakeStore = { | |||
| getState: () => (mockState.selections.processes), | |||
| dispatch: action => dispatchedActions.push(action), | |||
| }; | |||
| // wait for saga to complete | |||
| await runSaga(fakeStore, getFilteredProcesses,filter).done; | |||
| expect(api.getAllFilteredProcessesReq.mock.calls.length).toBe(1); | |||
| expect(dispatchedActions).toContainEqual(setProcesses(filteredData)); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,61 @@ | |||
| import { fireEvent, render, screen } from "@testing-library/react"; | |||
| import * as redux from "react-redux"; | |||
| import SelectionProcessPage from "../../pages/selectionProcessPage/selectionProcessPage" | |||
| import store from "../../store"; | |||
| import "../../i18n"; | |||
| import { mockState } from "../../mockState"; | |||
| import { Router } from "react-router-dom"; | |||
| import history from "../../store/utils/history"; | |||
| describe("SelectionProcessPage render tests", () => { | |||
| const cont = ( | |||
| <redux.Provider store={store}> | |||
| <Router history={history}> | |||
| <SelectionProcessPage /> | |||
| </Router> | |||
| </redux.Provider> | |||
| ); | |||
| let spyOnUseSelector; | |||
| beforeEach(() => { | |||
| // Mock useSelector hook | |||
| spyOnUseSelector = jest.spyOn(redux, "useSelector"); | |||
| spyOnUseSelector.mockReturnValueOnce(mockState.selections).mockReturnValueOnce(mockState.selections.processes).mockReturnValueOnce(mockState.selections.statuses); | |||
| // spyOnUseSelector.mockReturnValue(mockState.selections); | |||
| }); | |||
| afterEach(() => { | |||
| jest.restoreAllMocks(); | |||
| }); | |||
| it("Should render", () => { | |||
| render(cont); | |||
| expect(screen.getByTestId("selections-page")).toBeDefined(); | |||
| }); | |||
| it("Should render a card foreach mocked level", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("selection-card").length).toBe(4); | |||
| }); | |||
| it("Should render a button with specific class for an enabled user", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("td-btn").length).toBe(0); | |||
| }); | |||
| it("Should render filter buttonn", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("userPageBtn").length).toBe(1); | |||
| }); | |||
| it("Drag and drop", () => { | |||
| const { container } = render(cont); | |||
| fireEvent.drop(container.getElementsByClassName("selection-card")[0], { | |||
| dataTransfer: { | |||
| getData: (type) => '{"id":32,"name":"random","status":"Kandidat primljen","date":null,"link":"link","applicant":{"applicantId":6,"firstName":"Safet","lastName":"Purkovic","position":"React Developer","dateOfApplication":"2021-05-05T00:00:00","cv":"dasdas","email":"safet@gmail.com","phoneNumber":"2313123","linkedlnLink":"sda","githubLink":null,"bitBucketLink":null,"experience":2,"applicationChannel":null,"typeOfEmployment":"Posao","technologyApplicants":[],"comments":[],"ads":[],"selectionProcesses":[{"status":"Kandidat primljen","date":null,"link":"link","scheduler":{"id":7,"firstName":"Safet","lastName":"Purkovic","email":"safet.purkovic@dilig.net","isEnabled":true},"selectionLevel":{"id":4,"name":"Konacna odluka"}}]},"selectionLevelId":4}' | |||
| }, | |||
| }) | |||
| }); | |||
| }) | |||
| @@ -24,15 +24,15 @@ const Selection = (props) => { | |||
| const errorMessage = useSelector(selectDoneProcessError); | |||
| const dispatch = useDispatch(); | |||
| const user = useSelector(selectAuthUser); | |||
| const dropItem = (e, selId) => { | |||
| var data = e.dataTransfer.getData("text/plain"); | |||
| const selectionProcess = JSON.parse(data); | |||
| if (selectionProcess.selectionLevelId < selId) { | |||
| dispatch(setDoneProcessReq({ | |||
| id: selectionProcess.id, | |||
| name: "radnom name", | |||
| name: "Some random name", | |||
| applicantId: selectionProcess.applicant.applicantId, | |||
| schedulerId: user.id | |||
| })); | |||
| @@ -69,7 +69,7 @@ const Selection = (props) => { | |||
| ); | |||
| return ( | |||
| <div dropppable="true" id={props.selection.id} className="selection-card" | |||
| <div data-testid="selection-level" dropppable="true" id={props.selection.id} className="selection-card" | |||
| onDragOver={e => dragOver(e)} | |||
| onDrop={e => dropItem(e, props.selection.id)} | |||
| > | |||
| @@ -78,8 +78,8 @@ const Selection = (props) => { | |||
| </div> | |||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||
| {applicants.length > 0 && renderList} | |||
| {applicants.length === 0 && <div className="sel-item-no-data"> | |||
| {applicants && applicants !== null && applicants?.length > 0 && renderList} | |||
| {applicants && applicants !== null && applicants?.length === 0 && <div className="sel-item-no-data"> | |||
| <div className="date"> | |||
| <p>Nema kandidata u selekciji</p> | |||
| </div> | |||
| @@ -0,0 +1,156 @@ | |||
| export const mockState = { | |||
| users: { | |||
| users: [ | |||
| { | |||
| id: 1, | |||
| firstName: "First", | |||
| lastName: "User", | |||
| email: "first@gmail.com", | |||
| isEnabled: false, | |||
| }, | |||
| { | |||
| id: 2, | |||
| firstName: "Second", | |||
| lastName: "User", | |||
| email: "second@gmail.com", | |||
| isEnabled: true, | |||
| }, | |||
| { | |||
| id: 3, | |||
| firstName: "Third", | |||
| lastName: "User", | |||
| email: "third@gmail.com", | |||
| isEnabled: false, | |||
| }, | |||
| ], | |||
| selected: {}, | |||
| fetchUsersErrorMessage: "Server error", | |||
| toggleEnableErrorMessage: "", | |||
| }, | |||
| selections: { | |||
| process:{doneProcess: false}, | |||
| processes: [ | |||
| { | |||
| id: 1, | |||
| name: "HR intervju", | |||
| selectionProcesses: [ | |||
| { | |||
| id: 1, | |||
| name: "sel1", | |||
| status: "Odrađen", | |||
| date: new Date(2023, 1, 1, 12, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 1, | |||
| applicant: { | |||
| applicantId: 1, | |||
| firstName: "Dzenis", | |||
| lastName: "Hadzifejzovic", | |||
| }, | |||
| }, | |||
| { | |||
| id: 2, | |||
| name: "sel2", | |||
| status: "Odrađen", | |||
| date: new Date(2023, 1, 1, 13, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 1, | |||
| applicant: { | |||
| applicantId: 2, | |||
| firstName: "Meris", | |||
| lastName: "Ahmatovic", | |||
| }, | |||
| }, | |||
| { | |||
| id: 3, | |||
| name: "sel3", | |||
| status: "Zakazan", | |||
| date: new Date(2023, 1, 10, 12, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 1, | |||
| applicant: { | |||
| applicantId: 3, | |||
| firstName: "Ermin", | |||
| lastName: "Bronja", | |||
| }, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| id: 2, | |||
| name: "Screening test", | |||
| selectionProcesses: [ | |||
| { | |||
| id: 4, | |||
| name: "sel4", | |||
| status: "Čeka na zakazivanje", | |||
| date: new Date(2023, 1, 10, 12, 0), | |||
| selectionLevelId: 2, | |||
| applicant: { | |||
| applicantId: 1, | |||
| firstName: "Dzenis", | |||
| lastName: "Hadzifejzovic", | |||
| }, | |||
| }, | |||
| { | |||
| id: 5, | |||
| name: "sel5", | |||
| status: "Odrađen", | |||
| date: new Date(2023, 1, 3, 12, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 1, | |||
| applicant: { | |||
| applicantId: 2, | |||
| firstName: "Meris", | |||
| lastName: "Ahmatovic", | |||
| }, | |||
| } | |||
| ], | |||
| }, | |||
| { | |||
| id: 3, | |||
| name: "Tehnicki intervju", | |||
| selectionProcesses: [ | |||
| { | |||
| id: 6, | |||
| name: "sel6", | |||
| status: "Zakazan", | |||
| date: new Date(2023, 1, 11, 10, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 3, | |||
| applicant: { | |||
| applicantId: 2, | |||
| firstName: "Meris", | |||
| lastName: "Ahmatovic", | |||
| }, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| id: 4, | |||
| name: "Konacna odluka", | |||
| selectionProcesses: [ | |||
| { | |||
| id: 6, | |||
| name: "sel6", | |||
| status: "Zakazan", | |||
| date: new Date(2023, 1, 11, 10, 0), | |||
| link: "http://google.com", | |||
| selectionLevelId: 3, | |||
| applicant: { | |||
| applicantId: 2, | |||
| firstName: "Meris", | |||
| lastName: "Ahmatovic", | |||
| }, | |||
| }, | |||
| ], | |||
| } | |||
| ], | |||
| selected: {}, | |||
| fetchSelectionsErrorMessage: "Server error", | |||
| statuses:[ | |||
| {"isChecked":false,"name":"Zakazan"}, | |||
| {"isChecked":false,"name":"Odrađen"}, | |||
| {"isChecked":false,"name":"Čeka na zakazivanje"}, | |||
| {"isChecked":false,"name":"Čeka se odgovor"}] | |||
| } | |||
| }; | |||
| @@ -53,13 +53,13 @@ const SelectionProcessPage = ({history}) => { | |||
| }; | |||
| const renderList = processes.map((item, index) => { | |||
| const renderList = processes?.map((item, index) => { | |||
| return <Selection selection={item} key={index} history={history}/> | |||
| } | |||
| ); | |||
| return ( | |||
| <> | |||
| <div data-testid="selections-page"> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| <SelectionFilter /> | |||
| @@ -99,7 +99,7 @@ const SelectionProcessPage = ({history}) => { | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </> | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -7,6 +7,7 @@ import { JWT_TOKEN } from "../../constants/localStorage"; | |||
| import { setDoneProcess, setDoneProcessError } from "../actions/processes/processAction"; | |||
| import { setApplicant, setApplicantError } from "../actions/processes/applicantAction"; | |||
| import { FETCH_PROCESSES_REQ, FETCH_FILTERED_PROCESSES_REQ ,PUT_PROCESS_REQ, FETCH_APPLICANT_PROCESSES_REQ } from "../actions/processes/processesActionConstants"; | |||
| import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||
| export function* getProcesses() { | |||
| try { | |||
| @@ -15,7 +16,10 @@ export function* getProcesses() { | |||
| const result = yield call(getAllLevels); | |||
| yield put(setProcesses(result.data)); | |||
| } catch (error) { | |||
| yield put(setProcessesError(error)); | |||
| if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(setProcessesError(errorMessage)); | |||
| } | |||
| } | |||
| } | |||
| @@ -27,7 +31,10 @@ export function* getFilteredProcesses(payload) { | |||
| const result = yield call(getAllFilteredProcessesReq, payload.payload); | |||
| yield put(setProcesses(result.data)); | |||
| } catch (error) { | |||
| yield put(setProcessesError(error)); | |||
| if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(setProcessesError(errorMessage)); | |||
| } | |||
| } | |||
| } | |||
| @@ -38,8 +45,10 @@ export function* finishProcess(payload) { | |||
| const model = payload.payload; | |||
| const result = yield call(doneProcess,model); | |||
| yield put(setDoneProcess(result.data)); | |||
| } catch (error) { | |||
| yield put(setDoneProcessError(error)); | |||
| } catch (error) { if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(setDoneProcessError(errorMessage)); | |||
| } | |||
| } | |||
| } | |||
| @@ -51,7 +60,10 @@ export function* getApplicantProcesses(payload) { | |||
| const {data} = yield call(getProcessesOfApplicant,id); | |||
| yield put(setApplicant(data)); | |||
| } catch (error) { | |||
| yield put(setApplicantError(error)); | |||
| if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(setApplicantError(errorMessage)); | |||
| } | |||
| } | |||
| } | |||