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" ); }); }); });