Skip to content

Commit

Permalink
Merge pull request #393 from NYPL/SCC-3760/edd-page-ui
Browse files Browse the repository at this point in the history
SCC-3760 - EDD page UI
  • Loading branch information
dgcohen authored Nov 19, 2024
2 parents 7e9abd8 + 3831266 commit c958141
Show file tree
Hide file tree
Showing 19 changed files with 1,240 additions and 90 deletions.
4 changes: 1 addition & 3 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Added

- Add EDD request page and field validation logic (SCC-3760)
- Holding location filters to refine search and advanced search panels
- "Go back" button to advanced search

### Added

- Add server side delivery location fetch and location utils (SCC-3759)
- Add holding location filters to refine search and advanced search panels
- Add go back button to advanced search
Expand Down
297 changes: 297 additions & 0 deletions __test__/pages/hold/eddRequestPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import {
render,
screen,
fireEvent,
waitFor,
} from "../../../src/utils/testUtils"
import userEvent from "@testing-library/user-event"

import EDDRequestPage, {
getServerSideProps,
} from "../../../pages/hold/request/[id]/edd"

import initializePatronTokenAuth, {
doRedirectBasedOnNyplAccountRedirects,
} from "../../../src/server/auth"
import { fetchBib } from "../../../src/server/api/bib"
import { bibWithItems, bibWithSingleAeonItem } from "../../fixtures/bibFixtures"
import {
BASE_URL,
PATHS,
EDD_FORM_FIELD_COPY,
} from "../../../src/config/constants"
import { fetchDeliveryLocations } from "../../../src/server/api/hold"

jest.mock("../../../src/server/auth")
jest.mock("../../../src/server/api/bib")
jest.mock("../../../src/server/sierraClient")
jest.mock("../../../src/server/api/hold")

jest.mock("next/router", () => jest.requireActual("next-router-mock"))

const mockRes = {
setHeader: jest.fn(),
}
const id = "b15080796-i39333697"
const mockReq = {
headers: {
host: "local.nypl.org:8080",
},
url: `/hold/request/${id}/edd`,
cookies: {
nyplIdentityPatron: '{"access_token":123}',
},
}

