Update unit tests for pagination

This commit is contained in:
Nathan Barber
2021-05-10 17:48:46 -04:00
parent 16c37cd5fe
commit 8d4c276652
13 changed files with 87 additions and 189 deletions

View File

@@ -9,7 +9,7 @@
"build": "yarn && webpack", "build": "yarn && webpack",
"hot": "webpack && webpack-dev-server", "hot": "webpack && webpack-dev-server",
"place": "cp -r build/admin-react.js ../share/jupyterhub/static/js/admin-react.js", "place": "cp -r build/admin-react.js ../share/jupyterhub/static/js/admin-react.js",
"test": "jest", "test": "jest --verbose",
"snap": "jest --updateSnapshot", "snap": "jest --updateSnapshot",
"lint": "eslint --ext .jsx --ext .js src/", "lint": "eslint --ext .jsx --ext .js src/",
"lint:fix": "eslint --ext .jsx --ext .js src/ --fix" "lint:fix": "eslint --ext .jsx --ext .js src/ --fix"

View File

@@ -2,7 +2,7 @@ import React from "react";
import Enzyme, { mount } from "enzyme"; import Enzyme, { mount } from "enzyme";
import AddUser from "./AddUser"; 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 { Provider, useDispatch, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
@@ -11,6 +11,7 @@ Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"), ...jest.requireActual("react-redux"),
useDispatch: jest.fn(), useDispatch: jest.fn(),
useSelector: jest.fn(),
})); }));
describe("AddUser Component: ", () => { describe("AddUser Component: ", () => {
@@ -23,17 +24,24 @@ describe("AddUser Component: ", () => {
<AddUser <AddUser
addUsers={callbackSpy} addUsers={callbackSpy}
failRegexEvent={callbackSpy} failRegexEvent={callbackSpy}
refreshUserData={callbackSpy} updateUsers={callbackSpy}
history={{ push: (a) => {} }} history={{ push: (a) => {} }}
/> />
</HashRouter> </HashRouter>
</Provider> </Provider>
); );
var mockAppState = () => ({
limit: 3,
});
beforeEach(() => { beforeEach(() => {
useDispatch.mockImplementation((callback) => { useDispatch.mockImplementation((callback) => {
return () => {}; return () => {};
}); });
useSelector.mockImplementation((callback) => {
return callback(mockAppState());
});
}); });
afterEach(() => { afterEach(() => {

View File

@@ -2,7 +2,7 @@ import React from "react";
import Enzyme, { mount } from "enzyme"; import Enzyme, { mount } from "enzyme";
import CreateGroup from "./CreateGroup"; 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 { Provider, useDispatch, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
@@ -11,6 +11,7 @@ Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"), ...jest.requireActual("react-redux"),
useDispatch: jest.fn(), useDispatch: jest.fn(),
useSelector: jest.fn(),
})); }));
describe("CreateGroup Component: ", () => { describe("CreateGroup Component: ", () => {
@@ -22,16 +23,23 @@ describe("CreateGroup Component: ", () => {
<HashRouter> <HashRouter>
<CreateGroup <CreateGroup
createGroup={callbackSpy} createGroup={callbackSpy}
refreshGroupsData={callbackSpy} updateGroups={callbackSpy}
history={{ push: () => {} }} history={{ push: () => {} }}
/> />
</HashRouter> </HashRouter>
</Provider> </Provider>
); );
var mockAppState = () => ({
limit: 3,
});
beforeEach(() => { beforeEach(() => {
useDispatch.mockImplementation((callback) => { useDispatch.mockImplementation((callback) => {
return () => {}; return () => () => {};
});
useSelector.mockImplementation((callback) => {
return callback(mockAppState());
}); });
}); });
@@ -52,6 +60,6 @@ describe("CreateGroup Component: ", () => {
input.simulate("change", { target: { value: "" } }); input.simulate("change", { target: { value: "" } });
submit.simulate("click"); submit.simulate("click");
expect(callbackSpy).toHaveBeenNthCalledWith(1, ""); expect(callbackSpy).toHaveBeenNthCalledWith(1, "");
expect(callbackSpy).toHaveBeenNthCalledWith(2); expect(callbackSpy).toHaveBeenNthCalledWith(2, 0, 3);
}); });
}); });

View File

@@ -2,7 +2,7 @@ import React from "react";
import Enzyme, { mount } from "enzyme"; import Enzyme, { mount } from "enzyme";
import EditUser from "./EditUser"; 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 { Provider, useDispatch, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
@@ -11,6 +11,7 @@ Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"), ...jest.requireActual("react-redux"),
useDispatch: jest.fn(), useDispatch: jest.fn(),
useSelector: jest.fn(),
})); }));
describe("EditUser Component: ", () => { describe("EditUser Component: ", () => {
@@ -27,7 +28,7 @@ describe("EditUser Component: ", () => {
} }
deleteUser={callbackSpy} deleteUser={callbackSpy}
editUser={callbackSpy} editUser={callbackSpy}
refreshUserData={callbackSpy} updateUsers={callbackSpy}
history={{ push: (a) => {} }} history={{ push: (a) => {} }}
failRegexEvent={callbackSpy} failRegexEvent={callbackSpy}
noChangeEvent={callbackSpy} noChangeEvent={callbackSpy}
@@ -36,10 +37,17 @@ describe("EditUser Component: ", () => {
</Provider> </Provider>
); );
var mockAppState = () => ({
limit: 3,
});
beforeEach(() => { beforeEach(() => {
useDispatch.mockImplementation((callback) => { useDispatch.mockImplementation((callback) => {
return () => {}; return () => {};
}); });
useSelector.mockImplementation((callback) => {
return callback(mockAppState());
});
}); });
afterEach(() => { afterEach(() => {

View File

@@ -31,7 +31,7 @@ const GroupEdit = (props) => {
removeFromGroup, removeFromGroup,
deleteGroup, deleteGroup,
updateGroups, updateGroups,
findUser, validateUser,
history, history,
location, location,
} = props; } = props;
@@ -41,9 +41,9 @@ const GroupEdit = (props) => {
return <></>; return <></>;
} }
var { group_data, user_data, callback } = location.state; var { group_data, callback } = location.state;
if (!(group_data && user_data)) return <div></div>; if (!group_data) return <div></div>;
return ( return (
<div className="container"> <div className="container">
@@ -56,10 +56,7 @@ const GroupEdit = (props) => {
</div> </div>
<GroupSelect <GroupSelect
users={group_data.users} users={group_data.users}
validateUser={async (username) => { validateUser={validateUser}
let user = await findUser(username);
return user.status > 200 ? false : true;
}}
onChange={(selection) => { onChange={(selection) => {
setSelected(selection); setSelected(selection);
setChanged(true); setChanged(true);
@@ -139,7 +136,6 @@ GroupEdit.propTypes = {
location: PropTypes.shape({ location: PropTypes.shape({
state: PropTypes.shape({ state: PropTypes.shape({
group_data: PropTypes.object, group_data: PropTypes.object,
user_data: PropTypes.array,
callback: PropTypes.func, callback: PropTypes.func,
}), }),
}), }),

View File

@@ -5,6 +5,8 @@ import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { Provider, useSelector } from "react-redux"; import { Provider, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
import ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
@@ -14,8 +16,9 @@ jest.mock("react-redux", () => ({
})); }));
describe("GroupEdit Component: ", () => { describe("GroupEdit Component: ", () => {
var mockAsync = () => var mockAsync = () => jest.fn().mockImplementation(() => Promise.resolve());
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var okPacket = new Promise((resolve) => resolve(true));
var groupEditJsx = (callbackSpy) => ( var groupEditJsx = (callbackSpy) => (
<Provider store={createStore(() => {}, {})}> <Provider store={createStore(() => {}, {})}>
@@ -23,7 +26,6 @@ describe("GroupEdit Component: ", () => {
<GroupEdit <GroupEdit
location={{ location={{
state: { state: {
user_data: [{ name: "foo" }, { name: "bar" }],
group_data: { users: ["foo"], name: "group" }, group_data: { users: ["foo"], name: "group" },
callback: () => {}, callback: () => {},
}, },
@@ -32,16 +34,15 @@ describe("GroupEdit Component: ", () => {
removeFromGroup={callbackSpy} removeFromGroup={callbackSpy}
deleteGroup={callbackSpy} deleteGroup={callbackSpy}
history={{ push: (a) => callbackSpy }} history={{ push: (a) => callbackSpy }}
refreshGroupsData={callbackSpy} updateGroups={callbackSpy}
validateUser={jest.fn().mockImplementation(() => okPacket)}
/> />
</HashRouter> </HashRouter>
</Provider> </Provider>
); );
var mockAppState = () => ({ var mockAppState = () => ({
user_data: JSON.parse( limit: 3,
'[{"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(() => { beforeEach(() => {
@@ -54,24 +55,39 @@ describe("GroupEdit Component: ", () => {
useSelector.mockClear(); useSelector.mockClear();
}); });
it("Adds a newly selected user to group on submit", () => { it("Adds user from input to user selectables on button click", async () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = mount(groupEditJsx(callbackSpy)), component = mount(groupEditJsx(callbackSpy)),
unselected = component.find(".unselected"), input = component.find("#username-input"),
validateUser = component.find("#validate-user"),
submit = component.find("#submit"); submit = component.find("#submit");
unselected.simulate("click");
input.simulate("change", { target: { value: "bar" } });
validateUser.simulate("click");
await act(() => okPacket);
submit.simulate("click"); submit.simulate("click");
expect(callbackSpy).toHaveBeenCalledWith(["bar"], "group"); expect(callbackSpy).toHaveBeenNthCalledWith(1, ["bar"], "group");
}); });
it("Removes a user from group on submit", () => { it("Removes a user recently added from input from the selectables list", () => {
let callbackSpy = mockAsync(), let callbackSpy = mockAsync(),
component = mount(groupEditJsx(callbackSpy)), component = mount(groupEditJsx(callbackSpy)),
selected = component.find(".selected"), unsubmittedUser = component.find(".item.selected").last();
submit = component.find("#submit"); unsubmittedUser.simulate("click");
selected.simulate("click"); expect(component.find(".item").length).toBe(1);
});
it("Grays out a user, already in the group, when unselected and calls deleteUser on submit", () => {
let callbackSpy = mockAsync(),
component = mount(groupEditJsx(callbackSpy)),
groupUser = component.find(".item.selected").first();
groupUser.simulate("click");
expect(component.find(".item.unselected").length).toBe(1);
expect(component.find(".item").length).toBe(1);
// test deleteUser call
let submit = component.find("#submit");
submit.simulate("click"); submit.simulate("click");
expect(callbackSpy).toHaveBeenCalledWith(["foo"], "group"); expect(callbackSpy).toHaveBeenNthCalledWith(1, ["foo"], "group");
}); });
it("Calls deleteGroup on button click", () => { it("Calls deleteGroup on button click", () => {
@@ -80,6 +96,5 @@ describe("GroupEdit Component: ", () => {
deleteGroup = component.find("#delete-group").first(); deleteGroup = component.find("#delete-group").first();
deleteGroup.simulate("click"); deleteGroup.simulate("click");
expect(callbackSpy).toHaveBeenNthCalledWith(1, "group"); expect(callbackSpy).toHaveBeenNthCalledWith(1, "group");
expect(callbackSpy).toHaveBeenNthCalledWith(2);
}); });
}); });

View File

@@ -23,6 +23,7 @@ const GroupSelect = (props) => {
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 text-left"> <div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 text-left">
<div className="input-group"> <div className="input-group">
<input <input
id="username-input"
type="text" type="text"
className="form-control" className="form-control"
placeholder="Add by username" placeholder="Add by username"
@@ -33,10 +34,10 @@ const GroupSelect = (props) => {
/> />
<span className="input-group-btn"> <span className="input-group-btn">
<button <button
id="validate-user"
className="btn btn-default" className="btn btn-default"
type="button" type="button"
onClick={() => { onClick={() => {
// check user exists then
validateUser(username).then((exists) => { validateUser(username).then((exists) => {
if (exists && !selected.includes(username)) { if (exists && !selected.includes(username)) {
let updated_selection = selected.concat([username]); let updated_selection = selected.concat([username]);

View File

@@ -2,7 +2,7 @@ import React from "react";
import Enzyme, { mount } from "enzyme"; import Enzyme, { mount } from "enzyme";
import Groups from "./Groups"; 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 { Provider, useDispatch, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
@@ -15,10 +15,13 @@ jest.mock("react-redux", () => ({
})); }));
describe("Groups Component: ", () => { describe("Groups Component: ", () => {
var groupsJsx = () => ( var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var groupsJsx = (callbackSpy) => (
<Provider store={createStore(() => {}, {})}> <Provider store={createStore(() => {}, {})}>
<HashRouter> <HashRouter>
<Groups /> <Groups location={{ search: "0" }} updateGroups={callbackSpy} />
</HashRouter> </HashRouter>
</Provider> </Provider>
); );
@@ -36,6 +39,9 @@ describe("Groups Component: ", () => {
useSelector.mockImplementation((callback) => { useSelector.mockImplementation((callback) => {
return callback(mockAppState()); return callback(mockAppState());
}); });
useDispatch.mockImplementation((callback) => {
return () => {};
});
}); });
afterEach(() => { afterEach(() => {
@@ -43,8 +49,9 @@ describe("Groups Component: ", () => {
}); });
it("Renders groups_data prop into links", () => { it("Renders groups_data prop into links", () => {
let component = mount(groupsJsx()), let callbackSpy = mockAsync(),
links = component.find(".group-edit-link"); component = mount(groupsJsx(callbackSpy)),
links = component.find("li");
expect(links.length).toBe(2); expect(links.length).toBe(2);
}); });

View File

@@ -1,56 +0,0 @@
import React, { useState } from "react";
import "./multi-select.css";
import PropTypes from "prop-types";
const Multiselect = (props) => {
var { onChange, options, value } = props;
var [selected, setSelected] = useState(value);
if (!options) return null;
return (
<div className="multi-container">
<div>
{selected.map((e, i) => (
<div
key={"selected" + i}
className="item selected"
onClick={() => {
let updated_selection = selected
.slice(0, i)
.concat(selected.slice(i + 1));
onChange(updated_selection, options);
setSelected(updated_selection);
}}
>
{e}
</div>
))}
{options.map((e, i) =>
selected.includes(e) ? undefined : (
<div
key={"unselected" + i}
className="item unselected"
onClick={() => {
let updated_selection = selected.concat([e]);
onChange(updated_selection, options);
setSelected(updated_selection);
}}
>
{e}
</div>
)
)}
</div>
</div>
);
};
Multiselect.propTypes = {
value: PropTypes.array,
onChange: PropTypes.func,
options: PropTypes.array,
};
export default Multiselect;

View File

@@ -1,51 +0,0 @@
import React from "react";
import Enzyme, { shallow } from "enzyme";
import Multiselect from "./Multiselect";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
Enzyme.configure({ adapter: new Adapter() });
describe("Multiselect Component: ", () => {
var multiselectJsx = () => (
<Multiselect
options={["foo", "bar", "wombat"]}
s
value={["wombat"]}
onChange={() => {}}
/>
);
it("Renders with initial value selected", () => {
let component = shallow(multiselectJsx()),
selected = component.find(".item.selected").first();
expect(selected.text()).toBe("wombat");
});
it("Deselects a value when div.item.selected is clicked", () => {
let component = shallow(multiselectJsx()),
selected = component.find(".item.selected").first();
selected.simulate("click");
expect(component.find(".item.selected").length).toBe(0);
});
it("Selects a an option when div.item.unselected is clicked", () => {
let component = shallow(multiselectJsx()),
unselected = component.find(".item.unselected").first();
unselected.simulate("click");
expect(component.find(".item.selected").length).toBe(2);
});
it("Triggers callback on any sort of change", () => {
let callbackSpy = jest.fn(),
component = shallow(
<Multiselect
options={["foo", "bar", "wombat"]}
value={["wombat"]}
onChange={callbackSpy}
/>
),
selected = component.find(".item.selected").first();
selected.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
});
});

View File

@@ -1,40 +0,0 @@
@import url(../../style/root.css);
.multi-container {
width: 100%;
position: relative;
padding: 5px;
overflow-x: scroll;
}
.multi-container div {
display: inline-block;
}
.multi-container .item {
padding: 3px;
padding-left: 6px;
padding-right: 6px;
border-radius: 3px;
font-size: 14px;
margin-left: 4px;
margin-right: 4px;
transition: 30ms ease-in all;
cursor: pointer;
user-select: none;
border: solid 1px #dfdfdf;
}
.multi-container .item.unselected {
background-color: #f7f7f7;
color: #777;
}
.multi-container .item.selected {
background-color: orange;
color: white;
}
.multi-container .item:hover {
opacity: 0.7;
}

View File

@@ -36,8 +36,6 @@ const ServerDashboard = (props) => {
limit = useSelector((state) => state.limit), limit = useSelector((state) => state.limit),
page = parseInt(new URLSearchParams(props.location.search).get("page")); page = parseInt(new URLSearchParams(props.location.search).get("page"));
console.log(user_page);
page = isNaN(page) ? 0 : page; page = isNaN(page) ? 0 : page;
var slice = [page * limit, limit]; var slice = [page * limit, limit];

View File

@@ -33,6 +33,10 @@ const withAPI = withProps((props) => ({
}), }),
deleteUser: (username) => jhapiRequest("/users/" + username, "DELETE"), deleteUser: (username) => jhapiRequest("/users/" + username, "DELETE"),
findUser: (username) => jhapiRequest("/users/" + username, "GET"), findUser: (username) => jhapiRequest("/users/" + username, "GET"),
validateUser: (username) =>
findUser(username)
.then((data) => data.status)
.then((data) => (data > 200 ? false : true)),
failRegexEvent: () => failRegexEvent: () =>
alert( alert(
"Cannot change username - either contains special characters or is too short." "Cannot change username - either contains special characters or is too short."