diff --git a/assets/src/components/notificationCard.tsx b/assets/src/components/notificationCard.tsx index ce4022f8a..b96f6205c 100644 --- a/assets/src/components/notificationCard.tsx +++ b/assets/src/components/notificationCard.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react" +import React, { ReactNode, useCallback, useState } from "react" import { useRoute, useRoutes } from "../contexts/routesContext" import { BlockWaiverNotification, @@ -13,6 +13,13 @@ import { CardBody, CardProperties, CardReadable } from "./card" import { fullStoryEvent } from "../helpers/fullStory" import { RoutePill } from "./routePill" import inTestGroup, { TestGroups } from "../userInTestGroup" +import { useApiCall } from "../hooks/useApiCall" +import { fetchDetour } from "../api" +import { createDetourMachine } from "../models/createDetourMachine" +import { isValidSnapshot } from "../util/isValidSnapshot" +import { isErr } from "../util/result" +import { DetourModal } from "./detours/detourModal" +import { DetourId } from "../models/detoursList" export const NotificationCard = ({ notification, @@ -27,6 +34,13 @@ export const NotificationCard = ({ hideLatestNotification?: () => void noFocusOrHover?: boolean }) => { + const [showDetourModal, setShowDetourModal] = useState(false) + + const detourId = + notification.content.$type === NotificationType.Detour + ? notification.content.detourId + : undefined + const routes = useRoutes( isBlockWaiverNotification(notification) ? notification.content.routeIds : [] ) @@ -43,52 +57,112 @@ export const NotificationCard = ({ return null } + const onClose = () => { + setShowDetourModal(false) + } const isUnread = notification.state === "unread" return ( - {title(notification)}} - style="kiwi" - isActive={isUnread} - openCallback={() => { - openVPPForCurrentVehicle(notification) - - if (hideLatestNotification) { - hideLatestNotification() - } - - if (notification.content.$type === NotificationType.BridgeMovement) { - fullStoryEvent("User clicked Chelsea Bridge Notification", {}) - } - }} - closeCallback={hideLatestNotification} - time={notification.createdAt} - noFocusOrHover={noFocusOrHover} - > - {description(notification, routes, routeAtCreation)} - {isBlockWaiverNotification(notification) && ( - 0 - ? notification.content.runIds.join(", ") - : null, - }, - { - label: "Operator", - value: - notification.content.operatorName !== null && - notification.content.operatorId !== null - ? `${notification.content.operatorName} #${notification.content.operatorId}` - : null, - sensitive: true, - }, - ]} + <> + {title(notification)}} + style="kiwi" + isActive={isUnread} + openCallback={() => { + if (notification.content.$type === NotificationType.Detour) { + setShowDetourModal(true) + } else { + openVPPForCurrentVehicle(notification) + + if (hideLatestNotification) { + hideLatestNotification() + } + + if ( + notification.content.$type === NotificationType.BridgeMovement + ) { + fullStoryEvent("User clicked Chelsea Bridge Notification", {}) + } + } + }} + closeCallback={hideLatestNotification} + time={notification.createdAt} + noFocusOrHover={noFocusOrHover} + > + + {description(notification, routes, routeAtCreation)} + + {isBlockWaiverNotification(notification) && ( + 0 + ? notification.content.runIds.join(", ") + : null, + }, + { + label: "Operator", + value: + notification.content.operatorName !== null && + notification.content.operatorId !== null + ? `${notification.content.operatorName} #${notification.content.operatorId}` + : null, + sensitive: true, + }, + ]} + /> + )} + + {detourId && ( + )} - + + ) +} + +const DetourNotificationModal = ({ + detourId, + show, + onClose, +}: { + detourId: DetourId + show: boolean + onClose: () => void +}) => { + const { result: stateOfDetourModal } = useApiCall({ + apiCall: useCallback(async () => { + if (detourId === undefined) { + return undefined + } + const detourResponse = await fetchDetour(detourId) + if (isErr(detourResponse)) { + return undefined + } + const snapshot = isValidSnapshot( + createDetourMachine, + detourResponse.ok.state + ) + if (isErr(snapshot)) { + return undefined + } + return snapshot.ok + }, [detourId]), + }) + + return ( + ) } diff --git a/assets/tests/components/notificationCard.openDetour.test.tsx b/assets/tests/components/notificationCard.openDetour.test.tsx new file mode 100644 index 000000000..8256b39bb --- /dev/null +++ b/assets/tests/components/notificationCard.openDetour.test.tsx @@ -0,0 +1,98 @@ +import { jest, describe, test, expect } from "@jest/globals" +import "@testing-library/jest-dom/jest-globals" +import React from "react" +import { render, screen } from "@testing-library/react" +import { NotificationCard } from "../../src/components/notificationCard" +import { detourActivatedNotificationFactory } from "../factories/notification" +import routeFactory from "../factories/route" +import userEvent from "@testing-library/user-event" +import { RoutesProvider } from "../../src/contexts/routesContext" +import { fetchDetour, fetchDetours } from "../../src/api" +import { Ok } from "../../src/util/result" +import { detourListFactory } from "../factories/detourListFactory" +import { createActor } from "xstate" +import { createDetourMachine } from "../../src/models/createDetourMachine" +import { originalRouteFactory } from "../factories/originalRouteFactory" +import { shapePointFactory } from "../factories/shapePointFactory" +import getTestGroups from "../../src/userTestGroups" +import { TestGroups } from "../../src/userInTestGroup" + +jest.mock("../../src/api") +jest.mock("../../src/helpers/fullStory") +jest.mock("../../src/userTestGroups") + +const routes = [ + routeFactory.build({ + id: "route1", + name: "r1", + }), + routeFactory.build({ + id: "route2", + name: "r2", + }), + routeFactory.build({ + id: "route3", + name: "r3", + }), +] + +describe("NotificationCard", () => { + test("renders detour details modal to match mocked fetchDetour", async () => { + jest + .mocked(getTestGroups) + .mockReturnValue([TestGroups.DetoursPilot, TestGroups.DetoursList]) + + jest.mocked(fetchDetours).mockResolvedValue(Ok(detourListFactory.build())) + + // Stub out a detour machine, and start a detour-in-progress + const machine = createActor(createDetourMachine, { + input: originalRouteFactory.build(), + }).start() + machine.send({ + type: "detour.edit.place-waypoint-on-route", + location: shapePointFactory.build(), + }) + machine.send({ + type: "detour.edit.place-waypoint", + location: shapePointFactory.build(), + }) + machine.send({ + type: "detour.edit.place-waypoint-on-route", + location: shapePointFactory.build(), + }) + machine.send({ type: "detour.edit.done" }) + + const snapshot = machine.getPersistedSnapshot() + machine.stop() + + // Return the state of the machine as the fetchDetour mocked value, + // even if it doesn't match the detour clicked + jest.mocked(fetchDetour).mockResolvedValue( + Ok({ + updatedAt: 1726147775, + author: "fake@email.com", + state: snapshot, + }) + ) + + const n = detourActivatedNotificationFactory.build() + + render( + + {}} + /> + + ) + + // await userEvent.click(screen.getByText(/run1/)) + await userEvent.click(screen.getByRole("button", { name: /Detour/ })) + // Render modal based on mocked value, which is a detour-in-progress + + expect( + screen.getByRole("heading", { name: "Share Detour Details" }) + ).toBeVisible() + }) +}) diff --git a/assets/tests/factories/notification.ts b/assets/tests/factories/notification.ts index f52da227d..f2fc2a3e4 100644 --- a/assets/tests/factories/notification.ts +++ b/assets/tests/factories/notification.ts @@ -4,6 +4,7 @@ import { BlockWaiverReason, BridgeLoweredNotification, BridgeRaisedNotification, + DetourNotification, Notification, NotificationType, } from "../../src/realtime" @@ -84,3 +85,23 @@ export const bridgeLoweredNotificationFactory = Factory.define< state: "unread", content: bridgeLoweredNotificationContentFactory.build(), })) + +const detourActivatedNotificationContentFactory = + Factory.define(() => ({ + $type: NotificationType.Detour, + status: "activated", + detourId: 79, + headsign: "79 - Wooded Mountains", + route: "79", + direction: "Inbound", + origin: "Maple Knoll", + })) + +export const detourActivatedNotificationFactory = Factory.define< + Notification +>(({ sequence }) => ({ + id: sequence.toString(), + createdAt: new Date(), + state: "unread", + content: detourActivatedNotificationContentFactory.build(), +}))