Combine API props, update tests for redux hooks

This commit is contained in:
Nathan Barber
2021-04-08 18:28:49 -04:00
parent 21f4988f24
commit 51deaa36f3
18 changed files with 347 additions and 222 deletions

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ htmlcov
pip-wheel-metadata pip-wheel-metadata
docs/source/reference/metrics.rst docs/source/reference/metrics.rst
oldest-requirements.txt oldest-requirements.txt
jupyterhub-proxy.pid

View File

@@ -1,7 +1,7 @@
{ {
"extends": ["plugin:react/recommended"], "extends": ["plugin:react/recommended"],
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 2018,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true

View File

@@ -2,11 +2,11 @@ import React, { Component, useEffect } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { Button } from "react-bootstrap"; import { compose } from "recompose";
import { initialState, reducers } from "./Store"; import { initialState, reducers } from "./Store";
import { jhapiRequest } from "./util/jhapiUtil"; import { jhapiRequest } from "./util/jhapiUtil";
import withAPI from "./util/withAPI";
import { HashRouter, Switch, Route, Link } from "react-router-dom"; import { HashRouter, Switch, Route, Link } from "react-router-dom";
import { createBrowserHistory } from "history";
import ServerDashboard from "./components/ServerDashboard/ServerDashboard"; import ServerDashboard from "./components/ServerDashboard/ServerDashboard";
import Groups from "./components/Groups/Groups"; import Groups from "./components/Groups/Groups";
@@ -37,12 +37,32 @@ const App = (props) => {
<Provider store={store}> <Provider store={store}>
<HashRouter> <HashRouter>
<Switch> <Switch>
<Route exact path="/" component={ServerDashboard} /> <Route
<Route exact path="/groups" component={Groups} /> exact
<Route exact path="/group-edit" component={GroupEdit} /> path="/"
<Route exact path="/create-group" component={CreateGroup} /> component={compose(withAPI)(ServerDashboard)}
<Route exact path="/add-users" component={AddUser} /> />
<Route exact path="/edit-user" component={EditUser} /> <Route exact path="/groups" component={compose(withAPI)(Groups)} />
<Route
exact
path="/group-edit"
component={compose(withAPI)(GroupEdit)}
/>
<Route
exact
path="/create-group"
component={compose(withAPI)(CreateGroup)}
/>
<Route
exact
path="/add-users"
component={compose(withAPI)(AddUser)}
/>
<Route
exact
path="/edit-user"
component={compose(withAPI)(EditUser)}
/>
</Switch> </Switch>
</HashRouter> </HashRouter>
</Provider> </Provider>

View File

@@ -107,17 +107,4 @@ AddUser.propTypes = {
}), }),
}; };
const withUserAPI = withProps((props) => ({ export default AddUser;
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);

View File

@@ -1,31 +1,53 @@
import React from "react"; import React from "react";
import Enzyme, { shallow } from "enzyme"; import Enzyme, { mount } from "enzyme";
import AddUser from "./AddUser.pre"; import AddUser from "./AddUser";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 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() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useDispatch: jest.fn(),
}));
describe("AddUser Component: ", () => { describe("AddUser Component: ", () => {
var mockAsync = () => var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var addUserJsx = (callbackSpy) => ( var addUserJsx = (callbackSpy) => (
<AddUser <Provider store={createStore(() => {}, {})}>
addUsers={callbackSpy} <HashRouter>
failRegexEvent={callbackSpy} <AddUser
refreshUserData={callbackSpy} addUsers={callbackSpy}
history={{ push: (a) => {} }} failRegexEvent={callbackSpy}
/> refreshUserData={callbackSpy}
history={{ push: (a) => {} }}
/>
</HashRouter>
</Provider>
); );
beforeEach(() => {
useDispatch.mockImplementation((callback) => {
return () => {};
});
});
afterEach(() => {
useDispatch.mockClear();
});
it("Renders", () => { it("Renders", () => {
let component = shallow(addUserJsx(mockAsync())); let component = mount(addUserJsx(mockAsync()));
expect(component.find(".container").length).toBe(1); expect(component.find(".container").length).toBe(1);
}); });
it("Removes users when they fail Regex", () => { it("Removes users when they fail Regex", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(addUserJsx(callbackSpy)), component = mount(addUserJsx(callbackSpy)),
textarea = component.find("textarea").first(); textarea = component.find("textarea").first();
textarea.simulate("blur", { target: { value: "foo\nbar\n!!*&*" } }); textarea.simulate("blur", { target: { value: "foo\nbar\n!!*&*" } });
let submit = component.find("#submit"); let submit = component.find("#submit");
@@ -35,7 +57,7 @@ describe("AddUser Component: ", () => {
it("Correctly submits admin", () => { it("Correctly submits admin", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(addUserJsx(callbackSpy)), component = mount(addUserJsx(callbackSpy)),
input = component.find("input").first(); input = component.find("input").first();
input.simulate("change", { target: { checked: true } }); input.simulate("change", { target: { checked: true } });
let submit = component.find("#submit"); let submit = component.find("#submit");

View File

@@ -81,16 +81,4 @@ CreateGroup.propTypes = {
}), }),
}; };
const withGroupsAPI = withProps((props) => ({ export default CreateGroup;
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);

View File

@@ -1,30 +1,52 @@
import React from "react"; import React from "react";
import Enzyme, { mount, shallow } from "enzyme"; import Enzyme, { mount } from "enzyme";
import CreateGroup from "./CreateGroup.pre"; import CreateGroup from "./CreateGroup";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 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() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useDispatch: jest.fn(),
}));
describe("CreateGroup Component: ", () => { describe("CreateGroup Component: ", () => {
var mockAsync = () => var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var createGroupJsx = (callbackSpy) => ( var createGroupJsx = (callbackSpy) => (
<CreateGroup <Provider store={createStore(() => {}, {})}>
createGroup={callbackSpy} <HashRouter>
refreshGroupsData={callbackSpy} <CreateGroup
history={{ push: () => {} }} createGroup={callbackSpy}
/> refreshGroupsData={callbackSpy}
history={{ push: () => {} }}
/>
</HashRouter>
</Provider>
); );
beforeEach(() => {
useDispatch.mockImplementation((callback) => {
return () => {};
});
});
afterEach(() => {
useDispatch.mockClear();
});
it("Renders", () => { it("Renders", () => {
let component = shallow(createGroupJsx()); let component = mount(createGroupJsx());
expect(component.find(".container").length).toBe(1); expect(component.find(".container").length).toBe(1);
}); });
it("Calls createGroup and refreshGroupsData on submit", () => { it("Calls createGroup and refreshGroupsData on submit", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(createGroupJsx(callbackSpy)), component = mount(createGroupJsx(callbackSpy)),
input = component.find("input").first(), input = component.find("input").first(),
submit = component.find("#submit").first(); submit = component.find("#submit").first();
input.simulate("change", { target: { value: "" } }); input.simulate("change", { target: { value: "" } });

View File

@@ -1,12 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { compose, withProps } from "recompose";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { jhapiRequest } from "../../util/jhapiUtil";
const EditUser = (props) => { const EditUser = (props) => {
var dispatch = useDispatch(); var dispatch = useDispatch();
@@ -160,22 +156,4 @@ EditUser.propTypes = {
refreshUserData: PropTypes.func, refreshUserData: PropTypes.func,
}; };
const withUserAPI = withProps((props) => ({ export default EditUser;
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);

View File

@@ -1,30 +1,54 @@
import React from "react"; import React from "react";
import Enzyme, { shallow } from "enzyme"; import Enzyme, { mount } from "enzyme";
import EditUser from "./EditUser.pre"; import EditUser from "./EditUser";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 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() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useDispatch: jest.fn(),
}));
describe("EditUser Component: ", () => { describe("EditUser Component: ", () => {
var mockAsync = () => var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var mockSync = () => jest.fn(); var mockSync = () => jest.fn();
var editUserJsx = (callbackSpy) => ( var editUserJsx = (callbackSpy, empty) => (
<EditUser <Provider store={createStore(() => {}, {})}>
location={{ state: { username: "foo", has_admin: false } }} <HashRouter>
deleteUser={callbackSpy} <EditUser
editUser={callbackSpy} location={
refreshUserData={mockSync()} empty ? {} : { state: { username: "foo", has_admin: false } }
history={{ push: (a) => {} }} }
failRegexEvent={callbackSpy} deleteUser={callbackSpy}
noChangeEvent={callbackSpy} editUser={callbackSpy}
/> refreshUserData={callbackSpy}
history={{ push: (a) => {} }}
failRegexEvent={callbackSpy}
noChangeEvent={callbackSpy}
/>
</HashRouter>
</Provider>
); );
beforeEach(() => {
useDispatch.mockImplementation((callback) => {
return () => {};
});
});
afterEach(() => {
useDispatch.mockClear();
});
it("Calls the delete user function when the button is pressed", () => { it("Calls the delete user function when the button is pressed", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(editUserJsx(callbackSpy)), component = mount(editUserJsx(callbackSpy)),
deleteUser = component.find("#delete-user"); deleteUser = component.find("#delete-user");
deleteUser.simulate("click"); deleteUser.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); expect(callbackSpy).toHaveBeenCalled();
@@ -32,9 +56,15 @@ describe("EditUser Component: ", () => {
it("Submits the edits when the button is pressed", () => { it("Submits the edits when the button is pressed", () => {
let callbackSpy = mockSync(), let callbackSpy = mockSync(),
component = shallow(editUserJsx(callbackSpy)), component = mount(editUserJsx(callbackSpy)),
submit = component.find("#submit"); submit = component.find("#submit");
submit.simulate("click"); submit.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); 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);
});
}); });

View File

@@ -137,14 +137,4 @@ GroupEdit.propTypes = {
refreshGroupsData: PropTypes.func, refreshGroupsData: PropTypes.func,
}; };
const withGroupsAPI = withProps((props) => ({ export default GroupEdit;
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);

View File

@@ -1,39 +1,62 @@
import React from "react"; import React from "react";
import Enzyme, { mount, shallow } from "enzyme"; import Enzyme, { mount, shallow } from "enzyme";
import GroupEdit from "./GroupEdit.pre"; import GroupEdit from "./GroupEdit";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { Provider, useSelector } from "react-redux";
import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useSelector: jest.fn(),
}));
describe("GroupEdit Component: ", () => { describe("GroupEdit Component: ", () => {
var mockAsync = () => var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" })); jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var groupEditJsx = (callbackSpy) => ( var groupEditJsx = (callbackSpy) => (
<GroupEdit <Provider store={createStore(() => {}, {})}>
location={{ <HashRouter>
state: { <GroupEdit
user_data: [{ name: "foo" }, { name: "bar" }], location={{
group_data: { users: ["foo"], name: "group" }, state: {
callback: () => {}, user_data: [{ name: "foo" }, { name: "bar" }],
}, group_data: { users: ["foo"], name: "group" },
}} callback: () => {},
addToGroup={callbackSpy} },
removeFromGroup={callbackSpy} }}
deleteGroup={callbackSpy} addToGroup={callbackSpy}
history={{ push: (a) => callbackSpy }} removeFromGroup={callbackSpy}
refreshGroupsData={() => {}} deleteGroup={callbackSpy}
/> history={{ push: (a) => callbackSpy }}
refreshGroupsData={callbackSpy}
/>
</HashRouter>
</Provider>
); );
var deepGroupEditJsx = (callbackSpy) => ( var mockAppState = () => ({
<HashRouter>{groupEditJsx(callbackSpy)}</HashRouter> 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", () => { it("Adds a newly selected user to group on submit", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = mount(deepGroupEditJsx(callbackSpy)), component = mount(groupEditJsx(callbackSpy)),
unselected = component.find(".unselected"), unselected = component.find(".unselected"),
submit = component.find("#submit"); submit = component.find("#submit");
unselected.simulate("click"); unselected.simulate("click");
@@ -43,7 +66,7 @@ describe("GroupEdit Component: ", () => {
it("Removes a user from group on submit", () => { it("Removes a user from group on submit", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = mount(deepGroupEditJsx(callbackSpy)), component = mount(groupEditJsx(callbackSpy)),
selected = component.find(".selected"), selected = component.find(".selected"),
submit = component.find("#submit"); submit = component.find("#submit");
selected.simulate("click"); selected.simulate("click");
@@ -53,9 +76,10 @@ describe("GroupEdit Component: ", () => {
it("Calls deleteGroup on button click", () => { it("Calls deleteGroup on button click", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(groupEditJsx(callbackSpy)), component = mount(groupEditJsx(callbackSpy)),
deleteGroup = component.find("#delete-group").first(); deleteGroup = component.find("#delete-group").first();
deleteGroup.simulate("click"); deleteGroup.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); expect(callbackSpy).toHaveBeenNthCalledWith(1, "group");
expect(callbackSpy).toHaveBeenNthCalledWith(2);
}); });
}); });

View File

@@ -102,21 +102,4 @@ Groups.propTypes = {
}), }),
}; };
const withGroupsAPI = withProps((props) => ({ export default Groups;
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);

View File

@@ -1,30 +1,58 @@
import React from "react"; import React from "react";
import Enzyme, { shallow } from "enzyme"; import Enzyme, { mount } from "enzyme";
import Groups from "./Groups.pre"; import Groups from "./Groups";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 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() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useSelector: jest.fn(),
useDispatch: jest.fn(),
}));
describe("Groups Component: ", () => { describe("Groups Component: ", () => {
var groupsJsx = () => ( var groupsJsx = () => (
<Groups <Provider store={createStore(() => {}, {})}>
user_data={JSON.parse( <HashRouter>
'[{"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 />
)} </HashRouter>
groups_data={JSON.parse( </Provider>
'[{"kind":"group","name":"testgroup","users":[]}, {"kind":"group","name":"testgroup2","users":["foo", "bar"]}]'
)}
/>
); );
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", () => { it("Renders groups_data prop into links", () => {
let component = shallow(groupsJsx()), let component = mount(groupsJsx()),
links = component.find(".group-edit-link"); links = component.find(".group-edit-link");
expect(links.length).toBe(2); expect(links.length).toBe(2);
}); });
it("Renders nothing if required data is not available", () => { it("Renders nothing if required data is not available", () => {
let component = shallow(<Groups />); useSelector.mockImplementation((callback) => {
return callback({});
});
let component = mount(groupsJsx());
expect(component.html()).toBe("<div></div>"); expect(component.html()).toBe("<div></div>");
}); });
}); });

View File

@@ -155,7 +155,7 @@ const ServerDashboard = (props) => {
{/* Shutdown Jupyterhub */} {/* Shutdown Jupyterhub */}
<Button <Button
variant="danger" variant="danger"
className="shutdown-button" id="shutdown-button"
onClick={shutdownHub} onClick={shutdownHub}
> >
Shutdown Hub Shutdown Hub
@@ -288,15 +288,4 @@ SortHandler.propTypes = {
callback: PropTypes.func, callback: PropTypes.func,
}; };
const withHubActions = withProps((props) => ({ export default ServerDashboard;
updateUsers: (cb) => jhapiRequest("/users", "GET"),
shutdownHub: () => jhapiRequest("/shutdown", "POST"),
startServer: (name) => jhapiRequest("/users/" + name + "/server", "POST"),
stopServer: (name) => jhapiRequest("/users/" + name + "/server", "DELETE"),
startAll: (names) =>
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
stopAll: (names) =>
names.map((e) => jhapiRequest("/users/" + e + "/server", "DELETE")),
}));
export default compose(withHubActions)(ServerDashboard);

View File

@@ -1,44 +1,34 @@
import React from "react"; import React from "react";
import Enzyme, { shallow, mount } from "enzyme"; import Enzyme, { shallow, mount } from "enzyme";
import ServerDashboard from "./ServerDashboard.pre"; import ServerDashboard from "./ServerDashboard";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { HashRouter, Switch } from "react-router-dom"; import { HashRouter, Switch } from "react-router-dom";
import { Provider, useSelector } from "react-redux";
import { createStore } from "redux";
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useSelector: jest.fn(),
}));
describe("ServerDashboard Component: ", () => { describe("ServerDashboard Component: ", () => {
var serverDashboardJsx = (callbackSpy) => ( var serverDashboardJsx = (callbackSpy) => (
<ServerDashboard <Provider store={createStore(() => {}, {})}>
user_data={JSON.parse( <HashRouter>
'[{"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":{}}]' <Switch>
)} <ServerDashboard
updateUsers={callbackSpy} updateUsers={callbackSpy}
shutdownHub={callbackSpy} shutdownHub={callbackSpy}
startServer={callbackSpy} startServer={callbackSpy}
stopServer={callbackSpy} stopServer={callbackSpy}
startAll={callbackSpy} startAll={callbackSpy}
stopAll={callbackSpy} stopAll={callbackSpy}
dispatch={callbackSpy} />
/> </Switch>
); </HashRouter>
</Provider>
var deepServerDashboardJsx = (callbackSpy) => (
<HashRouter>
<Switch>
<ServerDashboard
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":{}}]'
)}
updateUsers={callbackSpy}
shutdownHub={callbackSpy}
startServer={callbackSpy}
stopServer={callbackSpy}
startAll={callbackSpy}
stopAll={callbackSpy}
dispatch={callbackSpy}
/>
</Switch>
</HashRouter>
); );
var mockAsync = () => var mockAsync = () =>
@@ -48,14 +38,30 @@ describe("ServerDashboard Component: ", () => {
Promise.resolve({ json: () => Promise.resolve({ k: "v" }) }) Promise.resolve({ json: () => Promise.resolve({ k: "v" }) })
); );
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("Renders users from props.user_data into table", () => { it("Renders users from props.user_data into table", () => {
let component = shallow(serverDashboardJsx(jest.fn())), let component = mount(serverDashboardJsx(jest.fn())),
userRows = component.find(".user-row"); userRows = component.find(".user-row");
expect(userRows.length).toBe(2); expect(userRows.length).toBe(2);
}); });
it("Renders correctly the status of a single-user server", () => { it("Renders correctly the status of a single-user server", () => {
let component = shallow(serverDashboardJsx(jest.fn())), let component = mount(serverDashboardJsx(jest.fn())),
userRows = component.find(".user-row"); userRows = component.find(".user-row");
// Renders .stop-button when server is started // Renders .stop-button when server is started
// Should be 1 since user foo is started // Should be 1 since user foo is started
@@ -67,7 +73,7 @@ describe("ServerDashboard Component: ", () => {
it("Invokes the startServer event on button click", () => { it("Invokes the startServer event on button click", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(serverDashboardJsx(callbackSpy)), component = mount(serverDashboardJsx(callbackSpy)),
startBtn = component.find(".start-button"); startBtn = component.find(".start-button");
startBtn.simulate("click"); startBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); expect(callbackSpy).toHaveBeenCalled();
@@ -75,7 +81,7 @@ describe("ServerDashboard Component: ", () => {
it("Invokes the stopServer event on button click", () => { it("Invokes the stopServer event on button click", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(serverDashboardJsx(callbackSpy)), component = mount(serverDashboardJsx(callbackSpy)),
stopBtn = component.find(".stop-button"); stopBtn = component.find(".stop-button");
stopBtn.simulate("click"); stopBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); expect(callbackSpy).toHaveBeenCalled();
@@ -83,14 +89,14 @@ describe("ServerDashboard Component: ", () => {
it("Invokes the shutdownHub event on button click", () => { it("Invokes the shutdownHub event on button click", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = shallow(serverDashboardJsx(callbackSpy)), component = mount(serverDashboardJsx(callbackSpy)),
shutdownBtn = component.find(".shutdown-button"); shutdownBtn = component.find("#shutdown-button").first();
shutdownBtn.simulate("click"); shutdownBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled(); expect(callbackSpy).toHaveBeenCalled();
}); });
it("Sorts according to username", () => { it("Sorts according to username", () => {
let component = mount(deepServerDashboardJsx(jest.fn())).find( let component = mount(serverDashboardJsx(jest.fn())).find(
"ServerDashboard" "ServerDashboard"
), ),
handler = component.find("SortHandler").first(); handler = component.find("SortHandler").first();
@@ -103,7 +109,7 @@ describe("ServerDashboard Component: ", () => {
}); });
it("Sorts according to admin", () => { it("Sorts according to admin", () => {
let component = mount(deepServerDashboardJsx(jest.fn())).find( let component = mount(serverDashboardJsx(jest.fn())).find(
"ServerDashboard" "ServerDashboard"
), ),
handler = component.find("SortHandler").at(1); handler = component.find("SortHandler").at(1);
@@ -116,7 +122,7 @@ describe("ServerDashboard Component: ", () => {
}); });
it("Sorts according to last activity", () => { it("Sorts according to last activity", () => {
let component = mount(deepServerDashboardJsx(jest.fn())).find( let component = mount(serverDashboardJsx(jest.fn())).find(
"ServerDashboard" "ServerDashboard"
), ),
handler = component.find("SortHandler").at(2); handler = component.find("SortHandler").at(2);
@@ -131,7 +137,7 @@ describe("ServerDashboard Component: ", () => {
}); });
it("Sorts according to server status (running/not running)", () => { it("Sorts according to server status (running/not running)", () => {
let component = mount(deepServerDashboardJsx(jest.fn())).find( let component = mount(serverDashboardJsx(jest.fn())).find(
"ServerDashboard" "ServerDashboard"
), ),
handler = component.find("SortHandler").at(3); handler = component.find("SortHandler").at(3);
@@ -146,7 +152,10 @@ describe("ServerDashboard Component: ", () => {
}); });
it("Renders nothing if required data is not available", () => { it("Renders nothing if required data is not available", () => {
let component = shallow(<ServerDashboard />); useSelector.mockImplementation((callback) => {
return callback({});
});
let component = mount(serverDashboardJsx(jest.fn()));
expect(component.html()).toBe("<div></div>"); expect(component.html()).toBe("<div></div>");
}); });
}); });

40
jsx/src/util/withAPI.js Normal file
View File

@@ -0,0 +1,40 @@
import { withProps } from "recompose";
import { jhapiRequest } from "./jhapiUtil";
const withAPI = withProps((props) => ({
updateUsers: (cb) => jhapiRequest("/users", "GET"),
shutdownHub: () => jhapiRequest("/shutdown", "POST"),
startServer: (name) => jhapiRequest("/users/" + name + "/server", "POST"),
stopServer: (name) => jhapiRequest("/users/" + name + "/server", "DELETE"),
startAll: (names) =>
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
stopAll: (names) =>
names.map((e) => jhapiRequest("/users/" + e + "/server", "DELETE")),
addToGroup: (users, groupname) =>
jhapiRequest("/groups/" + groupname + "/users", "POST", { users }),
removeFromGroup: (users, groupname) =>
jhapiRequest("/groups/" + groupname + "/users", "DELETE", { users }),
createGroup: (groupName) => jhapiRequest("/groups/" + groupName, "POST"),
deleteGroup: (name) => jhapiRequest("/groups/" + name, "DELETE"),
addUsers: (usernames, admin) =>
jhapiRequest("/users", "POST", { usernames, admin }),
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;
},
refreshGroupsData: () =>
jhapiRequest("/groups", "GET").then((data) => data.json()),
refreshUserData: () =>
jhapiRequest("/users", "GET").then((data) => data.json()),
}));
export default withAPI;

View File

@@ -1 +0,0 @@
38441

File diff suppressed because one or more lines are too long