import * as React from "react";
import { act, renderHook } from "@testing-library/react-hooks";
import { within } from "@testing-library/react";
import {
componentWithProviders,
renderWithProviders,
} from "../testUtils/withProviders";
import { QueryClient } from "@tanstack/react-query";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import {
ACK_RESPONSE_BUTTON_CONTENT,
ACK_RESPONSE_HEADING,
CANCEL_BUTTON_CONTENT,
CONFIRM_BUTTON_CONTENT,
CONFIRMATION_MODAL_HEADING,
useGenerateReport,
useReportInfo,
} from "../../../src/components/InventoryReportRequestModal";
import * as AdminAPI from "../../../src/api/admin";
import InventoryReportRequestModal, {
NO_COLLECTIONS_MESSAGE,
SOME_COLLECTIONS_MESSAGE,
UNKNOWN_COLLECTIONS_MESSAGE,
} from "../../../src/components/InventoryReportRequestModal";
import userEvent from "@testing-library/user-event";
import { InventoryReportInfo } from "../../../src/api/admin";
const MODAL_HEADING_LEVEL = 4;
const COLLECTION_NAME = "Collection A";
const INFO_SUCCESS_SOME_COLLECTIONS: InventoryReportInfo = Object.freeze({
collections: [{ id: 0, name: COLLECTION_NAME }],
});
const INFO_SUCCESS_NO_COLLECTIONS: InventoryReportInfo = Object.freeze({
collections: [],
});
const GENERATE_REPORT_SUCCESS = {
message: "We triggered report generation.",
};
const MOCK_SERVER_API_ENDPOINT_PATH =
"/admin/reports/inventory_report/:library_short_name";
const API_ENDPOINT_PARAMS: AdminAPI.InventoryReportRequestParams = {
library: "library-short-name",
};
const setupMockServer = () => {
return setupServer(
http.get(MOCK_SERVER_API_ENDPOINT_PATH, () =>
HttpResponse.json(INFO_SUCCESS_SOME_COLLECTIONS, { status: 200 })
),
http.post(MOCK_SERVER_API_ENDPOINT_PATH, () =>
HttpResponse.json(GENERATE_REPORT_SUCCESS, { status: 202 })
)
);
};
describe("InventoryReportRequestModal", () => {
/* eslint-disable @typescript-eslint/no-empty-function */
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
log: console.log,
warn: console.warn,
error: process.env.NODE_ENV === "test" ? () => {} : console.error,
},
});
/* eslint-enable @typescript-eslint/no-empty-function */
describe("query hooks call correct api methods", () => {
const wrapper = componentWithProviders({ queryClient });
describe("get report information", () => {
const mock_info_api = jest.spyOn(AdminAPI, "getInventoryReportInfo");
afterEach(() => {
jest.clearAllMocks();
queryClient.clear();
});
afterAll(() => mock_info_api.mockRestore());
it("report info query hook performs fetch, when enabled", async () => {
const show = true;
mock_info_api.mockResolvedValue(INFO_SUCCESS_SOME_COLLECTIONS);
const { result, waitFor } = renderHook(
() => useReportInfo(show, API_ENDPOINT_PARAMS),
{ wrapper }
);
await waitFor(() => result.current.fetchStatus == "idle");
const { isSuccess, isError, error, collections } = result.current;
expect(mock_info_api).toHaveBeenCalledWith(API_ENDPOINT_PARAMS);
expect(isSuccess).toBe(true);
expect(isError).toBe(false);
expect(error).toBeNull();
expect(collections).toEqual(INFO_SUCCESS_SOME_COLLECTIONS.collections);
});
it("report info query hook doesn't fetch, when disabled", async () => {
const show = false;
mock_info_api.mockResolvedValue(INFO_SUCCESS_SOME_COLLECTIONS);
const { result, waitFor } = renderHook(
() => useReportInfo(show, API_ENDPOINT_PARAMS),
{ wrapper }
);
await waitFor(() => result.current.fetchStatus == "idle");
const { isSuccess, isError, error, collections } = result.current;
expect(mock_info_api).not.toHaveBeenCalled();
expect(isSuccess).toBe(false);
expect(isError).toBe(false);
expect(error).toBeNull();
expect(collections).not.toBeDefined();
});
it("report info query hook returns no collections, when api throws error", async () => {
const show = true;
mock_info_api.mockImplementation(() => {
throw new Error("an error occurred");
});
const { result, waitFor } = renderHook(
() => useReportInfo(show, API_ENDPOINT_PARAMS),
{ wrapper }
);
await waitFor(() => result.current.fetchStatus == "idle");
const { isSuccess, isError, error, collections } = result.current;
expect(mock_info_api).toHaveBeenCalledWith(API_ENDPOINT_PARAMS);
expect(isSuccess).toBe(false);
expect(isError).toBe(true);
expect((error as Error).message).toEqual("an error occurred");
expect(collections).not.toBeDefined();
});
});
describe("generate report", () => {
const mock_generate_api = jest.spyOn(AdminAPI, "requestInventoryReport");
const setResponseMessage = jest.fn();
const setShowConfirmationModal = jest.fn();
const setShowResponseModal = jest.fn();
afterEach(() => {
jest.clearAllMocks();
queryClient.clear();
});
afterAll(() => mock_generate_api.mockRestore());
it("handles api success", async () => {
mock_generate_api.mockResolvedValue(GENERATE_REPORT_SUCCESS);
const { result, waitFor } = renderHook(
() => {
const reportGenerator = useGenerateReport({
...API_ENDPOINT_PARAMS,
setResponseMessage,
setShowResponseModal,
setShowConfirmationModal,
});
return { reportGenerator };
},
{ wrapper }
);
expect(result.current.reportGenerator.isIdle).toBe(true);
expect(mock_generate_api).not.toHaveBeenCalled();
expect(setResponseMessage).not.toHaveBeenCalled();
expect(setShowResponseModal).not.toHaveBeenCalled();
expect(setShowConfirmationModal).not.toHaveBeenCalled();
act(() => result.current.reportGenerator.mutate());
await waitFor(
() =>
result.current.reportGenerator.isSuccess ||
result.current.reportGenerator.isError
);
const { isSuccess, isError, error } = result.current.reportGenerator;
expect(mock_generate_api).toHaveBeenCalledWith(API_ENDPOINT_PARAMS);
expect(isSuccess).toBe(true);
expect(isError).toBe(false);
expect(error).toBeNull();
expect(setResponseMessage).toHaveBeenCalledWith(
`✅ ${GENERATE_REPORT_SUCCESS.message}`
);
expect(setShowResponseModal).toHaveBeenCalledWith(true);
expect(setShowConfirmationModal).toHaveBeenCalledWith(false);
});
it("handles api error", async () => {
mock_generate_api.mockImplementation(() => {
throw new Error("an error occurred");
});
const { result, waitFor } = renderHook(
() => {
const reportGenerator = useGenerateReport({
...API_ENDPOINT_PARAMS,
setResponseMessage,
setShowResponseModal,
setShowConfirmationModal,
});
return { reportGenerator };
},
{ wrapper }
);
expect(result.current.reportGenerator.isIdle).toBe(true);
expect(mock_generate_api).not.toHaveBeenCalled();
expect(setResponseMessage).not.toHaveBeenCalled();
expect(setShowResponseModal).not.toHaveBeenCalled();
expect(setShowConfirmationModal).not.toHaveBeenCalled();
act(() => result.current.reportGenerator.mutate());
await waitFor(
() =>
result.current.reportGenerator.isSuccess ||
result.current.reportGenerator.isError
);
const { isSuccess, isError, error } = result.current.reportGenerator;
expect(mock_generate_api).toHaveBeenCalledWith(API_ENDPOINT_PARAMS);
expect(isSuccess).toBe(false);
expect(isError).toBe(true);
expect(error).not.toBeNull();
expect(setResponseMessage).toHaveBeenCalledWith(`❌ an error occurred`);
expect(setShowResponseModal).toHaveBeenCalledWith(true);
expect(setShowConfirmationModal).toHaveBeenCalledWith(false);
});
});
});
describe("mock server functionality", () => {
const server = setupMockServer();
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => {
server.resetHandlers();
server.restoreHandlers();
});
it("returns the success value for successful report info (get) requests", async () => {
const result = await AdminAPI.getInventoryReportInfo(API_ENDPOINT_PARAMS);
expect(result).toEqual(INFO_SUCCESS_SOME_COLLECTIONS);
});
it("returns an error value for unsuccessful report info (get) requests", async () => {
server.use(
http.get(
MOCK_SERVER_API_ENDPOINT_PATH,
() => new HttpResponse(null, { status: 404 })
)
);
let error = undefined;
try {
await AdminAPI.getInventoryReportInfo(API_ENDPOINT_PARAMS);
} catch (e) {
error = await e;
}
expect(error?.message).toMatch("Request failed with status 404: GET ");
});
it("returns the expected value for generate report (post) requests", async () => {
const result = await AdminAPI.requestInventoryReport(API_ENDPOINT_PARAMS);
expect(result).toEqual(GENERATE_REPORT_SUCCESS);
});
it("returns an error value for unsuccessful generate report (post) requests", async () => {
server.use(
http.post(
MOCK_SERVER_API_ENDPOINT_PATH,
() => new HttpResponse(null, { status: 404 })
)
);
let error = undefined;
try {
await AdminAPI.requestInventoryReport(API_ENDPOINT_PARAMS);
} catch (e) {
error = await e;
}
expect(error?.message).toMatch("Request failed with status 404: POST ");
});
});
describe("component rendering", () => {
const onHide = jest.fn();
const server = setupMockServer();
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => {
server.resetHandlers();
server.restoreHandlers();
onHide.mockReset();
queryClient.clear();
});
const LIBRARY = "library-short-name";
const email = "librarian@example.com";
const EMAIL_CONTEXT_PROVIDER_PROPS = { email };
const NO_EMAIL_CONTEXT_PROVIDER_PROPS = { email: undefined };
it("shows the modal when `show` is true", async () => {
const {
getAllByRole,
queryAllByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
expect(queryAllByRole("dialog")).toHaveLength(0);
rerender(
);
getAllByRole("dialog");
});
it("initially shows a request confirmation", () => {
const { getByRole } = renderWithProviders(
,
{ queryClient }
);
const modalContent = getByRole("document");
const heading = within(modalContent).getByRole("heading", {
level: MODAL_HEADING_LEVEL,
});
const modalBody = modalContent.querySelector(".modal-body");
getByRole("button", { name: CONFIRM_BUTTON_CONTENT });
getByRole("button", { name: CANCEL_BUTTON_CONTENT });
expect(heading).toHaveTextContent(CONFIRMATION_MODAL_HEADING);
expect(modalBody).toHaveTextContent("inventory report will be generated");
});
it("shows admin email, when present, in request confirmation", () => {
let { getByRole } = renderWithProviders(
,
{ queryClient, appConfigSettings: EMAIL_CONTEXT_PROVIDER_PROPS }
);
let modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(
"will be generated in the background and emailed to you at"
);
expect(modalBody).toHaveTextContent(email);
({ getByRole } = renderWithProviders(
,
{ queryClient, appConfigSettings: NO_EMAIL_CONTEXT_PROVIDER_PROPS }
));
modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(
"will be generated in the background and emailed to you"
);
expect(modalBody).not.toHaveTextContent(email);
});
it("clicking 'cancel' hides the component", async () => {
const user = userEvent.setup();
let show = true;
onHide.mockImplementation(() => (show = false));
const {
getAllByRole,
getByRole,
queryAllByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
getAllByRole("dialog");
expect(show).toBe(true);
expect(onHide).not.toHaveBeenCalled();
const cancelButton = getByRole("button", { name: CANCEL_BUTTON_CONTENT });
await user.click(cancelButton);
// `onHide` was called and modal elements are gone on the next render.
expect(onHide).toHaveBeenCalled();
expect(show).toBe(false);
rerender(
);
expect(queryAllByRole("dialog")).toHaveLength(0);
});
it("updates confirmation message, if report info request arrives in time", async () => {
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
let modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(UNKNOWN_COLLECTIONS_MESSAGE);
expect(modalBody).not.toHaveTextContent(COLLECTION_NAME);
await new Promise(process.nextTick);
rerender(
);
modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(SOME_COLLECTIONS_MESSAGE);
expect(modalBody).toHaveTextContent(COLLECTION_NAME);
});
it("disables report generation, if info indicates no collections", async () => {
server.use(
http.get(MOCK_SERVER_API_ENDPOINT_PATH, () =>
HttpResponse.json(INFO_SUCCESS_NO_COLLECTIONS, { status: 200 })
)
);
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
let modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(UNKNOWN_COLLECTIONS_MESSAGE);
expect(modalBody).not.toHaveTextContent(COLLECTION_NAME);
let requestButton = getByRole("button", { name: CONFIRM_BUTTON_CONTENT });
let cancelButton = getByRole("button", { name: CANCEL_BUTTON_CONTENT });
expect(requestButton).toBeEnabled();
expect(cancelButton).toBeEnabled();
await new Promise(process.nextTick);
rerender(
);
modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(NO_COLLECTIONS_MESSAGE);
expect(modalBody).not.toHaveTextContent(COLLECTION_NAME);
requestButton = getByRole("button", { name: CONFIRM_BUTTON_CONTENT });
cancelButton = getByRole("button", { name: CANCEL_BUTTON_CONTENT });
expect(requestButton).not.toBeEnabled();
expect(cancelButton).toBeEnabled();
});
it("displays minimal confirmation message, if report info request unsuccessful", async () => {
server.use(
http.get(
MOCK_SERVER_API_ENDPOINT_PATH,
() => new HttpResponse(null, { status: 404 })
)
);
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
let modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(UNKNOWN_COLLECTIONS_MESSAGE);
expect(modalBody).not.toHaveTextContent(COLLECTION_NAME);
await new Promise(process.nextTick);
rerender(
);
modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(UNKNOWN_COLLECTIONS_MESSAGE);
expect(modalBody).not.toHaveTextContent(COLLECTION_NAME);
});
it("requests report generation, if request confirmed", async () => {
const user = userEvent.setup();
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
const confirmationButton = getByRole("button", {
name: CONFIRM_BUTTON_CONTENT,
});
let modalHeading = getByRole("heading", { level: MODAL_HEADING_LEVEL });
expect(modalHeading).toHaveTextContent(CONFIRMATION_MODAL_HEADING);
await user.click(confirmationButton);
rerender(
);
getByRole("button", { name: ACK_RESPONSE_BUTTON_CONTENT });
modalHeading = getByRole("heading", { level: MODAL_HEADING_LEVEL });
expect(modalHeading).toHaveTextContent(ACK_RESPONSE_HEADING);
});
it("displays success message, when request is successful", async () => {
const user = userEvent.setup();
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
const confirmationButton = getByRole("button", {
name: CONFIRM_BUTTON_CONTENT,
});
await user.click(confirmationButton);
rerender(
);
getByRole("button", { name: ACK_RESPONSE_BUTTON_CONTENT });
const modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(
`✅ ${GENERATE_REPORT_SUCCESS.message}`
);
});
it("displays error message, when request is unsuccessful", async () => {
server.use(
http.post(
MOCK_SERVER_API_ENDPOINT_PATH,
() => new HttpResponse(null, { status: 404 })
)
);
const user = userEvent.setup();
const {
getByRole,
rerender,
} = renderWithProviders(
,
{ queryClient }
);
const confirmationButton = getByRole("button", {
name: CONFIRM_BUTTON_CONTENT,
});
await user.click(confirmationButton);
rerender(
);
getByRole("button", { name: ACK_RESPONSE_BUTTON_CONTENT });
const modalBody = getByRole("document").querySelector(".modal-body");
expect(modalBody).toHaveTextContent(
"❌ Request failed with status 404: POST"
);
});
});
});