Updated ServerDashboard to testing-library, added tests

This commit is contained in:
Nathan Barber
2021-12-01 01:32:19 -05:00
parent dfee471e22
commit 293fe4e838
3 changed files with 470 additions and 184 deletions

View File

@@ -61,7 +61,7 @@ const ServerDashboard = (props) => {
}; };
if (!user_data) { if (!user_data) {
return <div></div>; return <div data-testid="no-show"></div>;
} }
if (page != user_page) { if (page != user_page) {
@@ -73,7 +73,7 @@ const ServerDashboard = (props) => {
} }
return ( return (
<div className="container"> <div className="container" data-testid="container">
{errorAlert != null ? ( {errorAlert != null ? (
<div className="row"> <div className="row">
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> <div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
@@ -104,6 +104,7 @@ const ServerDashboard = (props) => {
<SortHandler <SortHandler
sorts={{ asc: usernameAsc, desc: usernameDesc }} sorts={{ asc: usernameAsc, desc: usernameDesc }}
callback={(method) => setSortMethod(() => method)} callback={(method) => setSortMethod(() => method)}
testid="user-sort"
/> />
</th> </th>
<th id="admin-header"> <th id="admin-header">
@@ -111,6 +112,7 @@ const ServerDashboard = (props) => {
<SortHandler <SortHandler
sorts={{ asc: adminAsc, desc: adminDesc }} sorts={{ asc: adminAsc, desc: adminDesc }}
callback={(method) => setSortMethod(() => method)} callback={(method) => setSortMethod(() => method)}
testid="admin-sort"
/> />
</th> </th>
<th id="last-activity-header"> <th id="last-activity-header">
@@ -118,6 +120,7 @@ const ServerDashboard = (props) => {
<SortHandler <SortHandler
sorts={{ asc: dateAsc, desc: dateDesc }} sorts={{ asc: dateAsc, desc: dateDesc }}
callback={(method) => setSortMethod(() => method)} callback={(method) => setSortMethod(() => method)}
testid="last-activity-sort"
/> />
</th> </th>
<th id="running-status-header"> <th id="running-status-header">
@@ -125,6 +128,7 @@ const ServerDashboard = (props) => {
<SortHandler <SortHandler
sorts={{ asc: runningAsc, desc: runningDesc }} sorts={{ asc: runningAsc, desc: runningDesc }}
callback={(method) => setSortMethod(() => method)} callback={(method) => setSortMethod(() => method)}
testid="running-status-sort"
/> />
</th> </th>
<th id="actions-header">Actions</th> <th id="actions-header">Actions</th>
@@ -144,13 +148,14 @@ const ServerDashboard = (props) => {
<Button <Button
variant="primary" variant="primary"
className="start-all" className="start-all"
data-testid="start-all"
onClick={() => { onClick={() => {
Promise.all(startAll(user_data.map((e) => e.name))) Promise.all(startAll(user_data.map((e) => e.name)))
.then((res) => { .then((res) => {
let failedServers = res.filter((e) => !e.ok); let failedServers = res.filter((e) => !e.ok);
if (failedServers.length > 0) { if (failedServers.length > 0) {
setErrorAlert( setErrorAlert(
`Could not start ${failedServers.length} ${ `Failed to start ${failedServers.length} ${
failedServers.length > 1 ? "servers" : "server" failedServers.length > 1 ? "servers" : "server"
}. ${ }. ${
failedServers.length > 1 ? "Are they " : "Is it " failedServers.length > 1 ? "Are they " : "Is it "
@@ -165,12 +170,12 @@ const ServerDashboard = (props) => {
dispatchPageUpdate(data, page); dispatchPageUpdate(data, page);
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not update users list.`) setErrorAlert(`Failed to update users list.`)
); );
return res; return res;
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not start servers.`) setErrorAlert(`Failed to start servers.`)
); );
}} }}
> >
@@ -181,13 +186,14 @@ const ServerDashboard = (props) => {
<Button <Button
variant="danger" variant="danger"
className="stop-all" className="stop-all"
data-testid="stop-all"
onClick={() => { onClick={() => {
Promise.all(stopAll(user_data.map((e) => e.name))) Promise.all(stopAll(user_data.map((e) => e.name)))
.then((res) => { .then((res) => {
let failedServers = res.filter((e) => !e.ok); let failedServers = res.filter((e) => !e.ok);
if (failedServers.length > 0) { if (failedServers.length > 0) {
setErrorAlert( setErrorAlert(
`Could not stop ${failedServers.length} ${ `Failed to stop ${failedServers.length} ${
failedServers.length > 1 ? "servers" : "server" failedServers.length > 1 ? "servers" : "server"
}. ${ }. ${
failedServers.length > 1 ? "Are they " : "Is it " failedServers.length > 1 ? "Are they " : "Is it "
@@ -202,13 +208,11 @@ const ServerDashboard = (props) => {
dispatchPageUpdate(data, page); dispatchPageUpdate(data, page);
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not update users list.`) setErrorAlert(`Failed to update users list.`)
); );
return res; return res;
}) })
.catch((err) => .catch((err) => setErrorAlert(`Failed to stop servers.`));
setErrorAlert(`Could not stop all servers.`)
);
}} }}
> >
Stop All Stop All
@@ -227,12 +231,12 @@ const ServerDashboard = (props) => {
</tr> </tr>
{user_data.map((e, i) => ( {user_data.map((e, i) => (
<tr key={i + "row"} className="user-row"> <tr key={i + "row"} className="user-row">
<td>{e.name}</td> <td data-testid="user-row-name">{e.name}</td>
<td>{e.admin ? "admin" : ""}</td> <td data-testid="user-row-admin">{e.admin ? "admin" : ""}</td>
<td> <td data-testid="user-row-last-activity">
{e.last_activity ? timeSince(e.last_activity) : "Never"} {e.last_activity ? timeSince(e.last_activity) : "Never"}
</td> </td>
<td> <td data-testid="user-row-server-activity">
{e.server != null ? ( {e.server != null ? (
// Stop Single-user server // Stop Single-user server
<button <button
@@ -240,21 +244,21 @@ const ServerDashboard = (props) => {
onClick={() => onClick={() =>
stopServer(e.name) stopServer(e.name)
.then((res) => { .then((res) => {
if (res.ok) return res; data < 300
throw new Error(res.status); ? updateUsers(...slice)
}) .then((data) => {
.then((res) => { dispatchPageUpdate(data, page);
updateUsers(...slice) })
.then((data) => { .catch((err) =>
dispatchPageUpdate(data, page); setErrorAlert(
}) `Failed to update users list.`
.catch((err) => )
setErrorAlert(`Could not update users list.`) )
); : setErrorAlert(`Failed to stop server`);
return res; return res;
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not stop server.`) setErrorAlert(`Failed to stop server.`)
) )
} }
> >
@@ -267,21 +271,21 @@ const ServerDashboard = (props) => {
onClick={() => onClick={() =>
startServer(e.name) startServer(e.name)
.then((res) => { .then((res) => {
if (res.ok) return res; data < 300
throw new Error(res.status); ? updateUsers(...slice)
}) .then((data) => {
.then((res) => { dispatchPageUpdate(data, page);
updateUsers(...slice) })
.then((data) => { .catch((err) =>
dispatchPageUpdate(data, page); setErrorAlert(
}) `Failed to update users list.`
.catch((err) => )
setErrorAlert(`Could not update users list.`) )
); : setErrorAlert(`Failed to start server`);
return res; return res;
}) })
.catch((err) => { .catch((err) => {
setErrorAlert(`Could not start server.`); setErrorAlert(`Failed to start server.`);
}) })
} }
> >
@@ -342,13 +346,14 @@ ServerDashboard.propTypes = {
}; };
const SortHandler = (props) => { const SortHandler = (props) => {
var { sorts, callback } = props; var { sorts, callback, testid } = props;
var [direction, setDirection] = useState(undefined); var [direction, setDirection] = useState(undefined);
return ( return (
<div <div
className="sort-icon" className="sort-icon"
data-testid={testid}
onClick={() => { onClick={() => {
if (!direction) { if (!direction) {
callback(sorts.desc); callback(sorts.desc);
@@ -376,6 +381,7 @@ const SortHandler = (props) => {
SortHandler.propTypes = { SortHandler.propTypes = {
sorts: PropTypes.object, sorts: PropTypes.object,
callback: PropTypes.func, callback: PropTypes.func,
testid: PropTypes.string,
}; };
export default ServerDashboard; export default ServerDashboard;

View File

@@ -1,161 +1,441 @@
import React from "react"; import React from "react";
import Enzyme, { mount } from "enzyme"; import "@testing-library/jest-dom";
import ServerDashboard from "./ServerDashboard"; import { act } from "react-dom/test-utils";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { render, screen, fireEvent } from "@testing-library/react";
import { HashRouter, Switch } from "react-router-dom"; import { HashRouter, Switch } from "react-router-dom";
import { Provider, useSelector } from "react-redux"; import { Provider, useSelector } from "react-redux";
import { createStore } from "redux"; import { createStore } from "redux";
// eslint-disable-next-line
import regeneratorRuntime from 'regenerator-runtime'
Enzyme.configure({ adapter: new Adapter() }); import ServerDashboard from "./ServerDashboard";
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"), ...jest.requireActual("react-redux"),
useSelector: jest.fn(), useSelector: jest.fn(),
})); }));
describe("ServerDashboard Component: ", () => { var serverDashboardJsx = (spy) => (
var serverDashboardJsx = (callbackSpy) => ( <Provider store={createStore(() => {}, {})}>
<Provider store={createStore(() => {}, {})}> <HashRouter>
<HashRouter> <Switch>
<Switch> <ServerDashboard
<ServerDashboard updateUsers={spy}
updateUsers={callbackSpy} shutdownHub={spy}
shutdownHub={callbackSpy} startServer={spy}
startServer={callbackSpy} stopServer={spy}
stopServer={callbackSpy} startAll={spy}
startAll={callbackSpy} stopAll={spy}
stopAll={callbackSpy} />
/> </Switch>
</Switch> </HashRouter>
</HashRouter> </Provider>
</Provider> );
);
var mockAsync = () => var mockAsync = (data) =>
jest jest
.fn() .fn()
.mockImplementation(() => .mockImplementation(() =>
Promise.resolve({ json: () => Promise.resolve({ k: "v" }) }) Promise.resolve({ json: () => Promise.resolve(data ? data : { k: "v" }) })
); );
var mockAppState = () => ({ var mockAsyncRejection = () =>
user_data: JSON.parse( jest.fn().mockImplementation(() => Promise.reject());
'[{"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(() => { var mockAppState = () => ({
useSelector.mockImplementation((callback) => { user_data: JSON.parse(
return callback(mockAppState()); '[{"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":{}}]'
}); ),
}); });
afterEach(() => { beforeEach(() => {
useSelector.mockClear(); useSelector.mockImplementation((callback) => {
}); return callback(mockAppState());
it("Renders users from props.user_data into table", () => {
let component = mount(serverDashboardJsx(mockAsync())),
userRows = component.find(".user-row");
expect(userRows.length).toBe(2);
});
it("Renders correctly the status of a single-user server", () => {
let component = mount(serverDashboardJsx(mockAsync())),
userRows = component.find(".user-row");
// Renders .stop-button when server is started
// Should be 1 since user foo is started
expect(userRows.at(0).find(".stop-button").length).toBe(1);
// Renders .start-button when server is stopped
// Should be 1 since user bar is stopped
expect(userRows.at(1).find(".start-button").length).toBe(1);
});
it("Invokes the startServer event on button click", () => {
let callbackSpy = mockAsync(),
component = mount(serverDashboardJsx(callbackSpy)),
startBtn = component.find(".start-button");
startBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
});
it("Invokes the stopServer event on button click", () => {
let callbackSpy = mockAsync(),
component = mount(serverDashboardJsx(callbackSpy)),
stopBtn = component.find(".stop-button");
stopBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
});
it("Invokes the shutdownHub event on button click", () => {
let callbackSpy = mockAsync(),
component = mount(serverDashboardJsx(callbackSpy)),
shutdownBtn = component.find("#shutdown-button").first();
shutdownBtn.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
});
it("Sorts according to username", () => {
let component = mount(serverDashboardJsx(mockAsync())).find(
"ServerDashboard"
),
handler = component.find("SortHandler").first();
handler.simulate("click");
let first = component.find(".user-row").first();
expect(first.html().includes("bar")).toBe(true);
handler.simulate("click");
first = component.find(".user-row").first();
expect(first.html().includes("foo")).toBe(true);
});
it("Sorts according to admin", () => {
let component = mount(serverDashboardJsx(mockAsync())).find(
"ServerDashboard"
),
handler = component.find("SortHandler").at(1);
handler.simulate("click");
let first = component.find(".user-row").first();
expect(first.html().includes("admin")).toBe(true);
handler.simulate("click");
first = component.find(".user-row").first();
expect(first.html().includes("admin")).toBe(false);
});
it("Sorts according to last activity", () => {
let component = mount(serverDashboardJsx(mockAsync())).find(
"ServerDashboard"
),
handler = component.find("SortHandler").at(2);
handler.simulate("click");
let first = component.find(".user-row").first();
// foo used most recently
expect(first.html().includes("foo")).toBe(true);
handler.simulate("click");
first = component.find(".user-row").first();
// invert sort - bar used least recently
expect(first.html().includes("bar")).toBe(true);
});
it("Sorts according to server status (running/not running)", () => {
let component = mount(serverDashboardJsx(mockAsync())).find(
"ServerDashboard"
),
handler = component.find("SortHandler").at(3);
handler.simulate("click");
let first = component.find(".user-row").first();
// foo running
expect(first.html().includes("foo")).toBe(true);
handler.simulate("click");
first = component.find(".user-row").first();
// invert sort - bar not running
expect(first.html().includes("bar")).toBe(true);
});
it("Renders nothing if required data is not available", () => {
useSelector.mockImplementation((callback) => {
return callback({});
});
let component = mount(serverDashboardJsx(jest.fn()));
expect(component.html()).toBe("<div></div>");
}); });
}); });
afterEach(() => {
useSelector.mockClear();
});
test("Renders", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
expect(screen.getByTestId("container")).toBeVisible();
});
test("Renders users from props.user_data into table", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let foo = screen.getByText("foo");
let bar = screen.getByText("bar");
expect(foo).toBeVisible();
expect(bar).toBeVisible();
});
test("Renders correctly the status of a single-user server", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let start = screen.getByText("Start Server");
let stop = screen.getByText("Stop Server");
expect(start).toBeVisible();
expect(stop).toBeVisible();
});
test("Invokes the startServer event on button click", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let start = screen.getByText("Start Server");
await act(async () => {
fireEvent.click(start);
});
expect(callbackSpy).toHaveBeenCalled();
});
test("Invokes the stopServer event on button click", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
});
expect(callbackSpy).toHaveBeenCalled();
});
test("Invokes the shutdownHub event on button click", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let shutdown = screen.getByText("Shutdown Hub");
await act(async () => {
fireEvent.click(shutdown);
});
expect(callbackSpy).toHaveBeenCalled();
});
test("Sorts according to username", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let handler = screen.getByTestId("user-sort");
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
});
test("Sorts according to admin", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let handler = screen.getByTestId("admin-sort");
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-admin")[0];
expect(first.textContent).toBe("admin");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-admin")[0];
expect(first.textContent).toBe("");
});
test("Sorts according to last activity", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let handler = screen.getByTestId("last-activity-sort");
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
});
test("Sorts according to server status (running/not running)", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let handler = screen.getByTestId("running-status-sort");
fireEvent.click(handler);
let first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("foo");
fireEvent.click(handler);
first = screen.getAllByTestId("user-row-name")[0];
expect(first.textContent).toBe("bar");
});
test("Renders nothing if required data is not available", async () => {
useSelector.mockImplementation((callback) => {
return callback({});
});
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
let noShow = screen.getByTestId("no-show");
expect(noShow).toBeVisible();
});
test("Shows a UI error dialogue when start all servers fails", async () => {
let spy = mockAsync();
let rejectSpy = mockAsyncRejection;
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={spy}
stopServer={spy}
startAll={rejectSpy}
stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let startAll = screen.getByTestId("start-all");
await act(async () => {
fireEvent.click(startAll);
});
let errorDialog = screen.getByText("Failed to start servers.");
expect(errorDialog).toBeVisible();
});
test("Shows a UI error dialogue when stop all servers fails", async () => {
let spy = mockAsync();
let rejectSpy = mockAsyncRejection;
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={spy}
stopServer={spy}
startAll={spy}
stopAll={rejectSpy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let stopAll = screen.getByTestId("stop-all");
await act(async () => {
fireEvent.click(stopAll);
});
let errorDialog = screen.getByText("Failed to stop servers.");
expect(errorDialog).toBeVisible();
});
test("Shows a UI error dialogue when start user server fails", async () => {
let spy = mockAsync();
let rejectSpy = mockAsyncRejection();
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={rejectSpy}
stopServer={spy}
startAll={spy}
stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let start = screen.getByText("Start Server");
await act(async () => {
fireEvent.click(start);
});
let errorDialog = screen.getByText("Failed to start server.");
expect(errorDialog).toBeVisible();
});
test("Shows a UI error dialogue when start user server returns an improper status code", async () => {
let spy = mockAsync();
let rejectSpy = mockAsync({ status: 403 });
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={rejectSpy}
stopServer={spy}
startAll={spy}
stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let start = screen.getByText("Start Server");
await act(async () => {
fireEvent.click(start);
});
let errorDialog = screen.getByText("Failed to start server.");
expect(errorDialog).toBeVisible();
});
test("Shows a UI error dialogue when stop user servers fails", async () => {
let spy = mockAsync();
let rejectSpy = mockAsyncRejection();
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={spy}
stopServer={rejectSpy}
startAll={spy}
stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
});
let errorDialog = screen.getByText("Failed to stop server.");
expect(errorDialog).toBeVisible();
});
test("Shows a UI error dialogue when stop user server returns an improper status code", async () => {
let spy = mockAsync();
let rejectSpy = mockAsync({ status: 403 });
await act(async () => {
render(
<Provider store={createStore(() => {}, {})}>
<HashRouter>
<Switch>
<ServerDashboard
updateUsers={spy}
shutdownHub={spy}
startServer={spy}
stopServer={rejectSpy}
startAll={spy}
stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
});
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
});
let errorDialog = screen.getByText("Failed to stop server.");
expect(errorDialog).toBeVisible();
});

File diff suppressed because one or more lines are too long