Skip to content

Commit

Permalink
Add tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
tdilauro committed Jul 2, 2024
1 parent adfc824 commit 9401ed1
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 3 deletions.
199 changes: 199 additions & 0 deletions tests/jest/components/BookEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import * as React from "react";
import { renderWithProviders } from "../testUtils/withProviders";
import userEvent from "@testing-library/user-event";
import {
bookEditorApiEndpoints,
PER_LIBRARY_SUPPRESS_REL,
PER_LIBRARY_UNSUPPRESS_REL,
} from "../../../src/features/book/bookEditorSlice";
import { BookDetailsEditor } from "../../../src/components/BookDetailsEditor";
import { expect } from "chai";
import { store } from "../../../src/store";
import * as fetchMock from "fetch-mock-jest";

describe("BookDetails", () => {
const suppressPerLibraryLink = {
href: "/suppress/href",
rel: PER_LIBRARY_SUPPRESS_REL,
};
const unsuppressPerLibraryLink = {
href: "/unsuppress/href",
rel: PER_LIBRARY_UNSUPPRESS_REL,
};

let fetchBookData;
let fetchRoles;
let fetchMedia;
let fetchLanguages;
let postBookData;
let dispatchProps;
const suppressBook = jest.fn().mockImplementation((url: string) =>
store.dispatch(
bookEditorApiEndpoints.endpoints.suppressBook.initiate({
url,
csrfToken: "token",
})
)
);
const unsuppressBook = jest.fn().mockImplementation((url: string) =>
store.dispatch(
bookEditorApiEndpoints.endpoints.unsuppressBook.initiate({
url,
csrfToken: "token",
})
)
);

beforeAll(() => {
fetchMock
.post("/suppress/href", {
status: 200,
body: { message: "Successfully suppressed book availability." },
})
.delete("/unsuppress/href", {
status: 200,
body: { message: "Successfully unsuppressed book availability." },
});
});
beforeEach(() => {
fetchBookData = jest.fn();
fetchRoles = jest.fn();
fetchMedia = jest.fn();
fetchLanguages = jest.fn();
postBookData = jest.fn();
dispatchProps = {
fetchBookData,
fetchRoles,
fetchMedia,
fetchLanguages,
postBookData,
suppressBook,
unsuppressBook,
};
});
afterEach(() => {
jest.clearAllMocks();
fetchMock.resetHistory();
});
afterAll(() => {
jest.restoreAllMocks();
fetchMock.restore();
});

it("uses modal for suppress book confirmation", async () => {
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
Object.assign(fetchMock.config, {
fetch,
Headers,
Request,
Response,
});

const user = userEvent.setup();

const { getByRole, getByText, queryByRole } = renderWithProviders(
<BookDetailsEditor
bookData={{ id: "id", title: "title", suppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
{...dispatchProps}
/>
);

// The `Hide` button should be present.
const hideButton = getByRole("button", { name: "Hide" });

// Clicking `Hide` should show the book suppression modal.
await user.click(hideButton);
getByRole("heading", { level: 4, name: "Suppressing Availability" });
getByText(/to hide this title from your library's catalog/);
let confirmButton = getByRole("button", { name: "Suppress Availability" });
let cancelButton = getByRole("button", { name: "Cancel" });

// Clicking `Cancel` should close the modal.
await user.click(cancelButton);
confirmButton = queryByRole("button", { name: "Suppress Availability" });
cancelButton = queryByRole("button", { name: "Cancel" });
expect(confirmButton).to.be.null;
expect(cancelButton).to.be.null;

// Clicking `Hide` again should show the modal again.
await user.click(hideButton);
confirmButton = getByRole("button", { name: "Suppress Availability" });

// Clicking the confirmation button should invoke the API and show a confirmation.
await user.click(confirmButton);
getByRole("heading", { level: 4, name: "Result" });
getByText(/Successfully suppressed book availability/);
getByRole("button", { name: "Dismiss" });

// Check that the API was invoked.
expect(suppressBook.mock.calls.length).to.equal(1);
expect(suppressBook.mock.calls[0][0]).to.equal("/suppress/href");
const fetchCalls = fetchMock.calls();
expect(fetchCalls.length).to.equal(1);
const fetchCall = fetchCalls[0];
const fetchOptions = fetchCalls[0][1];
expect(fetchCall[0]).to.equal("/suppress/href");
expect(fetchOptions["headers"]["X-CSRF-Token"]).to.contain("token");
expect(fetchOptions["method"]).to.equal("POST");
});
it("uses modal for unsuppress book confirmation", async () => {
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
Object.assign(fetchMock.config, {
fetch,
Headers,
Request,
Response,
});

const user = userEvent.setup();

const { getByRole, getByText, queryByRole } = renderWithProviders(
<BookDetailsEditor
bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
{...dispatchProps}
/>
);

// The `Restore` button should be present.
const restoreButton = getByRole("button", { name: "Restore" });

// Clicking `Restore` should show the book un/suppression modal.
await user.click(restoreButton);
getByRole("heading", { level: 4, name: "Restoring Availability" });
getByText(/to make this title visible in your library's catalog/);
let confirmButton = getByRole("button", { name: "Restore Availability" });
let cancelButton = getByRole("button", { name: "Cancel" });

// Clicking `Cancel` should close the modal.
await user.click(cancelButton);
confirmButton = queryByRole("button", { name: "Restore Availability" });
cancelButton = queryByRole("button", { name: "Cancel" });
expect(confirmButton).to.be.null;
expect(cancelButton).to.be.null;

// Clicking `Restore` again should show the modal again.
await user.click(restoreButton);
confirmButton = getByRole("button", { name: "Restore Availability" });

// Clicking the confirmation button should invoke the API and show a confirmation.
await user.click(confirmButton);
getByRole("heading", { level: 4, name: "Result" });
getByText(/Successfully unsuppressed book availability/);
getByRole("button", { name: "Dismiss" });

// Check that the API was invoked.
expect(unsuppressBook.mock.calls.length).to.equal(1);
expect(unsuppressBook.mock.calls[0][0]).to.equal("/unsuppress/href");
const fetchCalls = fetchMock.calls();
expect(fetchCalls.length).to.equal(1);
const fetchCall = fetchCalls[0];
const fetchOptions = fetchCalls[0][1];
expect(fetchCall[0]).to.equal("/unsuppress/href");
expect(fetchOptions["headers"]["X-CSRF-Token"]).to.contain("token");
expect(fetchOptions["method"]).to.equal("DELETE");
});
});
22 changes: 19 additions & 3 deletions tests/jest/testUtils/withProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import * as React from "react";
import { Provider, ProviderProps } from "react-redux";
import ContextProvider, {
ContextProviderProps,
} from "../../../src/components/ContextProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, RenderOptions, RenderResult } from "@testing-library/react";
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
import { store } from "../../../src/store";

export type TestProviderWrapperOptions = {
reduxProviderProps?: ProviderProps;
contextProviderProps?: Partial<ContextProviderProps>;
queryClient?: QueryClient;
};
export type TestRenderWrapperOptions = TestProviderWrapperOptions & {
renderOptions?: Omit<RenderOptions, "queries">;
};

// The `store` argument is required for the Redux Provider and should
// be the same for both the Redux Provider and the ContextProvider.
const defaultReduxStore = store;

// The `csrfToken` context provider prop is required, so we provide
// a default value here, so it can be easily merged with other props.
const defaultContextProviderProps: ContextProviderProps = {
Expand All @@ -26,11 +33,15 @@ const defaultContextProviderProps: ContextProviderProps = {
* a React element for testing.
*
* @param {TestProviderWrapperOptions} options
* @param options.reduxProviderProps Props to pass to the Redux `Provider` wrapper
* @param {ContextProviderProps} options.contextProviderProps Props to pass to the ContextProvider wrapper
* @param {QueryClient} options.queryClient A `tanstack/react-query` QueryClient
* @returns {React.FunctionComponent} A React component that wraps children with our providers
*/
export const componentWithProviders = ({
reduxProviderProps = {
store: defaultReduxStore,
},
contextProviderProps = {
csrfToken: "",
featureFlags: defaultFeatureFlags,
Expand All @@ -40,11 +51,16 @@ export const componentWithProviders = ({
const effectiveContextProviderProps = {
...defaultContextProviderProps,
...contextProviderProps,
...reduxProviderProps.store, // Context and Redux Provider stores must match.
};
const wrapper = ({ children }) => (
<ContextProvider {...effectiveContextProviderProps}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ContextProvider>
<Provider {...reduxProviderProps}>
<ContextProvider {...effectiveContextProviderProps}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ContextProvider>
</Provider>
);
wrapper.displayName = "TestWrapperComponent";
return wrapper;
Expand Down

0 comments on commit 9401ed1

Please sign in to comment.