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