describe("EDD Request page", () => {
describe("logout redirect handling", () => {
beforeEach(() => {
;(initializePatronTokenAuth as jest.Mock).mockResolvedValue({
isTokenValid: true,
errorCode: null,
decodedPatron: { sub: "123" },
})
;(fetchBib as jest.Mock).mockResolvedValue({
discoveryBibResult: bibWithItems.resource,
status: 200,
})
;(fetchDeliveryLocations as jest.Mock).mockResolvedValue({
eddRequestable: true,
status: 200,
})
})

it("redirects if cookie count is less than 3", async () => {
;(doRedirectBasedOnNyplAccountRedirects as jest.Mock).mockReturnValue(
true
)
;(initializePatronTokenAuth as jest.Mock).mockResolvedValue({
isTokenValid: false,
})

const responseWithZeroRedirects = await getServerSideProps({
params: { id },
req: mockReq,
res: mockRes,
})
expect(responseWithZeroRedirects.redirect).toBeDefined()
const responseWithTwoRedirects = await getServerSideProps({
params: { id: "123-456" },
req: { ...mockReq, cookies: { nyplAccountRedirects: 2 } },
res: mockRes,
})
expect(responseWithTwoRedirects.redirect).toBeDefined()
})
it("does not redirect if doRedirect method returns false", async () => {
;(doRedirectBasedOnNyplAccountRedirects as jest.Mock).mockReturnValue(
false
)
;(initializePatronTokenAuth as jest.Mock).mockResolvedValue({
decodedPatron: { sub: "123" },
isTokenValid: false,
})

const responseWithoutRedirect = await getServerSideProps({
params: { id },
req: mockReq,
res: mockRes,
})
expect(responseWithoutRedirect.redirect).not.toBeDefined()
})
it("does not redirect if patron is authenticated", async () => {
const response = await getServerSideProps({
params: { id },
req: mockReq,
res: mockRes,
})
expect(response.redirect).toBeUndefined()
})
it("updates the nyplAccountRedirectsCookie upon redirecting", async () => {
;(doRedirectBasedOnNyplAccountRedirects as jest.Mock).mockReturnValue(
true
)
;(initializePatronTokenAuth as jest.Mock).mockResolvedValue({
decodedPatron: { sub: "123" },
isTokenValid: false,
})
await getServerSideProps({
params: { id },
res: mockRes,
req: mockReq,
})
expect(mockRes.setHeader.mock.calls[0]).toStrictEqual([
"Set-Cookie",
"nyplAccountRedirects=1; Max-Age=10; path=/; domain=.nypl.org;",
])
})
})
describe("aeon redirect handling", () => {
beforeEach(() => {
;(initializePatronTokenAuth as jest.Mock).mockResolvedValue({
isTokenValid: true,
errorCode: null,
decodedPatron: { sub: "123" },
})
;(fetchBib as jest.Mock).mockResolvedValue({
discoveryBibResult: bibWithSingleAeonItem.resource,
status: 200,
})
})

it("redirects to aeonUrl when present in the fetched item", async () => {
const responseWithAeonRedirect = await getServerSideProps({
params: { id },
res: mockRes,
req: mockReq,
})
expect(responseWithAeonRedirect.redirect).toStrictEqual({
destination: bibWithSingleAeonItem.resource.items[0].aeonUrl[0],
permanent: false,
})
})
})
describe("EDD Request page UI", () => {
beforeEach(() => {
render(
<EDDRequestPage
discoveryBibResult={bibWithItems.resource}
discoveryItemResult={bibWithItems.resource.items[2]}
patronId="123"
eddRequestable={true}
isAuthenticated={true}
/>
)
})

it("renders an H2", () => {
expect(screen.getAllByRole("heading", { level: 2 })[0]).toHaveTextContent(
"Request scan"
)
})

it("renders the top bib and item details", () => {
expect(screen.getAllByTestId("title")[0]).toHaveTextContent(
"Urban spaghetti."
)
expect(screen.getByTestId("call-number")).toHaveTextContent(
"JFK 01-374 v. 2, no. 1 (1999)"
)
expect(screen.getByTestId("volume-date")).toHaveTextContent(
"v. 2, no. 1 (1999)"
)
})

it("renders an edd request form", () => {
expect(screen.getByTestId("edd-request-form")).toBeInTheDocument()
})
})
describe("EDD Request error handling", () => {
beforeEach(async () => {
render(
<EDDRequestPage
discoveryBibResult={bibWithItems.resource}
discoveryItemResult={bibWithItems.resource.items[0]}
patronId="123"
eddRequestable={true}
isAuthenticated={true}
/>
)

global.fetch = jest.fn().mockImplementationOnce(() =>
Promise.resolve({
status: 404,
json: () => Promise.resolve({ success: true }),
})
)

// Fill in all required form fields
await userEvent.type(
screen.getByPlaceholderText(
EDD_FORM_FIELD_COPY.emailAddress.placeholder
),
EDD_FORM_FIELD_COPY.emailAddress.placeholder
)
await userEvent.type(
screen.getByPlaceholderText(EDD_FORM_FIELD_COPY.startPage.placeholder),
EDD_FORM_FIELD_COPY.startPage.placeholder
)
await userEvent.type(
screen.getByPlaceholderText(EDD_FORM_FIELD_COPY.endPage.placeholder),
EDD_FORM_FIELD_COPY.endPage.placeholder
)
await userEvent.type(
screen.getByPlaceholderText(
EDD_FORM_FIELD_COPY.chapterTitle.placeholder
),
EDD_FORM_FIELD_COPY.chapterTitle.placeholder
)
})

it("shows an error when the request fails", async () => {
fireEvent(screen.getByText("Submit request"), new MouseEvent("click"))
await waitFor(() => {
expect(screen.getByTestId("hold-request-error")).toBeInTheDocument()
})

expect(screen.getByText("Request failed")).toBeInTheDocument()

expect(
screen.queryByText(
"We were unable to process your request at this time. Please try again, ",
{ exact: false }
)
).toBeInTheDocument()

expect(
screen.getByRole("button", { name: "contact us" })
).toBeInTheDocument()

expect(
screen.getByText("start a new search", { exact: false })
).toHaveAttribute("href", `${BASE_URL}${PATHS.SEARCH}`)
})

it("populates the feedback form with the call number and appropriate copy when the request fails", async () => {
fireEvent(screen.getByText("Submit request"), new MouseEvent("click"))
await waitFor(() => {
expect(screen.getByTestId("hold-request-error")).toBeInTheDocument()
})

await userEvent.click(screen.getByText("contact us"))

await waitFor(() => {
expect(
screen.queryByText("We are here to help!", { exact: false })
).toBeInTheDocument()

expect(
screen.queryByText(
`Request failed for call number ${bibWithItems.resource.items[0].shelfMark[0]}`,
{
exact: false,
}
)
).toBeInTheDocument()
})
})

it("shows an error when any field is invalid", async () => {
fireEvent.change(
screen.getByPlaceholderText(
EDD_FORM_FIELD_COPY.emailAddress.placeholder
),
{ target: { value: "bademail" } }
)
fireEvent(screen.getByText("Submit request"), new MouseEvent("click"))

await waitFor(() => {
expect(screen.getByTestId("hold-request-error")).toBeInTheDocument()
})
expect(
screen.getByText(
"Some fields contain errors. Please correct and submit again."
)
).toBeInTheDocument()
})
})
})
File renamed without changes.
19 changes: 19 additions & 0 deletions pages/api/hold/request/[id]/edd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextApiRequest, NextApiResponse } from "next"

/**
* Default API route handler for EDD requests
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
res.status(500).json({
error: "Please use a POST request for the EDD Request API endpoint",
})
}

// TODO: Add success state when EDD server-side post function is ready
res.status(500).json({
error: "Fake error to mock EDD request failure",
})
}

export default handler
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next"

import { postHoldRequest } from "../../../../src/server/api/hold"
import { BASE_URL, PATHS } from "../../../../src/config/constants"
import { postHoldRequest } from "../../../../../src/server/api/hold"
import { BASE_URL, PATHS } from "../../../../../src/config/constants"

/**
* Default API route handler for Hold requests
Expand Down
Loading

0 comments on commit c958141

Please sign in to comment.