diff --git a/.gitignore b/.gitignore index 5ff18d83..338f2f00 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ htmlcov pip-wheel-metadata docs/source/reference/metrics.rst oldest-requirements.txt +jupyterhub-proxy.pid diff --git a/jsx/.eslintrc.json b/jsx/.eslintrc.json index 673f8ecb..191fd680 100644 --- a/jsx/.eslintrc.json +++ b/jsx/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": ["plugin:react/recommended"], "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true diff --git a/jsx/src/App.jsx b/jsx/src/App.jsx index 9fa36e7c..7047e50a 100644 --- a/jsx/src/App.jsx +++ b/jsx/src/App.jsx @@ -2,11 +2,11 @@ import React, { Component, useEffect } from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import { createStore } from "redux"; -import { Button } from "react-bootstrap"; +import { compose } from "recompose"; import { initialState, reducers } from "./Store"; import { jhapiRequest } from "./util/jhapiUtil"; +import withAPI from "./util/withAPI"; import { HashRouter, Switch, Route, Link } from "react-router-dom"; -import { createBrowserHistory } from "history"; import ServerDashboard from "./components/ServerDashboard/ServerDashboard"; import Groups from "./components/Groups/Groups"; @@ -37,12 +37,32 @@ const App = (props) => { - - - - - - + + + + + + diff --git a/jsx/src/components/AddUser/AddUser.jsx b/jsx/src/components/AddUser/AddUser.jsx index af6ceb60..5407e566 100644 --- a/jsx/src/components/AddUser/AddUser.jsx +++ b/jsx/src/components/AddUser/AddUser.jsx @@ -107,17 +107,4 @@ AddUser.propTypes = { }), }; -const withUserAPI = withProps((props) => ({ - addUsers: (usernames, admin) => - jhapiRequest("/users", "POST", { usernames, admin }), - failRegexEvent: () => - alert( - "Removed " + - JSON.stringify(removed_users) + - " for either containing special characters or being too short." - ), - refreshUserData: () => - jhapiRequest("/users", "GET").then((data) => data.json()), -})); - -export default compose(withUserAPI)(AddUser); +export default AddUser; diff --git a/jsx/src/components/AddUser/AddUser.test.js b/jsx/src/components/AddUser/AddUser.test.js index 4d5a2909..ec5bb165 100644 --- a/jsx/src/components/AddUser/AddUser.test.js +++ b/jsx/src/components/AddUser/AddUser.test.js @@ -1,31 +1,53 @@ import React from "react"; -import Enzyme, { shallow } from "enzyme"; -import AddUser from "./AddUser.pre"; +import Enzyme, { mount } from "enzyme"; +import AddUser from "./AddUser"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { Provider, useDispatch } from "react-redux"; +import { createStore } from "redux"; +import { HashRouter } from "react-router-dom"; Enzyme.configure({ adapter: new Adapter() }); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: jest.fn(), +})); + describe("AddUser Component: ", () => { var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); var addUserJsx = (callbackSpy) => ( - {} }} - /> + {}, {})}> + + {} }} + /> + + ); + beforeEach(() => { + useDispatch.mockImplementation((callback) => { + return () => {}; + }); + }); + + afterEach(() => { + useDispatch.mockClear(); + }); + it("Renders", () => { - let component = shallow(addUserJsx(mockAsync())); + let component = mount(addUserJsx(mockAsync())); expect(component.find(".container").length).toBe(1); }); it("Removes users when they fail Regex", () => { let callbackSpy = mockAsync(), - component = shallow(addUserJsx(callbackSpy)), + component = mount(addUserJsx(callbackSpy)), textarea = component.find("textarea").first(); textarea.simulate("blur", { target: { value: "foo\nbar\n!!*&*" } }); let submit = component.find("#submit"); @@ -35,7 +57,7 @@ describe("AddUser Component: ", () => { it("Correctly submits admin", () => { let callbackSpy = mockAsync(), - component = shallow(addUserJsx(callbackSpy)), + component = mount(addUserJsx(callbackSpy)), input = component.find("input").first(); input.simulate("change", { target: { checked: true } }); let submit = component.find("#submit"); diff --git a/jsx/src/components/CreateGroup/CreateGroup.jsx b/jsx/src/components/CreateGroup/CreateGroup.jsx index 355091f1..431c2e18 100644 --- a/jsx/src/components/CreateGroup/CreateGroup.jsx +++ b/jsx/src/components/CreateGroup/CreateGroup.jsx @@ -81,16 +81,4 @@ CreateGroup.propTypes = { }), }; -const withGroupsAPI = withProps((props) => ({ - createGroup: (groupName) => jhapiRequest("/groups/" + groupName, "POST"), - failRegexEvent: () => - alert( - "Removed " + - JSON.stringify(removed_users) + - " for either containing special characters or being too short." - ), - refreshGroupsData: () => - jhapiRequest("/groups", "GET").then((data) => data.json()), -})); - -export default compose(withGroupsAPI)(CreateGroup); +export default CreateGroup; diff --git a/jsx/src/components/CreateGroup/CreateGroup.test.js b/jsx/src/components/CreateGroup/CreateGroup.test.js index 74d09a32..ed0a0790 100644 --- a/jsx/src/components/CreateGroup/CreateGroup.test.js +++ b/jsx/src/components/CreateGroup/CreateGroup.test.js @@ -1,30 +1,52 @@ import React from "react"; -import Enzyme, { mount, shallow } from "enzyme"; -import CreateGroup from "./CreateGroup.pre"; +import Enzyme, { mount } from "enzyme"; +import CreateGroup from "./CreateGroup"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { Provider, useDispatch } from "react-redux"; +import { createStore } from "redux"; +import { HashRouter } from "react-router-dom"; Enzyme.configure({ adapter: new Adapter() }); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: jest.fn(), +})); + describe("CreateGroup Component: ", () => { var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); var createGroupJsx = (callbackSpy) => ( - {} }} - /> + {}, {})}> + + {} }} + /> + + ); + beforeEach(() => { + useDispatch.mockImplementation((callback) => { + return () => {}; + }); + }); + + afterEach(() => { + useDispatch.mockClear(); + }); + it("Renders", () => { - let component = shallow(createGroupJsx()); + let component = mount(createGroupJsx()); expect(component.find(".container").length).toBe(1); }); it("Calls createGroup and refreshGroupsData on submit", () => { let callbackSpy = mockAsync(), - component = shallow(createGroupJsx(callbackSpy)), + component = mount(createGroupJsx(callbackSpy)), input = component.find("input").first(), submit = component.find("#submit").first(); input.simulate("change", { target: { value: "" } }); diff --git a/jsx/src/components/EditUser/EditUser.jsx b/jsx/src/components/EditUser/EditUser.jsx index f899b205..26d52543 100644 --- a/jsx/src/components/EditUser/EditUser.jsx +++ b/jsx/src/components/EditUser/EditUser.jsx @@ -1,12 +1,8 @@ import React, { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { compose, withProps } from "recompose"; +import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; - import { Link } from "react-router-dom"; -import { jhapiRequest } from "../../util/jhapiUtil"; - const EditUser = (props) => { var dispatch = useDispatch(); @@ -160,22 +156,4 @@ EditUser.propTypes = { refreshUserData: PropTypes.func, }; -const withUserAPI = withProps((props) => ({ - editUser: (username, updated_username, admin) => - jhapiRequest("/users/" + username, "PATCH", { - name: updated_username, - admin, - }), - deleteUser: (username) => jhapiRequest("/users/" + username, "DELETE"), - failRegexEvent: () => - alert( - "Cannot change username - either contains special characters or is too short." - ), - noChangeEvent: () => { - returns; - }, - refreshUserData: () => - jhapiRequest("/users", "GET").then((data) => data.json()), -})); - -export default compose(withUserAPI)(EditUser); +export default EditUser; diff --git a/jsx/src/components/EditUser/EditUser.test.js b/jsx/src/components/EditUser/EditUser.test.js index 462ffff4..5c879e6a 100644 --- a/jsx/src/components/EditUser/EditUser.test.js +++ b/jsx/src/components/EditUser/EditUser.test.js @@ -1,30 +1,54 @@ import React from "react"; -import Enzyme, { shallow } from "enzyme"; -import EditUser from "./EditUser.pre"; +import Enzyme, { mount } from "enzyme"; +import EditUser from "./EditUser"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { Provider, useDispatch } from "react-redux"; +import { createStore } from "redux"; +import { HashRouter } from "react-router-dom"; Enzyme.configure({ adapter: new Adapter() }); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: jest.fn(), +})); + describe("EditUser Component: ", () => { var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); var mockSync = () => jest.fn(); - var editUserJsx = (callbackSpy) => ( - {} }} - failRegexEvent={callbackSpy} - noChangeEvent={callbackSpy} - /> + var editUserJsx = (callbackSpy, empty) => ( + {}, {})}> + + {} }} + failRegexEvent={callbackSpy} + noChangeEvent={callbackSpy} + /> + + ); + beforeEach(() => { + useDispatch.mockImplementation((callback) => { + return () => {}; + }); + }); + + afterEach(() => { + useDispatch.mockClear(); + }); + it("Calls the delete user function when the button is pressed", () => { let callbackSpy = mockAsync(), - component = shallow(editUserJsx(callbackSpy)), + component = mount(editUserJsx(callbackSpy)), deleteUser = component.find("#delete-user"); deleteUser.simulate("click"); expect(callbackSpy).toHaveBeenCalled(); @@ -32,9 +56,15 @@ describe("EditUser Component: ", () => { it("Submits the edits when the button is pressed", () => { let callbackSpy = mockSync(), - component = shallow(editUserJsx(callbackSpy)), + component = mount(editUserJsx(callbackSpy)), submit = component.find("#submit"); submit.simulate("click"); expect(callbackSpy).toHaveBeenCalled(); }); + + it("Doesn't render when no data is provided", () => { + let callbackSpy = mockSync(), + component = mount(editUserJsx(callbackSpy, true)); + expect(component.find(".container").length).toBe(0); + }); }); diff --git a/jsx/src/components/GroupEdit/GroupEdit.jsx b/jsx/src/components/GroupEdit/GroupEdit.jsx index 2d1121ed..3e226706 100644 --- a/jsx/src/components/GroupEdit/GroupEdit.jsx +++ b/jsx/src/components/GroupEdit/GroupEdit.jsx @@ -137,14 +137,4 @@ GroupEdit.propTypes = { refreshGroupsData: PropTypes.func, }; -const withGroupsAPI = withProps((props) => ({ - addToGroup: (users, groupname) => - jhapiRequest("/groups/" + groupname + "/users", "POST", { users }), - removeFromGroup: (users, groupname) => - jhapiRequest("/groups/" + groupname + "/users", "DELETE", { users }), - deleteGroup: (name) => jhapiRequest("/groups/" + name, "DELETE"), - refreshGroupsData: () => - jhapiRequest("/groups", "GET").then((data) => data.json()), -})); - -export default compose(withGroupsAPI)(GroupEdit); +export default GroupEdit; diff --git a/jsx/src/components/GroupEdit/GroupEdit.test.jsx b/jsx/src/components/GroupEdit/GroupEdit.test.jsx index 0b0bbf42..aef1c3bd 100644 --- a/jsx/src/components/GroupEdit/GroupEdit.test.jsx +++ b/jsx/src/components/GroupEdit/GroupEdit.test.jsx @@ -1,39 +1,62 @@ import React from "react"; import Enzyme, { mount, shallow } from "enzyme"; -import GroupEdit from "./GroupEdit.pre"; +import GroupEdit from "./GroupEdit"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { Provider, useSelector } from "react-redux"; +import { createStore } from "redux"; import { HashRouter } from "react-router-dom"; Enzyme.configure({ adapter: new Adapter() }); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useSelector: jest.fn(), +})); + describe("GroupEdit Component: ", () => { var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); var groupEditJsx = (callbackSpy) => ( - {}, - }, - }} - addToGroup={callbackSpy} - removeFromGroup={callbackSpy} - deleteGroup={callbackSpy} - history={{ push: (a) => callbackSpy }} - refreshGroupsData={() => {}} - /> + {}, {})}> + + {}, + }, + }} + addToGroup={callbackSpy} + removeFromGroup={callbackSpy} + deleteGroup={callbackSpy} + history={{ push: (a) => callbackSpy }} + refreshGroupsData={callbackSpy} + /> + + ); - var deepGroupEditJsx = (callbackSpy) => ( - {groupEditJsx(callbackSpy)} - ); + var mockAppState = () => ({ + user_data: JSON.parse( + '[{"kind":"user","name":"foo","admin":true,"groups":[],"server":"/user/foo/","pending":null,"created":"2020-12-07T18:46:27.112695Z","last_activity":"2020-12-07T21:00:33.336354Z","servers":{"":{"name":"","last_activity":"2020-12-07T20:58:02.437408Z","started":"2020-12-07T20:58:01.508266Z","pending":null,"ready":true,"state":{"pid":28085},"url":"/user/foo/","user_options":{},"progress_url":"/hub/api/users/foo/server/progress"}}},{"kind":"user","name":"bar","admin":false,"groups":[],"server":null,"pending":null,"created":"2020-12-07T18:46:27.115528Z","last_activity":"2020-12-07T20:43:51.013613Z","servers":{}}]' + ), + }); + + beforeEach(() => { + useSelector.mockImplementation((callback) => { + return callback(mockAppState()); + }); + }); + + afterEach(() => { + useSelector.mockClear(); + }); it("Adds a newly selected user to group on submit", () => { let callbackSpy = mockAsync(), - component = mount(deepGroupEditJsx(callbackSpy)), + component = mount(groupEditJsx(callbackSpy)), unselected = component.find(".unselected"), submit = component.find("#submit"); unselected.simulate("click"); @@ -43,7 +66,7 @@ describe("GroupEdit Component: ", () => { it("Removes a user from group on submit", () => { let callbackSpy = mockAsync(), - component = mount(deepGroupEditJsx(callbackSpy)), + component = mount(groupEditJsx(callbackSpy)), selected = component.find(".selected"), submit = component.find("#submit"); selected.simulate("click"); @@ -53,9 +76,10 @@ describe("GroupEdit Component: ", () => { it("Calls deleteGroup on button click", () => { let callbackSpy = mockAsync(), - component = shallow(groupEditJsx(callbackSpy)), + component = mount(groupEditJsx(callbackSpy)), deleteGroup = component.find("#delete-group").first(); deleteGroup.simulate("click"); - expect(callbackSpy).toHaveBeenCalled(); + expect(callbackSpy).toHaveBeenNthCalledWith(1, "group"); + expect(callbackSpy).toHaveBeenNthCalledWith(2); }); }); diff --git a/jsx/src/components/Groups/Groups.jsx b/jsx/src/components/Groups/Groups.jsx index 1f92ae28..7465141d 100644 --- a/jsx/src/components/Groups/Groups.jsx +++ b/jsx/src/components/Groups/Groups.jsx @@ -102,21 +102,4 @@ Groups.propTypes = { }), }; -const withGroupsAPI = withProps((props) => ({ - refreshGroupsData: () => - jhapiRequest("/groups", "GET").then((data) => data.json()), - refreshUserData: () => - jhapiRequest("/users", "GET").then((data) => data.json()), - addUsersToGroup: (name, new_users) => - jhapiRequest("/groups/" + name + "/users", "POST", { - body: { users: new_users }, - json: true, - }), - removeUsersFromGroup: (name, removed_users) => - jhapiRequest("/groups/" + name + "/users", "DELETE", { - body: { users: removed_users }, - json: true, - }), -})); - -export default compose(withGroupsAPI)(Groups); +export default Groups; diff --git a/jsx/src/components/Groups/Groups.test.js b/jsx/src/components/Groups/Groups.test.js index f7396433..f1eed6b5 100644 --- a/jsx/src/components/Groups/Groups.test.js +++ b/jsx/src/components/Groups/Groups.test.js @@ -1,30 +1,58 @@ import React from "react"; -import Enzyme, { shallow } from "enzyme"; -import Groups from "./Groups.pre"; +import Enzyme, { mount } from "enzyme"; +import Groups from "./Groups"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { Provider, useSelector } from "react-redux"; +import { createStore } from "redux"; +import { HashRouter } from "react-router-dom"; Enzyme.configure({ adapter: new Adapter() }); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + describe("Groups Component: ", () => { var groupsJsx = () => ( - + {}, {})}> + + + + ); + var mockAppState = () => ({ + user_data: JSON.parse( + '[{"kind":"user","name":"foo","admin":true,"groups":[],"server":"/user/foo/","pending":null,"created":"2020-12-07T18:46:27.112695Z","last_activity":"2020-12-07T21:00:33.336354Z","servers":{"":{"name":"","last_activity":"2020-12-07T20:58:02.437408Z","started":"2020-12-07T20:58:01.508266Z","pending":null,"ready":true,"state":{"pid":28085},"url":"/user/foo/","user_options":{},"progress_url":"/hub/api/users/foo/server/progress"}}},{"kind":"user","name":"bar","admin":false,"groups":[],"server":null,"pending":null,"created":"2020-12-07T18:46:27.115528Z","last_activity":"2020-12-07T20:43:51.013613Z","servers":{}}]' + ), + groups_data: JSON.parse( + '[{"kind":"group","name":"testgroup","users":[]}, {"kind":"group","name":"testgroup2","users":["foo", "bar"]}]' + ), + }); + + beforeEach(() => { + useSelector.mockImplementation((callback) => { + return callback(mockAppState()); + }); + }); + + afterEach(() => { + useSelector.mockClear(); + }); + it("Renders groups_data prop into links", () => { - let component = shallow(groupsJsx()), + let component = mount(groupsJsx()), links = component.find(".group-edit-link"); expect(links.length).toBe(2); }); it("Renders nothing if required data is not available", () => { - let component = shallow(); + useSelector.mockImplementation((callback) => { + return callback({}); + }); + let component = mount(groupsJsx()); expect(component.html()).toBe("
"); }); }); diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.jsx b/jsx/src/components/ServerDashboard/ServerDashboard.jsx index 1724f9b4..d110f939 100644 --- a/jsx/src/components/ServerDashboard/ServerDashboard.jsx +++ b/jsx/src/components/ServerDashboard/ServerDashboard.jsx @@ -155,7 +155,7 @@ const ServerDashboard = (props) => { {/* Shutdown Jupyterhub */}