From 27de03c99228e184a918e35b2a9cfdae9c5527cc Mon Sep 17 00:00:00 2001
From: Josh Larson
Date: Wed, 25 Sep 2024 15:39:06 -0400
Subject: [PATCH] feat: Deactivate detour flow (#2805)
---
.../components/detours/activeDetourPanel.tsx | 12 +-
.../detours/deactivateDetourModal.tsx | 32 +++++
.../src/components/detours/diversionPage.tsx | 20 ++-
assets/src/models/createDetourMachine.ts | 29 +++-
.../detours/activeDetourPanel.stories.tsx | 2 +-
.../detours/diversionPage.activate.test.tsx | 12 --
.../detours/diversionPage.deactivate.test.tsx | 133 ++++++++++++++++++
lib/skate/detours/detours.ex | 7 +-
.../controllers/detours_controller_test.exs | 8 +-
9 files changed, 224 insertions(+), 31 deletions(-)
create mode 100644 assets/src/components/detours/deactivateDetourModal.tsx
create mode 100644 assets/tests/components/detours/diversionPage.deactivate.test.tsx
diff --git a/assets/src/components/detours/activeDetourPanel.tsx b/assets/src/components/detours/activeDetourPanel.tsx
index 49f9100bf..27adae45a 100644
--- a/assets/src/components/detours/activeDetourPanel.tsx
+++ b/assets/src/components/detours/activeDetourPanel.tsx
@@ -1,4 +1,4 @@
-import React from "react"
+import React, { PropsWithChildren } from "react"
import { DetourDirection } from "../../models/detour"
import { Button, ListGroup } from "react-bootstrap"
import { Panel } from "./diversionPage"
@@ -10,7 +10,7 @@ import {
} from "../../helpers/bsIcons"
import { AffectedRoute, MissedStops } from "./detourPanelComponents"
-export interface ActiveDetourPanelProps {
+export interface ActiveDetourPanelProps extends PropsWithChildren {
directions?: DetourDirection[]
connectionPoints?: string[]
missedStops?: Stop[]
@@ -18,7 +18,7 @@ export interface ActiveDetourPanelProps {
routeDescription: string
routeOrigin: string
routeDirection: string
- onDeactivateDetour?: () => void
+ onOpenDeactivateModal?: () => void
onNavigateBack: () => void
}
@@ -30,8 +30,9 @@ export const ActiveDetourPanel = ({
routeDescription,
routeOrigin,
routeDirection,
- onDeactivateDetour,
+ onOpenDeactivateModal,
onNavigateBack,
+ children,
}: ActiveDetourPanelProps) => (
@@ -94,12 +95,13 @@ export const ActiveDetourPanel = ({
+ {children}
)
diff --git a/assets/src/components/detours/deactivateDetourModal.tsx b/assets/src/components/detours/deactivateDetourModal.tsx
new file mode 100644
index 000000000..bd7b1fb8e
--- /dev/null
+++ b/assets/src/components/detours/deactivateDetourModal.tsx
@@ -0,0 +1,32 @@
+import React from "react"
+import { Button, Modal } from "react-bootstrap"
+
+export const DeactivateDetourModal = ({
+ onDeactivate,
+ onCancel,
+}: {
+ onDeactivate: () => void
+ onCancel: () => void
+}) => {
+ return (
+
+ Return to regular route?
+
+ Are you sure that you want to stop this detour and return to the regular
+ route?
+
+
+
+
+
+
+ )
+}
diff --git a/assets/src/components/detours/diversionPage.tsx b/assets/src/components/detours/diversionPage.tsx
index df73b52ff..82b8484f9 100644
--- a/assets/src/components/detours/diversionPage.tsx
+++ b/assets/src/components/detours/diversionPage.tsx
@@ -24,6 +24,7 @@ import { ActiveDetourPanel } from "./activeDetourPanel"
import { PastDetourPanel } from "./pastDetourPanel"
import userInTestGroup from "../../userInTestGroup"
import { ActivateDetour } from "./activateDetourModal"
+import { DeactivateDetourModal } from "./deactivateDetourModal"
const displayFieldsFromRouteAndPattern = (
route: Route,
@@ -349,10 +350,23 @@ export const DiversionPage = ({
routeOrigin={routeOrigin ?? "??"}
routeDirection={routeDirection ?? "??"}
onNavigateBack={onConfirmClose}
- onDeactivateDetour={() => {
- send({ type: "detour.active.deactivate" })
+ onOpenDeactivateModal={() => {
+ send({ type: "detour.active.open-deactivate-modal" })
}}
- />
+ >
+ {snapshot.matches({
+ "Detour Drawing": { Active: "Deactivating" },
+ }) ? (
+
+ send({ type: "detour.active.deactivate-modal.deactivate" })
+ }
+ onCancel={() =>
+ send({ type: "detour.active.deactivate-modal.cancel" })
+ }
+ />
+ ) : null}
+
) : snapshot.matches({ "Detour Drawing": "Past" }) ? (
) : null}
diff --git a/assets/src/models/createDetourMachine.ts b/assets/src/models/createDetourMachine.ts
index 09c90751f..35ebec8a3 100644
--- a/assets/src/models/createDetourMachine.ts
+++ b/assets/src/models/createDetourMachine.ts
@@ -80,7 +80,9 @@ export const createDetourMachine = setup({
| { type: "detour.share.activate-modal.cancel" }
| { type: "detour.share.activate-modal.back" }
| { type: "detour.share.activate-modal.activate" }
- | { type: "detour.active.deactivate" }
+ | { type: "detour.active.open-deactivate-modal" }
+ | { type: "detour.active.deactivate-modal.deactivate" }
+ | { type: "detour.active.deactivate-modal.cancel" }
| { type: "detour.save.begin-save" }
| { type: "detour.save.set-uuid"; uuid: number },
@@ -599,10 +601,29 @@ export const createDetourMachine = setup({
},
},
Active: {
- on: {
- "detour.active.deactivate": {
- target: "Past",
+ initial: "Reviewing",
+ states: {
+ Reviewing: {
+ on: {
+ "detour.active.open-deactivate-modal": {
+ target: "Deactivating",
+ },
+ },
},
+ Deactivating: {
+ on: {
+ "detour.active.deactivate-modal.deactivate": {
+ target: "Done",
+ },
+ "detour.active.deactivate-modal.cancel": {
+ target: "Reviewing",
+ },
+ },
+ },
+ Done: { type: "final" },
+ },
+ onDone: {
+ target: "Past",
},
},
Past: {},
diff --git a/assets/stories/skate-components/detours/activeDetourPanel.stories.tsx b/assets/stories/skate-components/detours/activeDetourPanel.stories.tsx
index 7638446d3..e3b5c87b0 100644
--- a/assets/stories/skate-components/detours/activeDetourPanel.stories.tsx
+++ b/assets/stories/skate-components/detours/activeDetourPanel.stories.tsx
@@ -28,7 +28,7 @@ const meta = {
routeDescription: "Harvard via Allston",
routeOrigin: "from Andrew Station",
routeDirection: "Outbound",
- onDeactivateDetour: undefined,
+ onOpenDeactivateModal: undefined,
onNavigateBack: undefined,
},
// The bootstrap CSS reset is supposed to set box-sizing: border-box by
diff --git a/assets/tests/components/detours/diversionPage.activate.test.tsx b/assets/tests/components/detours/diversionPage.activate.test.tsx
index c641b9868..1ea256c8c 100644
--- a/assets/tests/components/detours/diversionPage.activate.test.tsx
+++ b/assets/tests/components/detours/diversionPage.activate.test.tsx
@@ -374,17 +374,5 @@ describe("DiversionPage activate workflow", () => {
screen.getByRole("button", { name: "Return to regular route" })
).toBeVisible()
})
-
- test("clicking the 'Return to regular route' button shows the 'Past Detour' screen", async () => {
- await diversionPageOnActiveDetourScreen()
-
- await userEvent.click(
- screen.getByRole("button", { name: "Return to regular route" })
- )
- expect(
- screen.queryByRole("heading", { name: "Active Detour" })
- ).not.toBeInTheDocument()
- expect(screen.getByRole("heading", { name: "Past Detour" })).toBeVisible()
- })
})
})
diff --git a/assets/tests/components/detours/diversionPage.deactivate.test.tsx b/assets/tests/components/detours/diversionPage.deactivate.test.tsx
new file mode 100644
index 000000000..0437ed6c8
--- /dev/null
+++ b/assets/tests/components/detours/diversionPage.deactivate.test.tsx
@@ -0,0 +1,133 @@
+import React from "react"
+import {
+ DiversionPage as DiversionPageDefault,
+ DiversionPageProps,
+} from "../../../src/components/detours/diversionPage"
+import { originalRouteFactory } from "../../factories/originalRouteFactory"
+import { beforeEach, describe, expect, jest, test } from "@jest/globals"
+import "@testing-library/jest-dom/jest-globals"
+import getTestGroups from "../../../src/userTestGroups"
+import { TestGroups } from "../../../src/userInTestGroup"
+import { act, fireEvent, render } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import {
+ activateDetourButton,
+ originalRouteShape,
+ reviewDetourButton,
+} from "../../testHelpers/selectors/components/detours/diversionPage"
+import {
+ fetchDetourDirections,
+ fetchFinishedDetour,
+ fetchNearestIntersection,
+ fetchRoutePatterns,
+ fetchUnfinishedDetour,
+ putDetourUpdate,
+} from "../../../src/api"
+import { neverPromise } from "../../testHelpers/mockHelpers"
+import { byRole } from "testing-library-selector"
+
+beforeEach(() => {
+ jest.spyOn(global, "scrollTo").mockImplementationOnce(jest.fn())
+})
+
+const DiversionPage = (props: Partial) => (
+ null}
+ {...props}
+ />
+)
+
+jest.mock("../../../src/api")
+jest.mock("../../../src/userTestGroups")
+
+beforeEach(() => {
+ jest.mocked(fetchDetourDirections).mockReturnValue(neverPromise())
+ jest.mocked(fetchUnfinishedDetour).mockReturnValue(neverPromise())
+ jest.mocked(fetchFinishedDetour).mockReturnValue(neverPromise())
+ jest.mocked(fetchNearestIntersection).mockReturnValue(neverPromise())
+ jest.mocked(fetchRoutePatterns).mockReturnValue(neverPromise())
+ jest.mocked(putDetourUpdate).mockReturnValue(neverPromise())
+
+ jest
+ .mocked(getTestGroups)
+ .mockReturnValue([TestGroups.DetoursPilot, TestGroups.DetoursList])
+})
+
+const diversionPageOnActiveDetourScreen = async (
+ props?: Partial
+) => {
+ const { container } = render()
+
+ act(() => {
+ fireEvent.click(originalRouteShape.get(container))
+ })
+ act(() => {
+ fireEvent.click(originalRouteShape.get(container))
+ })
+ await userEvent.click(reviewDetourButton.get())
+ await userEvent.click(activateDetourButton.get())
+ await userEvent.click(threeHoursRadio.get())
+ await userEvent.click(nextButton.get())
+ await userEvent.click(constructionRadio.get())
+ await userEvent.click(nextButton.get())
+ await userEvent.click(activateButton.get())
+
+ return { container }
+}
+
+const nextButton = byRole("button", { name: "Next" })
+const activateButton = byRole("button", { name: "Activate detour" })
+const threeHoursRadio = byRole("radio", { name: "3 hours" })
+const constructionRadio = byRole("radio", { name: "Construction" })
+
+const activeDetourHeading = byRole("heading", { name: "Active Detour" })
+const pastDetourHeading = byRole("heading", { name: "Past Detour" })
+const returnModalHeading = byRole("heading", {
+ name: "Return to regular route?",
+})
+
+const regularRouteButton = byRole("button", { name: "Return to regular route" })
+const confirmButton = byRole("button", { name: "Confirm" })
+const cancelButton = byRole("button", { name: "Cancel" })
+
+describe("DiversionPage deactivate workflow", () => {
+ test("clicking the 'Return to regular route' button keeps existing headers on the screen", async () => {
+ await diversionPageOnActiveDetourScreen()
+
+ await userEvent.click(regularRouteButton.get())
+ expect(activeDetourHeading.get()).toBeVisible()
+
+ expect(pastDetourHeading.query()).not.toBeInTheDocument()
+ })
+
+ test("clicking the 'Return to regular route' button opens the deactivate modal", async () => {
+ await diversionPageOnActiveDetourScreen()
+
+ await userEvent.click(regularRouteButton.get())
+ expect(returnModalHeading.get()).toBeVisible()
+ })
+
+ test("clicking the 'Return to regular route' button from the the deactivate modal deactivates the detour", async () => {
+ await diversionPageOnActiveDetourScreen()
+
+ await userEvent.click(regularRouteButton.get())
+ await userEvent.click(confirmButton.get())
+
+ expect(activeDetourHeading.query()).not.toBeInTheDocument()
+ expect(pastDetourHeading.get()).toBeVisible()
+ })
+
+ test("clicking the 'Cancel' button from the the deactivate modal closes the modal", async () => {
+ await diversionPageOnActiveDetourScreen()
+
+ await userEvent.click(regularRouteButton.get())
+ await userEvent.click(cancelButton.get())
+
+ expect(activeDetourHeading.get()).toBeVisible()
+ expect(pastDetourHeading.query()).not.toBeInTheDocument()
+
+ expect(returnModalHeading.query()).not.toBeInTheDocument()
+ })
+})
diff --git a/lib/skate/detours/detours.ex b/lib/skate/detours/detours.ex
index 8e6b80231..5b6c0abd5 100644
--- a/lib/skate/detours/detours.ex
+++ b/lib/skate/detours/detours.ex
@@ -96,8 +96,11 @@ defmodule Skate.Detours.Detours do
@type detour_type :: :active | :draft | :past
@spec categorize_detour(map(), integer()) :: detour_type
- defp categorize_detour(%{state: %{"value" => %{"Detour Drawing" => "Active"}}}, _user_id),
- do: :active
+ defp categorize_detour(
+ %{state: %{"value" => %{"Detour Drawing" => %{"Active" => _}}}},
+ _user_id
+ ),
+ do: :active
defp categorize_detour(%{state: %{"value" => %{"Detour Drawing" => "Past"}}}, _user_id),
do: :past
diff --git a/test/skate_web/controllers/detours_controller_test.exs b/test/skate_web/controllers/detours_controller_test.exs
index 641e29097..fa4caa289 100644
--- a/test/skate_web/controllers/detours_controller_test.exs
+++ b/test/skate_web/controllers/detours_controller_test.exs
@@ -77,7 +77,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 1
},
- "value" => %{"Detour Drawing" => "Active"}
+ "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})
@@ -151,7 +151,7 @@ defmodule SkateWeb.DetoursControllerTest do
"routePattern" => %{"directionId" => 0, "headsign" => "Headsign"},
"uuid" => 1
},
- "value" => %{"Detour Drawing" => "Active"}
+ "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
},
"updated_at" => _
}
@@ -296,7 +296,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 4
},
- "value" => %{"Detour Drawing" => "Active"}
+ "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})
@@ -314,7 +314,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 5
},
- "value" => %{"Detour Drawing" => "Active"}
+ "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})