Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deactivate detour flow #2805

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions assets/src/components/detours/activeDetourPanel.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -10,15 +10,15 @@ import {
} from "../../helpers/bsIcons"
import { AffectedRoute, MissedStops } from "./detourPanelComponents"

export interface ActiveDetourPanelProps {
export interface ActiveDetourPanelProps extends PropsWithChildren {
directions?: DetourDirection[]
connectionPoints?: string[]
missedStops?: Stop[]
routeName: string
routeDescription: string
routeOrigin: string
routeDirection: string
onDeactivateDetour?: () => void
onOpenDeactivateModal?: () => void
onNavigateBack: () => void
}

Expand All @@ -30,8 +30,9 @@ export const ActiveDetourPanel = ({
routeDescription,
routeOrigin,
routeDirection,
onDeactivateDetour,
onOpenDeactivateModal,
onNavigateBack,
children,
}: ActiveDetourPanelProps) => (
<Panel as="article" className="c-diversion-panel">
<Panel.Header>
Expand Down Expand Up @@ -94,12 +95,13 @@ export const ActiveDetourPanel = ({
<Button
variant="ui-alert"
className="flex-grow-1 m-3 icon-link text-light"
onClick={onDeactivateDetour}
onClick={onOpenDeactivateModal}
>
<StopCircle />
Return to regular route
</Button>
</Panel.Body.Footer>
</Panel.Body>
{children}
</Panel>
)
32 changes: 32 additions & 0 deletions assets/src/components/detours/deactivateDetourModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react"
import { Button, Modal } from "react-bootstrap"

export const DeactivateDetourModal = ({
onDeactivate,
onCancel,
}: {
onDeactivate: () => void
onCancel: () => void
}) => {
return (
<Modal show animation={false}>
<Modal.Header role="heading">Return to regular route?</Modal.Header>
<Modal.Body>
Are you sure that you want to stop this detour and return to the regular
route?
</Modal.Body>
<Modal.Footer>
<Button variant="outline-primary" onClick={onCancel}>
Cancel
</Button>
<Button
variant="ui-alert"
onClick={onDeactivate}
className="text-white"
>
Confirm
</Button>
</Modal.Footer>
</Modal>
)
}
20 changes: 17 additions & 3 deletions assets/src/components/detours/diversionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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" },
}) ? (
<DeactivateDetourModal
onDeactivate={() =>
send({ type: "detour.active.deactivate-modal.deactivate" })
}
onCancel={() =>
send({ type: "detour.active.deactivate-modal.cancel" })
}
/>
) : null}
</ActiveDetourPanel>
) : snapshot.matches({ "Detour Drawing": "Past" }) ? (
<PastDetourPanel />
) : null}
Expand Down
29 changes: 25 additions & 4 deletions assets/src/models/createDetourMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },

Expand Down Expand Up @@ -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: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 0 additions & 12 deletions assets/tests/components/detours/diversionPage.activate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
133 changes: 133 additions & 0 deletions assets/tests/components/detours/diversionPage.deactivate.test.tsx
Original file line number Diff line number Diff line change
@@ -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<DiversionPageProps>) => (
<DiversionPageDefault
originalRoute={originalRouteFactory.build()}
showConfirmCloseModal={false}
onConfirmClose={() => 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<DiversionPageProps>
) => {
const { container } = render(<DiversionPage {...props} />)

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()
})
})
7 changes: 5 additions & 2 deletions lib/skate/detours/detours.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions test/skate_web/controllers/detours_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 1
},
"value" => %{"Detour Drawing" => "Active"}
"value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})

Expand Down Expand Up @@ -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" => _
}
Expand Down Expand Up @@ -296,7 +296,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 4
},
"value" => %{"Detour Drawing" => "Active"}
"value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})

Expand All @@ -314,7 +314,7 @@ defmodule SkateWeb.DetoursControllerTest do
"nearestIntersection" => "Street A & Avenue B",
"uuid" => 5
},
"value" => %{"Detour Drawing" => "Active"}
"value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
}
})

Expand Down
Loading