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((res) => {
updateUsers(...slice)
.then((data) => { .then((data) => {
dispatchPageUpdate(data, page); dispatchPageUpdate(data, page);
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not update users list.`) setErrorAlert(
); `Failed to 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((res) => {
updateUsers(...slice)
.then((data) => { .then((data) => {
dispatchPageUpdate(data, page); dispatchPageUpdate(data, page);
}) })
.catch((err) => .catch((err) =>
setErrorAlert(`Could not update users list.`) setErrorAlert(
); `Failed to 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={callbackSpy} updateUsers={spy}
shutdownHub={callbackSpy} shutdownHub={spy}
startServer={callbackSpy} startServer={spy}
stopServer={callbackSpy} stopServer={spy}
startAll={callbackSpy} startAll={spy}
stopAll={callbackSpy} stopAll={spy}
/>
</Switch>
</HashRouter>
</Provider>
);
var mockAsync = (data) =>
jest
.fn()
.mockImplementation(() =>
Promise.resolve({ json: () => Promise.resolve(data ? data : { k: "v" }) })
);
var mockAsyncRejection = () =>
jest.fn().mockImplementation(() => Promise.reject());
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();
});
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> </Switch>
</HashRouter> </HashRouter>
</Provider> </Provider>
); );
var mockAsync = () =>
jest
.fn()
.mockImplementation(() =>
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(() => { let startAll = screen.getByTestId("start-all");
useSelector.mockImplementation((callback) => {
return callback(mockAppState()); await act(async () => {
}); fireEvent.click(startAll);
}); });
afterEach(() => { let errorDialog = screen.getByText("Failed to start servers.");
useSelector.mockClear();
});
it("Renders users from props.user_data into table", () => { expect(errorDialog).toBeVisible();
let component = mount(serverDashboardJsx(mockAsync())), });
userRows = component.find(".user-row");
expect(userRows.length).toBe(2); test("Shows a UI error dialogue when stop all servers fails", async () => {
}); let spy = mockAsync();
let rejectSpy = mockAsyncRejection;
it("Renders correctly the status of a single-user server", () => {
let component = mount(serverDashboardJsx(mockAsync())), await act(async () => {
userRows = component.find(".user-row"); render(
// Renders .stop-button when server is started <Provider store={createStore(() => {}, {})}>
// Should be 1 since user foo is started <HashRouter>
expect(userRows.at(0).find(".stop-button").length).toBe(1); <Switch>
// Renders .start-button when server is stopped <ServerDashboard
// Should be 1 since user bar is stopped updateUsers={spy}
expect(userRows.at(1).find(".start-button").length).toBe(1); shutdownHub={spy}
}); startServer={spy}
stopServer={spy}
it("Invokes the startServer event on button click", () => { startAll={spy}
let callbackSpy = mockAsync(), stopAll={rejectSpy}
component = mount(serverDashboardJsx(callbackSpy)), />
startBtn = component.find(".start-button"); </Switch>
startBtn.simulate("click"); </HashRouter>
expect(callbackSpy).toHaveBeenCalled(); </Provider>
}); );
});
it("Invokes the stopServer event on button click", () => {
let callbackSpy = mockAsync(), let stopAll = screen.getByTestId("stop-all");
component = mount(serverDashboardJsx(callbackSpy)),
stopBtn = component.find(".stop-button"); await act(async () => {
stopBtn.simulate("click"); fireEvent.click(stopAll);
expect(callbackSpy).toHaveBeenCalled(); });
});
let errorDialog = screen.getByText("Failed to stop servers.");
it("Invokes the shutdownHub event on button click", () => {
let callbackSpy = mockAsync(), expect(errorDialog).toBeVisible();
component = mount(serverDashboardJsx(callbackSpy)), });
shutdownBtn = component.find("#shutdown-button").first();
shutdownBtn.simulate("click"); test("Shows a UI error dialogue when start user server fails", async () => {
expect(callbackSpy).toHaveBeenCalled(); let spy = mockAsync();
}); let rejectSpy = mockAsyncRejection();
it("Sorts according to username", () => { await act(async () => {
let component = mount(serverDashboardJsx(mockAsync())).find( render(
"ServerDashboard" <Provider store={createStore(() => {}, {})}>
), <HashRouter>
handler = component.find("SortHandler").first(); <Switch>
handler.simulate("click"); <ServerDashboard
let first = component.find(".user-row").first(); updateUsers={spy}
expect(first.html().includes("bar")).toBe(true); shutdownHub={spy}
handler.simulate("click"); startServer={rejectSpy}
first = component.find(".user-row").first(); stopServer={spy}
expect(first.html().includes("foo")).toBe(true); startAll={spy}
}); stopAll={spy}
/>
it("Sorts according to admin", () => { </Switch>
let component = mount(serverDashboardJsx(mockAsync())).find( </HashRouter>
"ServerDashboard" </Provider>
), );
handler = component.find("SortHandler").at(1); });
handler.simulate("click");
let first = component.find(".user-row").first(); let start = screen.getByText("Start Server");
expect(first.html().includes("admin")).toBe(true);
handler.simulate("click"); await act(async () => {
first = component.find(".user-row").first(); fireEvent.click(start);
expect(first.html().includes("admin")).toBe(false); });
});
let errorDialog = screen.getByText("Failed to start server.");
it("Sorts according to last activity", () => {
let component = mount(serverDashboardJsx(mockAsync())).find( expect(errorDialog).toBeVisible();
"ServerDashboard" });
),
handler = component.find("SortHandler").at(2); test("Shows a UI error dialogue when start user server returns an improper status code", async () => {
handler.simulate("click"); let spy = mockAsync();
let first = component.find(".user-row").first(); let rejectSpy = mockAsync({ status: 403 });
// foo used most recently
expect(first.html().includes("foo")).toBe(true); await act(async () => {
handler.simulate("click"); render(
first = component.find(".user-row").first(); <Provider store={createStore(() => {}, {})}>
// invert sort - bar used least recently <HashRouter>
expect(first.html().includes("bar")).toBe(true); <Switch>
}); <ServerDashboard
updateUsers={spy}
it("Sorts according to server status (running/not running)", () => { shutdownHub={spy}
let component = mount(serverDashboardJsx(mockAsync())).find( startServer={rejectSpy}
"ServerDashboard" stopServer={spy}
), startAll={spy}
handler = component.find("SortHandler").at(3); stopAll={spy}
handler.simulate("click"); />
let first = component.find(".user-row").first(); </Switch>
// foo running </HashRouter>
expect(first.html().includes("foo")).toBe(true); </Provider>
handler.simulate("click"); );
first = component.find(".user-row").first(); });
// invert sort - bar not running
expect(first.html().includes("bar")).toBe(true); let start = screen.getByText("Start Server");
});
await act(async () => {
it("Renders nothing if required data is not available", () => { fireEvent.click(start);
useSelector.mockImplementation((callback) => { });
return callback({});
}); let errorDialog = screen.getByText("Failed to start server.");
let component = mount(serverDashboardJsx(jest.fn()));
expect(component.html()).toBe("<div></div>"); 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