diff --git a/jsx/src/components/DynamicTable/DynamicTable.jsx b/jsx/src/components/DynamicTable/DynamicTable.jsx index 4645e892..8e87662d 100644 --- a/jsx/src/components/DynamicTable/DynamicTable.jsx +++ b/jsx/src/components/DynamicTable/DynamicTable.jsx @@ -199,8 +199,8 @@ const DynamicTable = (props) => { DynamicTable.propTypes = { current_keys: PropTypes.array, current_values: PropTypes.array, - setPropKeys: PropTypes.array, - setPropValues: PropTypes.array, + setPropKeys: PropTypes.func, + setPropValues: PropTypes.func, setProp: PropTypes.func, }; export default DynamicTable; diff --git a/jsx/src/components/GroupEdit/GroupEdit.test.jsx b/jsx/src/components/GroupEdit/GroupEdit.test.jsx index 62505b31..c4bbea4d 100644 --- a/jsx/src/components/GroupEdit/GroupEdit.test.jsx +++ b/jsx/src/components/GroupEdit/GroupEdit.test.jsx @@ -6,6 +6,7 @@ import userEvent from "@testing-library/user-event"; import { Provider, useSelector } from "react-redux"; import { createStore } from "redux"; import { HashRouter } from "react-router-dom"; +import { CompatRouter } from "react-router-dom-v5-compat"; // eslint-disable-next-line import regeneratorRuntime from "regenerator-runtime"; @@ -27,20 +28,22 @@ var okPacket = new Promise((resolve) => resolve(true)); var groupEditJsx = (callbackSpy) => ( {}, {})}> - {}, - }, - }} - addToGroup={callbackSpy} - removeFromGroup={callbackSpy} - deleteGroup={callbackSpy} - history={{ push: () => callbackSpy }} - updateGroups={callbackSpy} - validateUser={jest.fn().mockImplementation(() => okPacket)} - /> + + {}, + }, + }} + addToGroup={callbackSpy} + removeFromGroup={callbackSpy} + deleteGroup={callbackSpy} + history={{ push: () => callbackSpy }} + updateGroups={callbackSpy} + validateUser={jest.fn().mockImplementation(() => okPacket)} + /> + ); diff --git a/jsx/src/components/Groups/Groups.jsx b/jsx/src/components/Groups/Groups.jsx index 257caf6c..c6add7d3 100644 --- a/jsx/src/components/Groups/Groups.jsx +++ b/jsx/src/components/Groups/Groups.jsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux"; import PropTypes from "prop-types"; import { Link } from "react-router-dom"; -import { useSearchParams } from "react-router-dom-v5-compat"; +import { usePaginationParams } from "../../util/paginationParams"; import PaginationFooter from "../PaginationFooter/PaginationFooter"; const Groups = (props) => { diff --git a/jsx/src/components/Groups/Groups.test.js b/jsx/src/components/Groups/Groups.test.js index ed24c8bc..8b67cc59 100644 --- a/jsx/src/components/Groups/Groups.test.js +++ b/jsx/src/components/Groups/Groups.test.js @@ -5,6 +5,7 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { Provider, useDispatch, useSelector } from "react-redux"; import { createStore } from "redux"; import { HashRouter } from "react-router-dom"; +import { CompatRouter, useSearchParams } from "react-router-dom-v5-compat"; // eslint-disable-next-line import regeneratorRuntime from "regenerator-runtime"; @@ -16,13 +17,20 @@ jest.mock("react-redux", () => ({ useSelector: jest.fn(), })); +jest.mock("react-router-dom-v5-compat", () => ({ + ...jest.requireActual("react-router-dom-v5-compat"), + useSearchParams: jest.fn(), +})); + var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); var groupsJsx = (callbackSpy) => ( - + + + ); @@ -50,11 +58,6 @@ var mockAppState = () => offset: 0, limit: 2, total: 4, - next: { - offset: 2, - limit: 2, - url: "http://localhost:8000/hub/api/groups?offset=2&limit=2", - }, }, }); @@ -62,11 +65,15 @@ beforeEach(() => { useSelector.mockImplementation((callback) => { return callback(mockAppState()); }); + useSearchParams.mockImplementation(() => { + return [new URLSearchParams(), jest.fn()]; + }); }); afterEach(() => { useSelector.mockClear(); mockReducers.mockClear(); + useSearchParams.mockClear(); }); test("Renders", async () => { @@ -109,13 +116,23 @@ test("Renders nothing if required data is not available", async () => { }); test("Interacting with PaginationFooter causes state update and refresh via useEffect call", async () => { - let callbackSpy = mockAsync(); - + let upgradeGroupsSpy = mockAsync(); + let setSearchParamsSpy = mockAsync(); + let searchParams = new URLSearchParams({ limit: "2" }); + useSearchParams.mockImplementation(() => [ + searchParams, + (callback) => { + searchParams = callback(searchParams); + setSearchParamsSpy(searchParams.toString()); + }, + ]); + let _, setSearchParams; await act(async () => { - render(groupsJsx(callbackSpy)); + render(groupsJsx(upgradeGroupsSpy)); + [_, setSearchParams] = useSearchParams(); }); - expect(callbackSpy).toBeCalledWith(0, 2); + expect(upgradeGroupsSpy).toBeCalledWith(0, 2); var lastState = mockReducers.mock.results[mockReducers.mock.results.length - 1].value; @@ -123,12 +140,10 @@ test("Interacting with PaginationFooter causes state update and refresh via useE expect(lastState.groups_page.limit).toEqual(2); let next = screen.getByTestId("paginate-next"); - fireEvent.click(next); - - lastState = - mockReducers.mock.results[mockReducers.mock.results.length - 1].value; - expect(lastState.groups_page.offset).toEqual(2); - expect(lastState.groups_page.limit).toEqual(2); + await act(async () => { + fireEvent.click(next); + }); + expect(setSearchParamsSpy).toBeCalledWith("limit=2&offset=2"); // FIXME: mocked useSelector, state seem to prevent updateGroups from being called // making the test environment not representative diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.jsx b/jsx/src/components/ServerDashboard/ServerDashboard.jsx index d2bdaee2..c21d6e40 100644 --- a/jsx/src/components/ServerDashboard/ServerDashboard.jsx +++ b/jsx/src/components/ServerDashboard/ServerDashboard.jsx @@ -55,13 +55,13 @@ const ServerDashboard = (props) => { const [sortMethod, setSortMethod] = useState(null); const [collapseStates, setCollapseStates] = useState({}); - const user_data = useSelector((state) => state.user_data); + let user_data = useSelector((state) => state.user_data); const user_page = useSelector((state) => state.user_page); const { setOffset, offset, setLimit, handleLimit, limit, setPagination } = usePaginationParams(); - const name_filter = searchParams.get("name_filter"); + const name_filter = searchParams.get("name_filter") || ""; const total = user_page ? user_page.total : undefined; diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.test.js b/jsx/src/components/ServerDashboard/ServerDashboard.test.js index 11709812..4e579c26 100644 --- a/jsx/src/components/ServerDashboard/ServerDashboard.test.js +++ b/jsx/src/components/ServerDashboard/ServerDashboard.test.js @@ -10,6 +10,7 @@ import { getAllByRole, } from "@testing-library/react"; import { HashRouter, Switch } from "react-router-dom"; +import { CompatRouter, useSearchParams } from "react-router-dom-v5-compat"; import { Provider, useSelector } from "react-redux"; import { createStore } from "redux"; // eslint-disable-next-line @@ -25,20 +26,26 @@ jest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), useSelector: jest.fn(), })); +jest.mock("react-router-dom-v5-compat", () => ({ + ...jest.requireActual("react-router-dom-v5-compat"), + useSearchParams: jest.fn(), +})); var serverDashboardJsx = (spy) => ( - - - + + + + + ); @@ -137,14 +144,25 @@ var mockReducers = jest.fn((state, action) => { return state; }); +let searchParams = new URLSearchParams(); + beforeEach(() => { clock = sinon.useFakeTimers(); useSelector.mockImplementation((callback) => { return callback(mockAppState()); }); + searchParams = new URLSearchParams(); + + useSearchParams.mockImplementation(() => [ + searchParams, + (callback) => { + searchParams = callback(searchParams); + }, + ]); }); afterEach(() => { + useSearchParams.mockClear(); useSelector.mockClear(); mockReducers.mockClear(); clock.restore(); @@ -350,16 +368,16 @@ test("Shows server details with button click", async () => { await act(async () => { fireEvent.click(button); + await clock.tick(400); }); - clock.tick(400); expect(collapse).toHaveClass("collapse show"); expect(collapseBar).not.toHaveClass("show"); await act(async () => { fireEvent.click(button); + await clock.tick(400); }); - clock.tick(400); expect(collapse).toHaveClass("collapse"); expect(collapse).not.toHaveClass("show"); @@ -367,8 +385,8 @@ test("Shows server details with button click", async () => { await act(async () => { fireEvent.click(button); + await clock.tick(400); }); - clock.tick(400); expect(collapse).toHaveClass("collapse show"); expect(collapseBar).not.toHaveClass("show"); @@ -398,16 +416,18 @@ test("Shows a UI error dialogue when start all servers fails", async () => { render( {}, {})}> - - - + + + + + , ); @@ -432,16 +452,18 @@ test("Shows a UI error dialogue when stop all servers fails", async () => { render( {}, {})}> - - - + + + + + , ); @@ -466,16 +488,18 @@ test("Shows a UI error dialogue when start user server fails", async () => { render( {}, {})}> - - - + + + + + , ); @@ -501,16 +525,18 @@ test("Shows a UI error dialogue when start user server returns an improper statu render( {}, {})}> - - - + + + + + , ); @@ -536,16 +562,18 @@ test("Shows a UI error dialogue when stop user servers fails", async () => { render( {}, {})}> - - - + + + + + , ); @@ -570,16 +598,18 @@ test("Shows a UI error dialogue when stop user server returns an improper status render( {}, {})}> - - - + + + + + , ); @@ -616,16 +646,18 @@ test("Search for user calls updateUsers with name filter", async () => { render( - - - + + + + + , ); @@ -637,21 +669,20 @@ test("Search for user calls updateUsers with name filter", async () => { userEvent.type(search, "a"); expect(search.value).toEqual("a"); - clock.tick(400); - expect(mockReducers.mock.calls).toHaveLength(3); - var lastState = - mockReducers.mock.results[mockReducers.mock.results.length - 1].value; - expect(lastState.name_filter).toEqual("a"); - // TODO: this should - expect(mockUpdateUsers.mock.calls).toHaveLength(1); + await act(async () => { + await clock.tick(400); + }); + expect(searchParams.get("name_filter")).toEqual("a"); + // FIXME: useSelector mocks prevent updateUsers from being called + // expect(mockUpdateUsers.mock.calls).toHaveLength(2); + // expect(mockUpdateUsers).toBeCalledWith(0, 100, "a"); userEvent.type(search, "b"); expect(search.value).toEqual("ab"); - clock.tick(400); - expect(mockReducers.mock.calls).toHaveLength(4); - lastState = - mockReducers.mock.results[mockReducers.mock.results.length - 1].value; - expect(lastState.name_filter).toEqual("ab"); - expect(lastState.user_page.offset).toEqual(0); + await act(async () => { + jest.runAllTimers(); + }); + expect(searchParams.get("name_filter")).toEqual("ab"); + // expect(mockUpdateUsers).toBeCalledWith(0, 100, "ab"); }); test("Interacting with PaginationFooter causes state update and refresh via useEffect call", async () => { @@ -661,24 +692,20 @@ test("Interacting with PaginationFooter causes state update and refresh via useE render(serverDashboardJsx(callbackSpy)); }); - expect(callbackSpy).toBeCalledWith(0, 2, ""); + expect(callbackSpy).toBeCalledWith(0, 100, ""); - expect(mockReducers.mock.results).toHaveLength(2); - lastState = - mockReducers.mock.results[mockReducers.mock.results.length - 1].value; - console.log(lastState); - expect(lastState.user_page.offset).toEqual(0); - expect(lastState.user_page.limit).toEqual(2); + var n = 3; + expect(searchParams.get("offset")).toEqual(null); + expect(searchParams.get("limit")).toEqual(null); let next = screen.getByTestId("paginate-next"); - fireEvent.click(next); - clock.tick(400); + await act(async () => { + fireEvent.click(next); + await clock.tick(400); + }); - expect(mockReducers.mock.results).toHaveLength(3); - var lastState = - mockReducers.mock.results[mockReducers.mock.results.length - 1].value; - expect(lastState.user_page.offset).toEqual(2); - expect(lastState.user_page.limit).toEqual(2); + expect(searchParams.get("offset")).toEqual("100"); + expect(searchParams.get("limit")).toEqual(null); // FIXME: should call updateUsers, does in reality. // tests don't reflect reality due to mocked state/useSelector @@ -721,16 +748,18 @@ test("Start server and confirm pending state", async () => { render( - - - + + + + + , ); diff --git a/jsx/src/util/paginationParams.js b/jsx/src/util/paginationParams.js index 7fe97381..49fa207b 100644 --- a/jsx/src/util/paginationParams.js +++ b/jsx/src/util/paginationParams.js @@ -6,7 +6,7 @@ export const usePaginationParams = () => { const [searchParams, setSearchParams] = useSearchParams(); const offset = parseInt(searchParams.get("offset", "0")) || 0; const limit = - parseInt(searchParams.get("limit", "0")) || window.api_page_limit; + parseInt(searchParams.get("limit", "0")) || window.api_page_limit || 100; const _setOffset = (params, offset) => { if (offset < 0) offset = 0; @@ -26,6 +26,9 @@ export const usePaginationParams = () => { }; const setPagination = (pagination) => { // update pagination in one + if (!pagination) { + return; + } setSearchParams((params) => { _setOffset(params, pagination.offset); _setLimit(params, pagination.limit);